Python Final: Protect Constants and Inheritance

Published on: August 29, 2026
Reading time: 5 minutes
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

Python allows names to be reassigned, methods to be overridden, and most classes to be subclassed. That flexibility is valuable, but some parts of an API should remain stable: constants should not receive another value, selected instance attributes should be assigned only once, certain methods should not be overridden, and some classes were not designed for inheritance. The typing module provides Final and the @final decorator to express these intentions to static checkers.

This guide covers module constants, class and instance attributes, the difference between Final and real immutability, dataclasses, configuration, final methods and classes, runtime enforcement, and common design mistakes.

A module constant

from typing import Final

MAX_ATTEMPTS: Final[int] = 3
API_URL: Final = "https://api.example.com"

The first form declares both the type and final intent. The second lets the checker infer str. A later reassignment should be reported:

MAX_ATTEMPTS = 5  # type-checking error

Python can still execute that assignment at runtime. Final is a static contract, not an automatic interpreter-level restriction.

Final does not freeze the object

ROUTES: Final[list[str]] = ["/home"]
ROUTES.append("/help")  # allowed
ROUTES = []              # should be rejected

Final prevents rebinding the name, but it does not make the referenced list immutable. For an immutable structure, choose an appropriate runtime representation:

FIXED_ROUTES: Final[tuple[str, ...]] = ("/home", "/help")

Final protects the name statically, while the tuple provides structural immutability at runtime.

Naming conventions and Final

Uppercase constants are only a convention. Final adds a rule that a checker can verify.

DEFAULT_TIMEOUT: Final[float] = 5.0

Use both: uppercase communicates intent to readers, and Final communicates it to tooling.

Class attributes

class ProtocolVersion:
    VERSION: Final[str] = "1.0"

A subclass should not redefine a Final attribute:

class NewProtocol(ProtocolVersion):
    VERSION = "2.0"  # expected static error

If subclass-specific versions are legitimate, do not mark the attribute Final. The annotation must reflect a real hierarchy invariant.

Instance attributes assigned once

class Session:
    id: Final[str]

    def __init__(self, id_value: str) -> None:
        self.id = id_value

A later assignment should be rejected:

session = Session("abc")
session.id = "xyz"  # type error

This still does not block assignment at runtime. Use a read-only property, a frozen dataclass, or custom __setattr__ logic when runtime enforcement matters.

Where to initialize a Final attribute

A Final attribute should be initialized clearly and exactly once, usually in the class body or __init__. Complex conditional assignment may be harder for checkers to understand.

class Request:
    token: Final[str]

    def __init__(self, token: str | None) -> None:
        value = generate_token() if token is None else token
        self.token = value

Computing the value first and assigning once is often the clearest pattern.

Final in dataclasses

from dataclasses import dataclass

@dataclass
class Event:
    id: Final[str]
    payload: dict[str, object]

Support may vary between checkers because the dataclass generates __init__. For runtime immutability, use @dataclass(frozen=True):

@dataclass(frozen=True)
class ImmutableEvent:
    id: str
    payload: dict[str, object]

A frozen dataclass still does not freeze mutable objects stored inside it. The payload dictionary can be changed unless an immutable mapping is used.

Final and ClassVar

ClassVar says an attribute belongs to the class rather than instances. Final says it should not be redefined. Combining them may have syntax or checker limitations depending on the supported Python version. A Final annotation in the class body often communicates enough.

Use ClassVar without Final when subclasses are expected to configure the value. Use Final when the value is fixed across the hierarchy and verify behavior with the project’s checker.

Final and Literal

Final protects a name; Literal describes exact possible values.

from typing import Literal

DEFAULT_MODE: Final[Literal["safe"]] = "safe"

This combination is rarely necessary for a simple constant. Literal is especially useful in parameters and return types, while Final is useful for definitions that should not change.

Final and NewType

from typing import NewType

UserId = NewType("UserId", int)
SYSTEM_USER: Final[UserId] = UserId(1)

NewType preserves the semantic identity of the value and Final prevents static reassignment of the name.

The @final decorator on methods

from typing import final

class Authenticator:
    @final
    def verify_signature(self, token: str) -> bool:
        return verify(token)

A subclass should not override the method:

class CustomAuthenticator(Authenticator):
    def verify_signature(self, token: str) -> bool:
        return True  # expected static error

The decorator communicates that the algorithm is a fixed part of the contract. Use it selectively because preventing overrides reduces extensibility.

@final on classes

@final
class InternalToken:
    def __init__(self, value: str) -> None:
        self.value = value

A checker should reject subclassing:

class SpecialToken(InternalToken):  # error
    pass

This can be appropriate when subclassing could break invariants, when the implementation relies on internal optimizations, or when composition is the intended extension model.

@final does not block inheritance at runtime

The typing decorator does not normally stop the interpreter from creating a subclass. Modern versions may set a reflection marker such as __final__, but enforcement still depends on tools or custom runtime logic.

class ClosedClass:
    def __init_subclass__(cls) -> None:
        raise TypeError("subclassing is not allowed")

