Final and ClassVar Annotations

typing.Final and typing.ClassVar answer two different questions about an attribute. Final (PEP 591) asks “can this be rewritten or overridden?” and the answer it enforces is no. ClassVar (PEP 526) asks “does this value belong to the class or to each instance?” and marks it as class-level. They are easy to confuse because both appear on class bodies and both are honored only by static analyzers, but they gate different mistakes — and they compose, since a class constant is very often both shared and immutable.

This section maps out where each annotation applies: module-level constants, Final methods that subclasses may not override, and ClassVar fields that dataclasses exclude from the generated __init__. Two focused guides drill into the details of using typing.Final for constants and methods and ClassVar vs instance attributes in dataclasses.

Where Final and ClassVar apply Module-level Final marks a constant, class-level Final blocks override, and ClassVar marks a shared attribute that dataclasses keep out of the generated init. Module constant MAX_RETRIES: Final = 5 reassign later → mypy [misc] Belongs to module Value fixed once immutable name Class Final timeout: Final = 30 subclass override → mypy [misc] Per-instance value Cannot be redefined sealed attribute ClassVar count: ClassVar [int] = 0 put in __init__ → skipped by dataclass Shared by all Not a field class-level state
Final governs whether a name can change; ClassVar governs whether it is a shared class attribute or a per-instance field.

Final: names that cannot be reassigned

Marking a name Final tells the checker it is bound exactly once. A module constant declared MAX_RETRIES: Final = 5 cannot be rebound anywhere in the module, and a checker will flag the second assignment. The value is part of the declaration; you may write Final[int] = 5 to spell the type explicitly, or let inference read 5 as the type.

Lifecycle of a Final name A Final name starts unbound, becomes bound by its single declaration, and every later assignment is rejected by the checker. unbound id: Final[int] bound once id = 5 rebind rejected mypy [misc] declare + assign assign again one legal transition; the second binding never type-checks
A Final name has exactly one legal binding; the checker rejects every assignment after it.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Final

RETRY_BUDGET: Final = 5

def escalate() -> None:
    global RETRY_BUDGET
    RETRY_BUDGET = 0   # mypy: Cannot assign to final name "RETRY_BUDGET"  [misc]

Final arrived in Python 3.8 through PEP 591; on 3.7 and earlier import it from typing_extensions, which backports the identical semantics, so a checker treats typing_extensions.Final and typing.Final interchangeably. The annotation is legal in four positions — module scope, a class body, an __init__ instance assignment, and a local variable inside a function — but it is not valid as a parameter or return annotation. Writing def f(x: Final[int]) -> None: is rejected (mypy [valid-type]), because Final describes how a name is bound, not a value that flows through a signature.

A subtle inference point: bare Final widens a literal to its ordinary type, so MODE: Final = "fast" is inferred as str for most purposes. But PEP 591 also grants a Final name bound to a literal the ability to stand in wherever a matching Literal is expected, so MODE can still drive Literal-based narrowing. When you want the exact value preserved unconditionally, spell it MODE: Final[Literal["fast"]] = "fast"; pyright infers that literal type automatically, while mypy keeps the wider str unless you ask for the literal explicitly.

Final is not restricted to constants. Applied to a method (via the @final decorator), it forbids subclasses from overriding it — a lightweight way to seal an API surface without reaching for ABCMeta. Applied to a class attribute, it forbids redeclaration in subclasses. And a Final instance attribute may be assigned exactly once, in __init__; a later self.id = ... in another method is flagged [misc]. The using typing.Final for constants and methods guide covers the reassignment, override, and stub-only cases in full.

ClassVar: attributes owned by the class

ClassVar[T] states that an attribute lives on the class rather than on each instance. The immediate payoff shows up with the @dataclass decorator: any field annotated ClassVar is excluded from the generated __init__, is not counted as a field, and keeps its class-body default as shared state. This is the correct way to attach a counter, a registry, or a configuration table to a dataclass without it becoming a constructor argument.

Ownership: ClassVar versus instance field A ClassVar is a single attribute owned by the class and shared by all instances, while a plain field is copied per instance and becomes an init parameter. class OrderRepository instances: ClassVar[int] = 0 repo A name = "orders" repo B name = "carts" instances shared, not in __init__ name → per instance one copy for all
Each instance gets its own name; the ClassVar instances is a single value shared across every instance and left out of __init__.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class OrderRepository:
    instances: ClassVar[int] = 0          # shared, not an __init__ param
    default_page_size: ClassVar[int] = 50
    name: str                             # per-instance field

    def __post_init__(self) -> None:
        OrderRepository.instances += 1

