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

    A person reads 'Python for Unix and Linux System Administration' indoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ClassVar: Separate Class and Instance State

    Learn Python ClassVar to separate class and instance state in dataclasses, registries, caches, inheritance, and counters.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Annotated: Add Type Metadata

    Learn Python Annotated to attach metadata to types for validation, schemas, units, documentation, and framework integration.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python NewType: Keep IDs Distinct

    Learn Python NewType to keep IDs, codes, units, and primitive values distinct, validate boundaries, and prevent domain mix-ups.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close view of a python resting on sandy terrain outdoors in natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Never: Mark Unreachable Code

    Learn Python Never for non-returning functions, unreachable code, and exhaustive unions with assert_never, Literal, Enum, and match.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Concatenate: Change Parameters

    Learn Python Concatenate to add or hide leading parameters in typed decorators with ParamSpec, contexts, locks, and dependencies.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A close-up view of a person's hand signing a business contract on a desk with a pen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ParamSpec: Preserve Signatures

    Learn Python ParamSpec to preserve complete function signatures in decorators, callbacks, async wrappers, and higher-order utilities.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026