Use runtime blocking only when it is truly required, because it changes normal Python behavior.

Template methods with final steps

class Importer:
    def run(self, path: str) -> None:
        data = self.read(path)
        data = self.transform(data)
        self.save(data)

    def read(self, path: str) -> bytes:
        raise NotImplementedError

    def transform(self, data: bytes) -> bytes:
        return data

    @final
    def save(self, data: bytes) -> None:
        write_with_audit(data)

Reading and transformation remain extension points, while saving is fixed because auditing is mandatory.

Final in Protocol

Protocol describes structural behavior, while Final and @final primarily constrain nominal implementations and inheritance. A structurally compatible class does not have to inherit from the Protocol, so final markers on the Protocol often provide limited value.

Final and read-only properties

A property without a setter gives practical runtime protection:

class Account:
    def __init__(self, number: str) -> None:
        self._number = number

    @property
    def number(self) -> str:
        return self._number

The internal storage can also be annotated Final to reinforce static intent.

Runtime-loaded configuration

A value may become final after initialization even when it comes from an environment variable or file.

class Configuration:
    environment: Final[str]
    timeout: Final[float]

    def __init__(self) -> None:
        self.environment = read_environment()
        self.timeout = read_timeout()

Final does not require a compile-time constant. It requires that the attribute not be reassigned after initialization.

Imports and monkey patching

Importing a Final name into another module does not prevent dynamic changes. Python also allows module and class attributes to be monkey patched. Final communicates that such changes violate the contract but does not disable them. Prefer explicit dependency-injection points in tests instead of patching final members.

When not to use Final

  • Subclasses are expected to configure the value.
  • Reassignment is part of the lifecycle.
  • The public API is intentionally extensible.
  • You need object immutability rather than name stability.
  • The project does not run a static checker.

Common mistakes

  • Confusing Final with a runtime const: Python still permits assignment.
  • Annotating a list and expecting it to freeze: the contents remain mutable.
  • Marking every method final: the hierarchy becomes difficult to extend.
  • Marking configurable values final: the annotation contradicts the design.
  • Using unclear initialization paths: checkers may reject or misinterpret them.
  • Treating Final as security: permissions and data still need runtime validation.

Complete stable API client example

from typing import Final, final

DEFAULT_URL: Final[str] = "https://api.example.com"

class ApiClient:
    url: Final[str]
    _version_header: Final[str] = "X-Api-Version"

    def __init__(self, url: str = DEFAULT_URL) -> None:
        self.url = url.rstrip("/")

    def get(self, path: str) -> bytes:
        return self._send("GET", path)

    @final
    def _send(self, method: str, path: str) -> bytes:
        headers = {self._version_header: "1"}
        return http_transport(
            method,
            f"{self.url}/{path.lstrip('/')}",
            headers=headers,
        )

class CachedClient(ApiClient):
    def get(self, path: str) -> bytes:
        if data := cache_get(path):
            return data
        data = super().get(path)
        cache_set(path, data)
        return data

Each instance URL is assigned once, the header name is fixed across the hierarchy, and low-level sending cannot be overridden. The high-level get() operation remains extensible for caching.

Testing the contract

Run a checker in continuous integration and keep type-checking fixtures for invalid reassignments and overrides. Runtime tests should verify real immutability only when it is implemented through tuples, read-only properties, frozen dataclasses, or other concrete mechanisms.

Conclusion

typing.Final protects names and attributes from reassignment in the static contract, while @final prevents method overriding and class inheritance for checkers. They document invariants and reduce accidental extension.

The official Python Final documentation and final decorator documentation define the semantics. Combine them with immutable structures or runtime controls when the rule must be enforced by the program itself.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python code with type annotations on a laptop
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Read Annotations Safely

    Learn Python annotationlib to read annotations as values, strings, or forward references safely.

    Ler mais

    Tempo de leitura: 5 minutos
    25/10/2026
    ZIP file compression icon
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ZipInfo.compress_level: Control ZIP Compression

    Learn Python ZipInfo.compress_level to choose compression per ZIP member and balance size, CPU, latency, and compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    24/10/2026
    Binary code representing Python bytearray.resize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    bytearray.resize: Resize Buffers Safely

    Learn Python bytearray.resize for safe buffer growth and shrinking, limits, memoryview handling, and compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    24/10/2026
    Python code on a laptop for filtering None values
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.is_none: Filter None Without Losing Falsy

    Learn operator.is_none and is_not_none to filter None values without removing valid falsy data in Python.

    Ler mais

    Tempo de leitura: 4 minutos
    23/10/2026
    Python code on a laptop for creating dynamic dataclasses
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    make_dataclass decorator: Transform Dataclasses

    Learn how the make_dataclass decorator parameter validates, registers, and transforms dynamic dataclasses safely in Python.

    Ler mais

    Tempo de leitura: 5 minutos
    23/10/2026
    Code and XML structure for canonicalization with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree canonicalize: Normalize XML in Python

    Learn Python ElementTree canonicalize for stable XML comparisons, hashes, namespaces, comments, and signature preparation.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026