repo = OrderRepository(name="orders")     # only `name` is accepted

Trying to pass instances= to that constructor is a call error, because ClassVar fields are not parameters. The ClassVar vs instance attributes in dataclasses page walks through the shared-mutable-default pitfall this prevents.

ClassVar predates Final: it came in with PEP 526’s variable annotations in Python 3.6, and like Final it is re-exported from typing_extensions for the same semantics on older toolchains. It is only meaningful at the top level of a class body. ClassVar is rejected as a parameter or return annotation, inside a function body, or nested inside another type such as list[ClassVar[int]] — mypy reports ClassVar can only be used for assignments in class body under [misc]. The subscript is optional: bare ClassVar (without [int]) is legal and lets the checker infer the type from the assigned default.

One restriction trips people migrating generic classes: a ClassVar cannot include a class-scoped TypeVar. Because the attribute is shared across all instantiations, its type may not depend on the instance’s type parameter, so x: ClassVar[list[T]] inside a Generic[T] class is an error. Move that state to an instance field, or make it a genuinely non-generic default. Outside @dataclass the annotation is rarely required — a bare class-body assignment is already a class attribute — but checkers still honour it: assigning to a ClassVar through an instance (self.count = 1 where count is a ClassVar) is flagged, since that write would silently shadow the shared value with an instance attribute.

How they differ, and how they combine

The two annotations are orthogonal. Final constrains rebinding; ClassVar constrains ownership. A per-instance attribute can be Final (set once in __init__, never reassigned). A class attribute can be a mutable ClassVar you deliberately mutate over time — a counter is the classic example. And a shared constant that should never change is naturally both:

Final and ClassVar as independent axes A two by two grid crossing Final yes or no with ClassVar yes or no, giving four attribute kinds from a plain field to a shared sealed constant. rebinding (Final) × ownership (ClassVar) not Final Final not ClassVar ClassVar plain field name: str · per instance, mutable mutable class state count: ClassVar[int] · a counter sealed instance value self.id: Final · set once in __init__ shared sealed constant ClassVar[Final[int]]
The two axes are independent; the bottom-right cell is the shared, immutable constant that needs both qualifiers.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import ClassVar, Final

class ServiceConfig:
    PROTOCOL_VERSION: ClassVar[Final[int]] = 2   # shared AND sealed

    def __init__(self, host: str) -> None:
        self.host: Final = host                  # per-instance, set once

ClassVar[Final[int]] reads as “a class-level attribute that is also immutable.” The ordering matters and is not symmetric: ClassVar[Final[int]] is accepted, but Final[ClassVar[int]] is rejected — mypy reports an invalid type, because ClassVar must be the outermost qualifier on a class attribute. In practice you rarely need the nesting: a bare Final in a class body already behaves as a sealed class-level constant, so reach for the explicit ClassVar[Final[...]] only when a decorator such as @dataclass needs the ClassVar marker to keep the constant out of __init__ and you want the immutability guarantee spelled out.

The distinction becomes concrete when you think about what each mistake costs. Forget ClassVar on a dataclass counter and it silently becomes a constructor parameter with a shared-mutable default; forget Final on a constant and nothing stops a later refactor from rebinding it. Neither error is caught at runtime, which is exactly why the annotations exist. These annotations pair well with the exact-value contracts in Literal and TypedDict when a constant should also be one of a fixed set of values, and with Basic Type Aliases when the same sealed shape recurs across modules.

Analyzer behavior

mypy reports final-name reassignment and Final override violations under the [misc] error code, and rejects assigning to a Final instance attribute outside __init__ with [assignment] in some phrasings. It also flags a ClassVar used where an instance attribute is required. pyright folds these into reportGeneralTypeIssues, emitting messages such as “declared Final cannot be reassigned” and “ClassVar is not allowed in this context.” Neither checker needs strict mode for the core Final/ClassVar rules — they are on by default — though tightening mypy strictness surfaces related issues like redundant re-declarations.

