Python ClassVar: Separate Class and Instance State

Published on: August 29, 2026
Reading time: 5 minutes
A person reads 'Python for Unix and Linux System Administration' indoors.

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 += 1

ClassVar 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 = 100

In 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: float

The 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_type

A 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.value

A 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 = 100

Premium 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 += 1

Called 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 ** 2

ClassVar 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 -= 1

The 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 = 1

Serialization

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python overload: Precise Function Signatures

    Learn Python overload for precise signatures with Literal, None, generics, methods, and return types that depend on arguments.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Final: Protect Constants and Inheritance

    Learn Python Final and @final to protect constants, attributes, methods, and classes while understanding runtime limitations.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Annotated: Add Type Metadata

    Learn Python Annotated to attach metadata to types for validation, schemas, units, documentation, and framework integration.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python NewType: Keep IDs Distinct

    Learn Python NewType to keep IDs, codes, units, and primitive values distinct, validate boundaries, and prevent domain mix-ups.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close view of a python resting on sandy terrain outdoors in natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Never: Mark Unreachable Code

    Learn Python Never for non-returning functions, unreachable code, and exhaustive unions with assert_never, Literal, Enum, and match.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Concatenate: Change Parameters

    Learn Python Concatenate to add or hide leading parameters in typed decorators with ParamSpec, contexts, locks, and dependencies.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026