Back to Blog
Python

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...

typingstatic analysismypyPEP 591type hints
Python code editor showing a Final type hint annotation on a variable, with a lock icon representing immutability for static type checkers.

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 Final variable is an error.
  • Overriding a Final class attribute or method is an error.
  • Final cannot 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.

python final type hint: Practical Usage and Code Examples | RYUSLOG DEV