What does typing.ClassVar mean in a class body, and how does @dataclass treat it?
answer
- Whose attribute is it, class or instance?
- A checker rejects setting it on an instance
- Skipped by the dataclass field scan
- No __init__ parameter, absent from fields()
- Exempt from the mutable-default ValueError
basics
~20 sClassVar[T] declares an attribute that lives on the class and is shared by every instance, so a checker rejects setting it on an instance. In a dataclass it is excluded from the generated init, repr and eq, and never appears in fields().
solid answer
~40 sIn a class body, `x: ClassVar[int] = 0` says the attribute belongs to the *class*: one object shared by all instances, not a per-instance value. A checker then rejects `self.x = 1` and any attempt to set it through an instance. `@dataclass` reads the annotations and skips ClassVar-annotated names entirely — they stay ordinary class attributes, take no `__init__` parameter, appear in no generated `__repr__` or `__eq__`, and are absent from `dataclasses.fields()`. That exclusion also exempts them from the mutable-default rule: `seen: ClassVar[dict] = {}` is accepted, whereas `seen: dict = {}` raises `ValueError` and demands a `default_factory`. The exemption is honest but sharp-edged — that one dict really is shared by every instance and every thread that touches the class, so it is shared mutable state, not a convenient default.
code
python · 13 linesfrom dataclasses import dataclass, field, fields
from typing import ClassVar
@dataclass
class ContigRecord:
name: str
bases: int = 0
seen: ClassVar[dict[str, int]] = {}
tags: list[str] = field(default_factory=list)
print([f.name for f in fields(ContigRecord)])
ContigRecord.seen["chr1"] = 92
print(ContigRecord("chr1", 92).seen)go deeper
Recall that a name annotated ClassVar lives on the class and is shared by every instance, while an ordinary annotated name in a dataclass body becomes a per-instance field with an __init__ parameter.
Explain the mechanics: the decorator skips ClassVar names when building __init__, __repr__, __eq__ and fields(), which is also why the mutable-default guard never fires on them.
Show the operational consequence — a class-level mutable table is process-wide shared state that outlives every request, needs an owner for bounding its growth, and needs synchronisation if more than one thread writes it.
Own the design call: decide when shared per-class state belongs on a class at all versus in an explicit collaborator you can inject, replace in tests, and reason about across processes and interpreters.
## Class attribute versus instance attribute Python has always distinguished the two: a name assigned in a class body lives on the class object and is found through every instance by attribute lookup, while a name assigned to `self` lives in that instance's own namespace. Nothing in the syntax distinguishes the *intent*, though — `registry = {}` in a class body might be a genuine class-level table or an accidentally shared default someone meant to be per-instance. `typing.ClassVar` makes the intent explicit and machine-checkable: ```python from typing import ClassVar class ContigRecord: seen: ClassVar[dict[str, int]] = {} # one dict for the whole class name: str # one per instance ``` A checker then enforces two things: the attribute may not be assigned through an instance (`record.seen = {}` and `self.seen = {}` are both errors), and it may not be overridden as an instance attribute in a subclass. Reading it through an instance stays perfectly fine, because ordinary attribute lookup falls back to the class. `ClassVar` is only meaningful in a class body. It is not allowed as a parameter annotation, a return annotation, or nested inside another type such as `list[ClassVar[int]]`, and the typing spec says it should not be combined with `Final`. As always, the interpreter does not police any of this — it builds the annotation object regardless, and only the checker objects. ## What `@dataclass` does with it This is where `ClassVar` stops being purely advisory and starts changing behaviour. The `dataclasses` decorator walks the class body's annotations to decide what counts as a *field*. Any name annotated `ClassVar[...]` is skipped: ```python from dataclasses import dataclass, field, fields from typing import ClassVar @dataclass class ContigRecord: name: str bases: int = 0 seen: ClassVar[dict[str, int]] = {} tags: list[str] = field(default_factory=list) [f.name for f in fields(ContigRecord)] # ['name', 'bases', 'tags'] ``` `seen` takes no `__init__` parameter, appears in no generated `__repr__`, is not compared by the generated `__eq__`, and is missing from `fields()`. It stays exactly what it was: a plain class attribute you set once in the class body and read through the class or any instance. The same skipping rule applies to `dataclasses.InitVar`, which is the mirror-image case — a value passed to `__init__` that becomes no attribute at all. Because a ClassVar name is not a field, it is also exempt from the guard that makes dataclasses refuse a mutable default. Writing `tags: list[str] = []` raises `ValueError: mutable default ... use default_factory`, but `seen: ClassVar[dict[str, int]] = {}` is accepted without complaint. That asymmetry is correct — a class attribute is *supposed* to be one shared object — but it is easy to read the acceptance as approval and reach for `ClassVar` just to silence the error. ## The shared-state trap Consider a genome-annotation pipeline where each `ContigRecord` is scored by a worker and a class-level `seen` dictionary accumulates per-contig counts so a later stage can compute the 92nd-percentile budget for the run. `ClassVar` is the honest annotation for that dictionary — it truly is one table for the whole class — but naming it correctly does not make it safe. Every instance, every module that imports the class, and every worker thread in the process mutates the same object; two workers updating the same key are a race on shared state, and a checker will not say a word. The annotation documents the sharing; it does not manage it. If a value should be per-instance, make it a real field with a `default_factory`. If it is genuinely global mutable state, give it a lock, or push it out of the class into something built for concurrent accumulation. A second, milder trap: a class-level mutable default survives for the process's lifetime, which in a long-running service means unbounded growth. The same annotation that documents the sharing should prompt the question "who empties this?" ## Reading it back `typing.get_type_hints()` and, on 3.14, `annotationlib.get_annotations()` return the annotation object, so `ClassVar[dict[str, int]]` is recoverable at runtime — which is exactly how `dataclasses` and the frameworks that build on annotations detect it. The declaration is therefore one of the few typing constructs that has an observable effect on ordinary Python code, and that is the reason it is worth knowing beyond checker-pleasing.
- Why does a dataclass accept ClassVar[dict] = {} but raise ValueError for a plain dict field with the same default?Because the ClassVar name is not a field at all — the decorator skips it before the mutable-default check runs, and it stays an ordinary class attribute that is meant to be one shared object. A real field's default is copied into every instance's `__init__` by reference, so a shared mutable default would silently link all instances; `dataclasses` refuses it and demands `default_factory` instead.
- Can a subclass turn a ClassVar into a per-instance attribute?Not without a checker complaining. Re-annotating the name without `ClassVar` in a subclass, or assigning it on `self`, contradicts the base declaration and is reported as an error. At runtime the assignment succeeds and shadows the class attribute for that one instance, which is precisely the confusing situation the declaration exists to prevent.
- How does @dataclass actually discover that an attribute is a ClassVar?It inspects the class body's annotations — on 3.14 through `annotationlib` — and skips any name whose annotation is `ClassVar` or a `ClassVar[...]` subscription, alongside the same treatment for `dataclasses.InitVar`. That makes `ClassVar` one of the few typing constructs with an observable runtime effect: ordinary stdlib code reads the annotation and changes what it generates.
saying these in an interview costs you the question
- Saying ClassVar gives each instance its own copy
- Believing a ClassVar becomes an __init__ parameter
- Reaching for ClassVar to silence the mutable-default ValueError
- Claiming CPython blocks instance assignment to a ClassVar
- Expecting ClassVar to appear in dataclasses.fields()