How do typing.Final and the @typing.final decorator differ, and does Python enforce either at runtime?
answer
- One annotates a name
- One decorates a class or method
- Advisory, never enforced by CPython
- The binding is frozen, not the object
- PEP 591, Python 3.8
basics
~20 styping.Final annotates a name that must not be rebound or redeclared. The @typing.final decorator marks a class that must not be subclassed or a method that must not be overridden. Both are checked statically only; CPython enforces neither.
solid answer
~50 sThey target different things. `MAX_WORKERS: Final = 92` says *this name* is a constant — a checker flags any later assignment to it, including a redeclaration in a subclass. `@final` decorates a *class* so that subclassing it is an error, or a *method* so that overriding it is. Both come from PEP 591 and landed in Python 3.8, and both are advisory: the interpreter happily rebinds the constant and happily builds the subclass, because annotations and this decorator do not install runtime guards. Since 3.11 `@final` does set a marker attribute on the decorated object so runtime tools can detect the intent, but nothing raises. Two traps: `Final` freezes the *binding*, never the object — a `Final` dict is still mutable — and the typing spec says not to combine `Final` with `ClassVar`, since a `Final` declared in a class body is already treated as a class-level constant.
code
python · 14 linesfrom typing import Final, final
MAX_WORKERS: Final = 92
@final
class Annotator:
def run(self) -> None: ...
class Faster(Annotator): # a type checker reports an error here
pass
MAX_WORKERS = 4 # and here
print(MAX_WORKERS, issubclass(Faster, Annotator))go deeper
Recall the split: Final marks a name as a constant, @final marks a class as not-to-be-subclassed or a method as not-to-be-overridden. Above all, say that Python itself does not stop you from doing either.
Explain the mechanics: annotations and this decorator install no runtime guard, Final constrains the binding rather than the object, and the typing spec forbids pairing Final with ClassVar in a class body.
Show when you would back the declaration with real machinery — __init_subclass__, a frozen dataclass, a read-only property — and when the checker in CI is enough on its own.
Own the policy angle: @final on library classes is a compatibility promise you are choosing not to make, so decide deliberately which classes are extension points and which are closed, and keep that consistent across the codebase.
## Two different "final"s Python's typing vocabulary uses the word twice, and interviewers ask about it precisely because the capital-F and lowercase-f spellings mean different things. `typing.Final` is an **annotation** applied to a name: ```python from typing import Final MAX_WORKERS: Final = 92 # type inferred as int TIMEOUT: Final[float] = 1.5 # or state it explicitly ``` It declares that the name is bound exactly once. A checker reports any later assignment, any augmented assignment, and any attempt to redeclare the same name in a subclass. It is legal on module-level names, class attributes, instance attributes assigned in `__init__`, and local variables. `typing.final` is a **decorator** applied to a class or a method: ```python from typing import final @final class Annotator: ... class Faster(Annotator): # a checker reports an error here ... ``` On a class it means "do not subclass this"; on a method it means "subclasses must not override this". Both spellings arrived together in Python 3.8 with PEP 591. ## Neither one is enforced by the interpreter This is the part candidates get wrong. Annotations are not runtime assertions — Python records the annotation and moves on — and the decorator returns the object unchanged. So the snippet above runs to completion: `Faster` is built, `MAX_WORKERS` is happily rebound to `4`, and no exception is raised anywhere. The only thing that complains is a static checker, an IDE, or a CI job running one. Since Python 3.11 the decorator additionally sets a marker attribute on the decorated class or function, so runtime introspection tools and frameworks can *see* the declaration if they choose to look — but seeing it is opt-in, and the standard library does not act on it. Setting the attribute can fail silently on objects that do not accept new attributes, which is why the decorator never raises. If you need real enforcement you need real machinery: a read-only `property`, a `__setattr__` that rejects writes, a frozen dataclass or a named tuple for values, and `__init_subclass__` raising `TypeError` if you truly must forbid subclassing at runtime. `Final` is a communication tool for humans and checkers; those are the runtime tools. ## `Final` freezes the binding, not the object ```python CONFIG: Final = {"threads": 92} CONFIG["threads"] = 4 # perfectly fine, statically and at runtime CONFIG = {} # a checker reports an error ``` `Final` says the *name* will keep pointing at this object. It says nothing about the object's contents. For genuine immutability you reach for a frozen dataclass, a `tuple`, a `frozenset`, or a `types.MappingProxyType` wrapper. Conflating the two is the single most common misreading of the annotation. ## `Final` in a class body PEP 591 has a rule worth knowing: a final attribute initialised in a class body is inferred by checkers to be a *class* variable, and the spec says not to annotate a name with both `Final` and `ClassVar` — the combination is redundant and rejected. If you want a per-instance constant instead, declare it `Final` in the class body without a value and assign it once in `__init__`. ## Where each earns its keep `Final` is worth writing on module-level configuration constants, on a lookup table you do not want a plugin to swap out, and on an instance attribute captured once at construction. Its real value is not the constant itself but the way it preserves a narrow inferred type: a name declared `Final` and initialised with a string literal keeps its literal type instead of widening, which makes it usable where an exact value is required. `@final` is worth writing on a class whose invariants a subclass would break — one that caches derived state, or that a serialiser or registry maps one-to-one — and on the small number of methods that are load-bearing for those invariants. It also gives a checker permission to reason more aggressively: knowing that a class has no subclasses lets it treat the class as its own exact type. Both are cheap, both are documentation with a machine reader attached, and neither should be described in an interview as something Python enforces. ## Reading the declarations back at runtime The annotation and the decorator are visible to introspection even though neither is enforced. `typing.get_type_hints()` — and on 3.14 `annotationlib.get_annotations()` — returns `Final[int]` as the recorded annotation for a constant, so a framework that wants to treat final attributes specially can. The decorator's runtime marker, added in 3.11, is set on a best-effort basis: if the decorated object refuses new attributes the decorator swallows the failure rather than raising, because its contract is to return the object unchanged. That combination — recorded, readable, never enforced — is the general shape of Python's typing constructs, and it is the right mental model to carry into any question about them. The declaration exists for humans first, for static analysis second, and for the runtime only when some other piece of code chooses to go and look.
- Does declaring a dict as Final stop code from mutating it?No. `Final` constrains the *binding*: the name must keep pointing at the same object. Mutating that object — adding a key, appending to a list — is untouched, statically and at runtime. For real immutability you use a frozen dataclass, a `tuple`, a `frozenset`, or wrap the mapping in `types.MappingProxyType`. Assuming `Final` means deeply immutable is the most common misreading of the annotation.
- If neither Final nor @final is enforced, how would you actually forbid subclassing at runtime?Define `__init_subclass__` on the class and raise `TypeError` from it, which fires whenever a subclass is created. That is the runtime equivalent of the decorator's intent. In practice most codebases do not bother: `@final` plus a checker in CI catches it before the code merges, and the runtime guard mainly buys protection against dynamically constructed classes.
- Can you annotate a class attribute as both Final and ClassVar?No — the typing specification says the two should not be combined. A final attribute initialised in a class body is already inferred to be a class variable, so `ClassVar` adds nothing and checkers report the combination as an error. If you want a constant that is per-instance rather than per-class, declare it `Final` in the class body without a value and assign it exactly once in `__init__`.
saying these in an interview costs you the question
- Saying Python raises an error when a Final name is rebound
- Claiming @final prevents subclassing at runtime
- Treating Final as deep immutability of the object
- Thinking Final and @final are the same declaration
- Using @final on a module-level constant