Python Final Type Hint: Usage and Limitations
python final type hint: Learn how to use the Final type hint in Python to mark variables and attributes as immutable for static type checkers, and understand its runti...
When you write final: int = 42 in Python, the final variable is not actually protected from reassignment. The Final type hint from the typing module signals intent to static type checkers, not to the Python interpreter. This article explains how to use python final type hint correctly, what it guarantees, and where it falls short.
What Final Means in Python's Type System
The Final annotation, defined in PEP 591, tells static type checkers that a name should not be reassigned or overridden. It works for module-level variables, class attributes, and instance attributes. The primary purpose is to prevent accidental modification during development, catching mistakes at analysis time rather than runtime.
For example, a configuration constant that should never change can be annotated as Final:
from typing import Final MAX_RETRIES: Final = 3
A type checker like mypy will report an error if you later assign a new value to MAX_RETRIES. The Python runtime, however, does nothing special with this annotation; it is just an object stored in __annotations__.
Declaring Final Variables and Attributes
You can use Final with an explicit type or let it infer from the value. Both forms are valid:
from typing import Final API_VERSION: Final[str] = "v2" # explicit type DEFAULT_TIMEOUT: Final = 30 # inferred as int
For class attributes, Final prevents subclass overrides. Consider a base class that defines a constant:
class Base: NAME: Final = "base" class Child(Base): NAME = "child" # mypy error: Cannot override final attribute
Without Final, the subclass could redefine NAME without any complaint from the type checker. Adding Final makes the intent explicit and enforces it statically.
Instance attributes can also be marked Final, but they must be assigned in __init__ or at class level. Once assigned, the type checker treats them as immutable for that instance.
Final for Methods and Overrides
The Final annotation can also be applied to methods to prevent overriding in subclasses. The syntax is slightly different: you use @final as a decorator from the typing module.
from typing import final class Service: @final def handle(self) -> None: print("handling") class SpecialService(Service): def handle(self) -> None: # mypy error: Cannot override final method print("special handling")
This is separate from the Final type hint used for variables, but it serves the same purpose: preventing unintended extension. The @final decorator also has no runtime effect; it only informs static analyzers.
Runtime Behavior: What Final Does Not Do
Because Final is purely a typing construct, the Python interpreter does not enforce immutability. You can still reassign a variable annotated with Final and the program will run without error:
from typing import Final COUNT: Final = 10 COUNT = 20 # no runtime error, but mypy flags it print(COUNT) # prints 20
This is a common point of confusion. If you need runtime enforcement, you must implement it yourself, for example with a custom descriptor or a property that raises on set. The Final hint is not a substitute for runtime guards.
Another runtime aspect: Final does not affect the value itself. It only marks the name binding. If the value is a mutable object, like a list, the object can still be modified:
from typing import Final ITEMS: Final = [1, 2, 3] ITEMS.append(4) # allowed, because the list object is mutable
Final prevents reassigning ITEMS to a new list, but it does not make the list immutable. For that, you would need Sequence or tuple.
Static Type Checking and Tooling Support
The main benefit of Final appears when you use a static type checker. mypy, Pyright, and Pyre all support PEP 591. Each tool may have slightly different behavior, but the core rules are consistent:
- Reassigning a
Finalvariable is an error. - Overriding a
Finalclass attribute or method is an error. Finalcannot be used with local variables inside functions; it is intended for module-level and class-level names.
For local variables, the type checker already assumes they can be reassigned, and there is no need to mark them. If you try to use Final on a local variable, mypy will report an error like Final can be only used for assignments at module or class level.
Tooling support is mature, but you need to enable strict mode in mypy to get the full benefit. In loose mode, some violations may be ignored. For production code, consider using --strict to enforce these annotations consistently.
Common Misconceptions and Edge Cases
One misconception is that Final makes a value immutable. As shown earlier, it only prevents rebinding. Another is that Final works at runtime; it does not. There is also confusion about using Final with dataclasses or NamedTuple. For example, a dataclass field annotated with Final is still mutable at runtime; the type checker will complain if you assign to it, but the dataclass itself does not enforce immutability.
An edge case: Final with a type alias. You can combine it with TypeAlias to define a constant that is also a type:
from typing import Final, TypeAlias ID: TypeAlias = int MAX_ID: Final = 1000
This is valid, but it is usually clearer to keep Final for values and TypeAlias for types.
Another edge case: Final in abstract base classes. You can mark an abstract method as final, but that combination is unusual. The abstract method must be overridden, yet final prevents overriding. This creates a contradiction that type checkers will flag. Avoid combining @abstractmethod with @final on the same method.
Practical Guidelines for Using Final in Real Projects
Use Final for constants that are truly fixed, such as configuration values, error codes, or default settings. This communicates intent to other developers and prevents accidental reassignment during refactoring. It also helps static analyzers catch bugs before they reach production.
When working with a codebase that does not use static type checking, Final adds little value because the annotation is invisible at runtime. In that case, consider using a NamedTuple or a module with __slots__ to enforce immutability at runtime if needed.
Be aware that Final is not a runtime performance optimization. It does not change how Python executes code. The only cost is the minimal overhead of storing the annotation in __annotations__, which is negligible. If you are concerned about memory, note that Final does not create extra objects beyond the annotation itself.
Finally, keep your use of Final consistent across the codebase. If you mark some constants as Final but leave others unannotated, the type checker cannot enforce a uniform policy. Establish a convention: every module-level constant that is never reassigned should be marked Final. This makes the code easier to reason about and reduces the chance of subtle bugs when a constant is accidentally overwritten.