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.
Final works on constants, methods, and single-assignment attributes — each rejecting a second binding at type-check time.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.
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.
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.
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.
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
- 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. - Expecting
Finalto freeze a container’s contents.HEADERS: Final = {"h": "1"}blocks rebindingHEADERSbut notHEADERS["h"] = "2". No error is raised for the mutation; useMappingor atupleif the contents 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. - Assigning a
Finalinstance attribute in two methods. Only the single__init__assignment is allowed; a second assignment elsewhere trips mypy[misc].
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.