In Python, an attribute defined in a class body may represent shared configuration, a registry, a counter, a constant, or merely a default that each instance later replaces. Runtime behavior permits all of these patterns, but the ambiguity can confuse readers, dataclasses, and static checkers. typing.ClassVar explicitly declares that an attribute belongs to the class and should not be treated as an instance field.
This guide explains ClassVar in regular classes and dataclasses, inheritance, shadowing, caches, registries, counters, interaction with Final, and the risks of accidental shared mutable state.
The ambiguity problem
class User:
total = 0
name = ""total looks like a shared counter, while name looks like per-instance data. Without clear initialization and annotations, tooling cannot reliably distinguish intent.
Declaring ClassVar
from typing import ClassVar
class User:
total: ClassVar[int] = 0
def __init__(self, name: str) -> None:
self.name = name
type(self).total += 1ClassVar says that total belongs to the class. A checker can report attempts to use it like an instance field where that would create ambiguity.
Access through the class and instance
print(User.total)
user = User("Ana")
print(user.total)Python’s lookup rules allow class attributes to be read through an instance. Still, prefer User.total or type(user).total when you mean shared state. The explicit form avoids suggesting that every object owns a separate value.
Instance assignment creates shadowing
user.total = 100In a regular class, this may create an instance attribute that hides the class attribute for that object. The shared counter remains available as User.total. ClassVar helps the checker warn about this assignment, but it does not change runtime behavior.
ClassVar in dataclasses
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class Product:
default_tax: ClassVar[float] = 0.1
name: str
price: floatThe dataclass does not treat default_tax as a field. It is not added to the generated __init__, does not participate as a field in comparisons, and is not returned by dataclass field enumeration.
product = Product("Keyboard", 200.0)Without ClassVar, the attribute might become an instance field and change the generated constructor.
Mutable shared values in dataclasses
@dataclass
class Catalog:
cache: ClassVar[dict[str, object]] = {}
name: str = "main"The cache dictionary is shared by every instance. This may be intentional, but it needs a cleanup policy, concurrency decisions, and isolated tests. ClassVar does not make the object immutable or thread-safe.
Shared mutable state
Class-level lists and dictionaries live longer than individual instances and may persist across requests or tests.
class Registry:
items: ClassVar[dict[str, type]] = {}
@classmethod
def register(cls, name: str, item_type: type) -> None:
cls.items[name] = item_typeA plugin registry is a reasonable use. Request-specific data is not. Encapsulate mutations, provide reset behavior, and consider synchronization.
ClassVar with classmethod
class Counter:
value: ClassVar[int] = 0
@classmethod
def increment(cls) -> int:
cls.value += 1
return cls.valueA classmethod receives the concrete class. Subclasses may share or acquire their own value depending on where assignment occurs.
Inheritance and subclass configuration
class Base:
limit: ClassVar[int] = 10
class Premium(Base):
limit = 100Premium defines its own attribute, while other subclasses inherit Base.limit. This is useful for polymorphic configuration. If redefinition must be forbidden, consider Final instead of ClassVar alone.
Updating through cls
class Base:
calls: ClassVar[int] = 0
@classmethod
def record_call(cls) -> None:
cls.calls += 1Called on a subclass, this operation may create a new attribute on that subclass. If the count must be global for the whole hierarchy, update Base.calls explicitly or move the state to a separate object.
ClassVar and Final
ClassVar means that the attribute belongs to the class. Final means that the name should not be redefined. The intentions can overlap, but exact combinations may depend on the Python version and checker.
from typing import Final
class Protocol:
VERSION: Final[str] = "1"For a value fixed across the hierarchy, Final in the class body is often sufficient. For configurable shared state, use ClassVar. Test advanced combinations with the project’s checker.
ClassVar and properties
A property represents calculated instance access; ClassVar describes class storage or configuration. Do not use ClassVar for a normal property.
class Circle:
pi: ClassVar[float] = 3.141592653589793
def __init__(self, radius: float) -> None:
self.radius = radius
@property
def area(self) -> float:
return self.pi * self.radius ** 2ClassVar in Protocol
A Protocol can describe a class-level attribute, although detailed support can vary between checkers.
from typing import Protocol
class Serializable(Protocol):
format: ClassVar[str]
def serialize(self) -> bytes: ...The contract says implementations expose a class configuration named format. Verify compatibility with real implementations.
Factories and registries
class Converter:
_formats: ClassVar[dict[str, type["Converter"]]] = {}
def __init_subclass__(cls, *, format: str, **kwargs) -> None:
super().__init_subclass__(**kwargs)
Converter._formats[format] = cls
@classmethod
def create(cls, format: str) -> "Converter":
converter_type = cls._formats[format]
return converter_type()The registry belongs to the class family, not to individual converter objects. Refer to the base class explicitly when the map must remain unique across the hierarchy.
Class-level caches
class Parser:
_cache: ClassVar[dict[str, object]] = {}
@classmethod
def compile(cls, expression: str) -> object:
if expression not in cls._cache:
cls._cache[expression] = compile_expression(expression)
return cls._cache[expression]Consider memory limits, invalidation, concurrency, and whether subclasses should share the same cache. functools.lru_cache or an external cache service may be safer.
Instance counters
class Connection:
open_count: ClassVar[int] = 0
def __init__(self) -> None:
type(self).open_count += 1
def close(self) -> None:
type(self).open_count -= 1The logic can break with double closes, exceptions, threads, or subclassing. ClassVar describes where the state lives; it does not guarantee correctness.
ClassVar and slots
__slots__ controls instance storage. Class attributes still live on the class object. ClassVar documents the distinction but does not replace slots or change memory layout.
ClassVar without a parameter
configuration: ClassVar = {}This may be accepted, but an explicit inner type is more useful:
configuration: ClassVar[dict[str, str]] = {}Do not use ClassVar for instance defaults
@dataclass
class Task:
priority: ClassVar[int] = 1
title: str = ""If every task needs its own priority field, remove ClassVar:
@dataclass
class Task:
title: str
priority: int = 1Serialization
Dataclass serializers normally ignore ClassVar because it is not a field. Tools based on vars(instance) also omit attributes that exist only on the class. Include shared configuration explicitly when it belongs in JSON or another output.
Test isolation
Class state can leak between tests. Clear registries and caches in fixtures or expose dedicated cleanup methods:
@classmethod
def clear_cache(cls) -> None:
cls._cache.clear()Do not let test results depend on execution order.
Concurrency
ClassVar does not make operations atomic. Shared counters, registries, and caches may require locks, queues, process-safe storage, or another synchronization strategy.
When module state is clearer
If the state does not conceptually belong to a class, a private module variable may be simpler. Use ClassVar when the configuration or registry is part of the class contract or is intentionally specialized by subclasses.
When composition is better
Complex registries, caches, and counters can be independent objects injected into consumers. Composition improves isolation and testability. ClassVar is convenient, but it should not become invisible global dependency storage.
Common mistakes
- Using ClassVar for a dataclass field: it disappears from the constructor.
- Assigning through an instance: shadowing may be created.
- Sharing a mutable collection unintentionally: instances affect each other.
- Ignoring inheritance: subclasses may share or split the state.
- Expecting concurrency safety: ClassVar is only an annotation.
- Using it as a disguised global: dependencies become difficult to test.
Complete registered-codec example
from typing import ClassVar
class Codec:
_types: ClassVar[dict[str, type["Codec"]]] = {}
name: ClassVar[str]
def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
name = getattr(cls, "name", None)
if name:
Codec._types[name] = cls
@classmethod
def create(cls, name: str) -> "Codec":
try:
codec_type = Codec._types[name]
except KeyError as error:
raise ValueError(f"unknown codec: {name}") from error
return codec_type()
def encode(self, text: str) -> bytes:
raise NotImplementedError
class Utf8Codec(Codec):
name: ClassVar[str] = "utf-8"
def encode(self, text: str) -> bytes:
return text.encode("utf-8")The map is unique to the hierarchy, and each subclass publishes a class-level name. Neither value is an instance field.
Best practices
Annotate shared state explicitly. Access it through the class. Encapsulate mutation in classmethods. Document whether subclasses share or replace values. Avoid public mutable collections. Provide cleanup for tests and make deliberate concurrency decisions.
Conclusion
typing.ClassVar separates class attributes from instance fields and is especially important in dataclasses, registries, caches, counters, and hierarchy configuration. It improves static clarity but does not change Python’s lookup or mutation rules.
The official Python ClassVar documentation defines its use. Combine it with encapsulation, inheritance policies, test isolation, and synchronization when shared state is mutable.







