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.

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.

# 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
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

  • 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 Final = 30, unless you are in a stub or __init__. mypy: [misc].
  • 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.
  • Reassigning a Final instance attribute outside __init__. Setting self.host = ... twice, or in another method, is rejected with mypy [misc] / [assignment].
  • Using ClassVar[] with a type parameter that references a TypeVar. ClassVar cannot include a class-scoped type variable; mypy reports [misc] and pyright reportGeneralTypeIssues.

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