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 cluster 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 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.
# 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 is not restricted to constants. Applied to a method, 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. 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.
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: Final may be nested inside ClassVar, but a bare Final in a class body already implies per-instance-or-class binding rules, so you only reach for the combination when you specifically want both guarantees. 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.
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
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
- Adding a type to a bare
Finalafter the fact. WritingTIMEOUT: Finalwith no value at module scope leaves the name unbound; you must assign, as inFinal = 30, unless you are in a stub or__init__. mypy:[misc]. - Passing a
ClassVarto a dataclass constructor. Because the field is not a parameter,OrderRepository(instances=1)is a call error — mypy[call-arg], pyrightreportCallIssue. - Reassigning a
Finalinstance attribute outside__init__. Settingself.host = ...twice, or in another method, is rejected with mypy[misc]/[assignment]. - Using
ClassVar[]with a type parameter that references aTypeVar.ClassVarcannot include a class-scoped type variable; mypy reports[misc]and pyrightreportGeneralTypeIssues.
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.