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.
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.
# 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.
# 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:
# 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.
# 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.
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.
- Adding a type to a bare
Finalafter the fact. WritingTIMEOUT: Finalwith no value at module scope leaves the name unbound; you must assign, as inTIMEOUT: 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
ClassVarto a dataclass constructor. Because the field is not a parameter,OrderRepository(instances=1)is a call error — mypy[call-arg], pyrightreportCallIssue. The fix is not to dropClassVarbut to stop passing the argument: shared state is initialised in the class body, not per call. - Reassigning a
Finalinstance attribute outside__init__. Settingself.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 beenFinal— model it as an ordinary field instead. - Using
ClassVar[]with a type parameter that references aTypeVar.ClassVarcannot include a class-scoped type variable, because the attribute is shared across every specialization of the generic; mypy reports[misc]and pyrightreportGeneralTypeIssues. Move the state to an instance field, or use a concrete type. - Expecting
Finalto 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 atuple,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.