Violations mapped to mypy codes and pyright reports Four common Final and ClassVar violations, each paired with the mypy error code and the pyright report it produces. violation mypy code pyright report reassign a Final name [misc] reportGeneralTypeIssues override a @final method [misc] reportGeneralTypeIssues pass a ClassVar to __init__ [call-arg] reportCallIssue write ClassVar via instance [misc] reportGeneralTypeIssues
mypy spreads these across specific codes; pyright collapses most of them into reportGeneralTypeIssues, using reportCallIssue for the constructor case.
# Python 3.11+, mypy 1.10 / pyright 1.1.370
from typing import ClassVar

class RetryPolicy:
    ceiling: ClassVar[int] = 10

policy = RetryPolicy()
policy.ceiling = 3   # mypy: Cannot assign to class variable "ceiling" via instance  [misc]
                     # pyright: reportGeneralTypeIssues

Two interactions matter in real code. First, a frozen dataclass (@dataclass(frozen=True)) and Final solve overlapping but different problems: frozen=True blocks all attribute assignment after construction at runtime, raising FrozenInstanceError, while Final blocks reassignment of one specific name at type-check time only. Combine them when you want both the runtime guarantee and the per-field intent spelled out; as a rule of thumb, reach for frozen=True when the runtime itself must reject mutation, and for Final when a checker-level guarantee is sufficient. Second, Final on a class attribute cooperates with @final on the class: sealing the type and sealing its constants together gives a checker enough information to treat the whole class as an immutable leaf, which sharpens exhaustiveness analysis and overload resolution in code that consumes it.

Runtime vs static analysis Neither Final nor ClassVar is enforced at runtime — they are erased to plain attributes. Python will happily let you rebind a Final constant or overwrite a ClassVar through an instance; only the type checker objects. If you need genuine runtime immutability, use @property without a setter, __slots__, or a frozen dataclass.

Common mistakes

Most Final/ClassVar errors trace back to three confusions: name versus object, field versus non-field, and where a value may legally be omitted. Three quick questions route the four most frequent ones straight to their fix.

Routing Final and ClassVar mistakes to a fix Three questions separate a container-mutation mistake, a dataclass-field mistake, and a value-omission mistake, each leading to its fix. checker rejects a Final / ClassVar line mutating contents? TAGS.append(...) a dataclass field? shows up in __init__? bare Final, no value? at module scope use tuple / frozenset Final seals the name only mark it ClassVar not a constructor arg assign on the same line unless stub / __init__
Three questions separate a name-versus-object mistake from a field or a value-omission mistake.
  • Adding a type to a bare Final after the fact. Writing TIMEOUT: Final with no value at module scope leaves the name unbound; you must assign, as in TIMEOUT: Final = 30, unless you are in a stub or __init__. mypy reports the dangling declaration as [misc], and a later attempt to “complete” it with a second assignment is itself the reassignment error.
  • Passing a ClassVar to a dataclass constructor. Because the field is not a parameter, OrderRepository(instances=1) is a call error — mypy [call-arg], pyright reportCallIssue. The fix is not to drop ClassVar but to stop passing the argument: shared state is initialised in the class body, not per call.
  • Reassigning a Final instance attribute outside __init__. Setting self.host = ... twice, or in another method, is rejected with mypy [misc] / [assignment]. If a value genuinely needs to change after construction, it should not have been Final — model it as an ordinary field instead.
  • Using ClassVar[] with a type parameter that references a TypeVar. ClassVar cannot include a class-scoped type variable, because the attribute is shared across every specialization of the generic; mypy reports [misc] and pyright reportGeneralTypeIssues. Move the state to an instance field, or use a concrete type.
  • Expecting Final to freeze a container. HEADERS: Final = {"h": "1"} seals the binding but not the dict — HEADERS["h"] = "2" raises no error at all, from either checker or interpreter. Reach for a tuple, frozenset, or an immutable mapping when the contents themselves must stay fixed.

FAQ

Does Final make an object immutable? No. Final only prevents rebinding the name. TAGS: Final = ["a"] forbids TAGS = [...] but still allows TAGS.append("b"), because the list object itself is mutable. Use an immutable type such as tuple or frozenset if you need the contents fixed too.

Can a dataclass field be both ClassVar and have a field() default? No — ClassVar attributes are not fields, so dataclasses.field() does not apply to them. Give the ClassVar an ordinary class-body value; only real per-instance fields use field(default_factory=...).

Is ClassVar required for every class attribute? No. It is only needed to disambiguate class-level attributes from instance fields, which chiefly matters inside @dataclass and similar decorators. In a plain class, a class-body assignment is already a class attribute without the annotation.

Back to Core Type Hints Fundamentals