Using typing.Final for Constants and Methods
TL;DR — Annotate a module or class constant with Final to turn any later reassignment into a type error, and put Final on a method to stop subclasses overriding it. The value goes in the declaration (RETRY_LIMIT: Final = 3 or the explicit Final[int] = 3); you may omit the value only in a stub file or a bare class-body declaration that __init__ later assigns.
typing.Final from PEP 591 is the checker-enforced way to say “this is bound once and stays bound.” It has three distinct jobs — sealing a constant, sealing a method, and sealing a class attribute against redeclaration — and they share one rule: a checker treats a second binding as an error, while the interpreter shrugs. This page is the practical tour of each job with the exact error codes you will see.
Sealing a constant
The most common use is a module-level constant. The declaration carries the value, and any subsequent assignment to that name — anywhere in the module — is flagged.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Final
CONNECT_TIMEOUT: Final = 30 # type inferred as int
API_ROOT: Final[str] = "/v2" # explicit element type
def reconfigure() -> None:
global CONNECT_TIMEOUT
CONNECT_TIMEOUT = 60 # mypy: Cannot assign to final name [misc]
# pyright: reportGeneralTypeIssues
You can write the type two ways. Bare Final infers the type from the assigned value, which is usually what you want for a literal. Final[int] states the element type explicitly — useful when the inferred type would be too narrow or too wide. Writing RETRY_LIMIT: Final[int] = 3 is the canonical fully-spelled form.
The single-binding rule is enforced lexically across the whole module, not just at top level. A global declaration buys no exception: the reconfigure() function above fails precisely because CONNECT_TIMEOUT appears on the left of = a second time, even though that reassignment sits inside a function the module may never call. mypy performs no reachability analysis to excuse it — the mere presence of the second binding is the error, so a rebinding hidden inside if False: is reported just the same.
There is exactly one place a module-level Final may legally appear without a value: a .pyi stub file. A stub describes an interface, so DEBUG: Final[bool] with no right-hand side is exactly right there — it tells consumers the name exists, is a bool, and will not be rebound, while the real value lives in the implementation module. In a regular .py module the identical line is instead an “unbound name” error, because nothing ever assigns it.
Final also composes with Literal to pin an exact value into the type system. HTTP_OK: Final = 200 is inferred as int by mypy, but HTTP_OK: Final[Literal[200]] = 200 records the value 200 in the type, which lets the constant drive exhaustiveness checks and Literal-based overload selection elsewhere. pyright infers that literal type from a bare Final automatically; mypy keeps the wider int unless you spell the Literal out. That single difference often explains why an exhaustiveness check passes under one checker and fails under the other.
Final behaves specially inside a @dataclass. A class-level x: Final[int] = 3 is still a field, but one the checker forbids reassigning; unlike ClassVar, it stays a per-instance field and an __init__ parameter unless the surrounding design makes it a shared constant. When you want a genuinely shared, sealed dataclass constant, combine the two as ClassVar[Final[int]] so it is both kept out of __init__ and sealed against rebinding. Final also pairs naturally with an Enum: DEFAULT: Final = Color.RED records that the module default never changes while preserving the precise Color type, and because enum members are singletons, the pairing buys both value-stability and identity-stability in one line.
Sealing a method against override
A subclass normally may replace any method. @final from typing — the decorator sibling of the Final annotation — forbids that. It is how you sell a guarantee: “if you subclass PaymentGateway, you cannot change how settle() behaves.”
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import final
class PaymentGateway:
@final
def settle(self) -> None:
...
class StripeGateway(PaymentGateway):
def settle(self) -> None: # mypy: Cannot override final attribute "settle" [misc]
... # pyright: reportGeneralTypeIssues
The same @final decorator applied to a class forbids subclassing it entirely. mypy reports an attempt to subclass a @final class with [misc] as well. Use the class form when a type is a leaf by design — for example a sealed error type or a value object.
The method seal is checked across the whole MRO: it is an error for any subclass, however deep, to define an attribute of that name — not only a direct child — and the check also fires if a subclass shadows the method with a plain attribute such as settle = None, since that too replaces the sealed behavior. What @final does not do is constrain the method’s own body or forbid @overload variants; it restricts descendants only. Both the decorator and the annotation arrived in Python 3.8 with PEP 591, and from typing_extensions import final covers 3.7. A @final class is also a gift to exhaustiveness checking: because its set of subtypes is closed, a checker can prove a chain of isinstance branches is complete, and mypy additionally uses @final to sharpen Self return types and to accept certain covariant overrides it would otherwise reject.
When you need both a sealed method and a documented extension point, split the responsibilities: keep the orchestration method @final and have it call a separate, overridable hook method. This is the template-method shape the standard library uses widely — a fixed public entry point that guarantees ordering and invariants, delegating the variable part to a method subclasses are meant to replace. @final has no purpose on a bodyless method in a Protocol or stub, so keep it on concrete implementations where there is real behavior to protect.
Sealing a class attribute
A Final class attribute cannot be redeclared in a subclass, and a Final instance attribute may be assigned exactly once, in __init__. This is where the “omit the value” rule matters: at the class body you may declare id: Final[int] with no value, provided __init__ assigns it exactly once.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Final
class UserRecord:
id: Final[int] # declared, not yet assigned — allowed
def __init__(self, uid: int) -> None:
self.id = uid # the one permitted assignment
def rekey(self, uid: int) -> None:
self.id = uid # mypy: Cannot assign to final attribute "id" [misc]
Omitting the value is legal in exactly two places: a bare class-body declaration that __init__ completes, and a .pyi stub file, where declarations never carry values. Everywhere else a bare Final with no assignment is itself an error.
The requirement that __init__ bind the attribute is stronger than “somewhere in the constructor”: the checker wants the assignment on every path. If __init__ sets self.id only inside an if branch, mypy reports the attribute as possibly unbound, so assign it unconditionally or resolve the branch before the single assignment. A subclass, meanwhile, may not re-annotate an inherited Final attribute even to the identical type — that counts as redeclaration and is rejected, which is exactly what makes a Final class attribute sealed across a hierarchy. Keep the two omission sites distinct: the class-body form is a deferred binding that __init__ must complete, whereas the stub form is a pure interface declaration never assigned in that file. Leaving a class-body Final unassigned in a real module produces an unbound-attribute error, not the sealing you intended.
Analyzer behavior
mypy routes essentially every Final violation — reassigned constant, overridden method, redeclared attribute — through the [misc] error code, occasionally [assignment] for attribute rebinding. pyright collapses them into reportGeneralTypeIssues with descriptive text. Both are active without strict mode. ruff does not have a dedicated Final correctness rule, so it will not catch these; keep them in your mypy configuration gate. For the broader contrast between Final (rebinding) and ClassVar (ownership), see the Final and ClassVar overview.
The [misc] bucket is deliberately broad, with a practical downside: a blanket # type: ignore[misc] on a line would suppress unrelated [misc] diagnostics too, so prefer fixing the binding to muting it. pyright’s reportGeneralTypeIssues is likewise a catch-all; if a legacy module genuinely needs relief you can lower it to "warning" in pyrightconfig.json rather than scattering per-line ignores. Neither checker needs strict mode for these rules, but strict mode surfaces adjacent problems — such as an unannotated helper that quietly returns Any and defeats a Final[Literal[...]] you were relying on for narrowing.
A practical CI note: because Final violations route through [misc] in mypy, they are easy to lose in a noisy baseline. When adopting Final across an existing codebase, enable warn_unused_ignores so stale suppressions surface, and grep for the message text (Cannot assign to final) rather than filtering by code, since [misc] is shared with many unrelated diagnostics. pyright users can raise reportGeneralTypeIssues to "error" in pyrightconfig.json to make every breach fail the build. Keep the two forms straight on any review checklist — Final the annotation seals a name, @final the decorator seals a method or class — and remember that a .pyi stub is the one place a module-level Final may omit its value, so a stub can declare VERSION: Final[str] while the implementation module assigns it.
Final and @final are erased at runtime — Python neither prevents the reassignment nor raises on a subclass that overrides a @final method. The guarantee exists only while a checker runs. For a name that must resist mutation at runtime, expose it through a read-only @property or store it on a frozen dataclass; Final alone is a promise to the checker, not the interpreter.
Common mistakes
These four account for the overwhelming majority of Final errors that surface in review. Each has a one-line fix once you recognise which one you have hit.
- Declaring a bare
Finalat module scope with no value.DEBUG: Finaloutside a stub or__init__leaves the name unbound and is an error — mypy[misc]. Assign the value on the same line; the deferred form is only for a class body that__init__completes. - Expecting
Finalto freeze a container’s contents.HEADERS: Final = {"h": "1"}blocks rebindingHEADERSbut notHEADERS["h"] = "2". This is the trap that produces no error from either checker or interpreter, so it hides in plain sight; annotate withMapping,Sequence, atuple, or afrozensetwhen the contents themselves must be fixed. - Overriding a
@finalmethod or subclassing a@finalclass. Both are reported with mypy[misc]/ pyrightreportGeneralTypeIssues; the seal is intentional, so remove the override rather than suppressing it. If you genuinely need to specialise the behavior, the base method should not have been@final. - Assigning a
Finalinstance attribute in two methods. Only the single__init__assignment is allowed; a second assignment elsewhere trips mypy[misc]. A value that must change after construction is by definition not final — model it as an ordinary field instead.
FAQ
When would I omit the value on a Final declaration?
Only in a stub file, or on a class-body attribute that __init__ assigns exactly once. In every other position a Final name must be given its value immediately, or the checker reports it unbound.
Is Final[int] = 3 different from Final = 3?
Only in the declared element type. Final = 3 infers int from the literal; Final[int] = 3 states it explicitly. The immutability guarantee is identical — reach for the explicit form when inference would otherwise be too narrow (for example a Literal[3]) or too wide.