Class-level attributes behave in ways that surprise programmers coming from C++ or Java.
A field declared in the class body, outside any method, is a class attribute. A class attribute is easy to misread as a per-object default value. It is not one.
A class attribute creates a single shared variable across all instances of the class.
If you then create an instance attribute of the same name,
that instance attribute shadows the class attribute. In
C++ or Java, the language allocates storage for such a field in
each object before the constructor runs, so a programmer from
those languages expects per-object storage here too. A Python
class attribute corresponds to a C++ or Java static
field. Python has no syntax for declaring a per-object field in
the class body. Assigning through self inside a
method creates that storage instead.
The next listing shows the confusion:
# class_attribute_confusion.py
class Stars:
rating = 5 # Shared across all instances
a, b = Stars(), Stars()
print(a.rating, b.rating) # Both read the same storage
#: 5 5
a.rating = 1 # Assigning makes an instance attribute on 'a'
# 'a' shadows it, 'b' sees the class
print(a.rating, b.rating)
#: 1 5
Stars.rating = 9 # Change the shared storage
print(a.rating, b.rating) # 'b' reads the class attribute
#: 1 9An instance and its class each have their own attribute
dictionary. Reading an attribute checks the instance first, then
falls back to the class. Assigning through an instance always
writes to the instance, creating the instance attribute on first
assignment. Assigning through the class name, as
Stars.rating = 9 did, changes the shared value.
vars() returns an object’s own attribute
dictionary, so inspecting the class with vars(A)
and the instance with vars(a) shows the split:
# inside_objects.py
class A:
x = 100 # Class attribute
a = A()
print(vars(A)["x"]) # The attribute lives in the class dict
#: 100
print(vars(a)) # The instance has no attributes yet
#: {}
a.x = 1
print(vars(a)) # Assignment created it on the instance
#: {'x': 1}
print(vars(A)["x"])
#: 100The listing subscripts vars(A) instead of
printing it whole, because a class’s dictionary is a read-only
mappingproxy that carries the compiler’s own
bookkeeping alongside x. The instance dictionary is
a plain dict holding only what the code
assigned.
That instance dictionary is not guaranteed. A class declared
with slots=True (Performance
shows the trade-off) has no instance __dict__ at
all. A class attribute in such a class cannot be shadowed:
assigning to that name on an instance raises an
AttributeError, because there is no instance
dictionary to write into. The rest of this chapter assumes an
ordinary class, one with an instance __dict__.
A method is a class attribute like any other.
def show(self): in a class body stores a function
object in the class dictionary, and a.show() finds
it by the same fallback that finds a.x: nothing on
the instance, so look at the class.
display_object(), the inspection helper first used
in Classes, reports
attributes and methods separately, but both live in the same
class dictionary. That is why assigning
a.show = something would shadow the method for
a alone.
One kind of class attribute follows a different rule. A
@property from Classes owns
its name on the class, so reading calls its getter and assigning
calls its setter, and neither one touches the instance
dictionary. The rest of this chapter covers ordinary values
stored in a class body.
A class attribute reads like a default right up until someone assigns to an attribute of the same name on one instance. After that, a change to the class attribute reaches every other object, while the object that assigned keeps its own value. The bug surfaces far from the line that caused it, often in a different function that never mentions the assignment:
# far_from_the_cause.py
class Stars:
rating = 5 # Shared across all instances
def sell(star: Stars) -> None:
star.rating = 1 # Shadows, buried in a helper
def show(star: Stars) -> None:
print(star.rating) # Reads far from where it shadowed
a, b = Stars(), Stars()
sell(a)
show(a)
#: 1
show(b) # sell() never touched b
#: 5sell() and show() share no line of
source between them. A reader debugging show(b)’s
surprising 5 has to trace back through every
earlier call that touched a Stars instance, because
the shadowing happened inside sell(), a function
show() never calls and does not import.
The shadowing rule confines a change to one object only while the shared value is immutable:
a.items.append("apple") never assigns to
a.items. It reads items, finds nothing
on a, falls back to the class, and mutates the one
list stored there. The mutation creates no instance attribute,
so b sees the apple too. The next line does assign,
and that assignment creates a.items on the instance
and shadows the class list, leaving b still reading
the shared one. Shadowing starts with an assignment, and
.append() makes none, so a read followed by a
mutation slips past the rule. A type checker accepts the line
too: a.items.append("apple") is a correct call on a
list[str]. Real
Per-Object Defaults, at the end of this chapter, gives each
object its own value instead. A mutable default belongs in a
@dataclass field with a
default_factory, covered in Data
Classes as Types.
When you genuinely want one shared value, say so with
ClassVar from typing. The type checker
then treats the attribute as class-wide, and rejects the
instance assignment that would shadow it:
# class_var.py
from typing import ClassVar
from display import display_object
class Tally:
total: ClassVar[int] = 0 # A single shared value
label: str # Declared, not yet assigned
def __init__(self, label: str) -> None:
self.label = label
Tally.total += 1
display_object(Tally)
#: [Attributes]
#: • total: typing.ClassVar[int] = 0 [CV]
#: [Methods]
#: None
a = Tally("a")
display_object(a)
#: [Attributes]
#: • label: str = 'a'
#: • total: typing.ClassVar[int] = 1 [CV]
#: [Methods]
#: None
b = Tally("b")
print(Tally.total)
#: 2
# a.total = 99 # ty: Cannot assign to ClassVar `total`display_object(Tally) shows what the class
holds: total, and nothing called
label. The [CV] tag, for class
variable, marks an attribute the class stores. An
assignment in the class body creates a class attribute, as class_attribute_confusion.py
showed. total: ClassVar[int] = 0 has the
= 0, so it exists on Tally before any
instance exists. label: str has no =,
so the class stores nothing under that name. The annotation
records, in Tally.__annotations__, that a
Tally will carry a label, and nothing
more. display_object() reports attributes that
exist, so the declaration stays out of its report.
display_object(a) tells a different story once
an instance exists. Both label and
total appear: label: str = 'a' and
total: typing.ClassVar[int] = 1. Constructing
a runs self.label = label, which
creates a real label attribute on a,
not on Tally. total shows up too, by
fallback. Reading an attribute checks the instance first, then
the class, the rule Stars demonstrated in class_attribute_confusion.py.
a holds no copy of its own. The tags agree.
label, stored on a, carries no
[CV], while total, found by fallback,
keeps it.
A bare annotation, one with no assigned value, is a
declaration rather than a placeholder. It states that instances
of this class carry a label attribute of type
str, set somewhere. Here that somewhere is
__init__(), and its self.label = label
produced the attribute display_object(a) found. If
you leave that assignment out of __init__(), no
attribute exists, on the instance or the class. The type checker
trusts the annotation rather than checking that some method sets
the attribute, so the check passes. The first code that reads
label raises an AttributeError.
The annotation on label is optional here. If you
delete it, ty still infers label: str
correctly from self.label = label, because the
parameter’s own type carries through to the attribute it
initializes. The annotation stays for symmetry with
total, so both names read together at the top
instead of one hiding inside the constructor. Simulation
shows the case that requires the annotation: code outside the
class sets the attribute, and the bare annotation is the type
checker’s one source for its type.
ClassVar
CatchesClassVar is a hint for the type checker. It
records that total belongs to the class, and turns
the accidental shadowing from class_attribute_confusion.py
into a check-time error. Python’s own attribute lookup ignores
the hint. One library does read it at runtime:
@dataclass leaves a ClassVar field out
of the constructor it generates, as Data
Classes as Types shows.
At runtime an assignment does the same thing with or without
ClassVar:
# counter_near_miss.py
from typing import ClassVar
class Tally:
total: ClassVar[int] = 0
def __init__(self) -> None:
self.total += 1 # type: ignore
a, b = Tally(), Tally()
print(a.total, b.total, Tally.total)
#: 1 1 0self.total += 1 expands to
self.total = self.total + 1. The read falls back to
the class and finds 0. The write creates a fresh
total on the instance. Every Tally
counts itself once and the shared counter never moves. That is
why class_var.py increments
through the class name, Tally.total += 1.
ClassVar does save you here, at check time:
ty rejects the augmented form as it rejects a
direct self.total = 5, reporting “Cannot assign to
ClassVar total from an instance of type
Tally” for a write like a.total = 99,
and naming the type of self where the write sits
inside __init__(). The # type: ignore
suppresses that report so the listing can show what the line
does when it runs.
Shared storage is right when you intend the sharing. A count
of every object created, a registry mapping names to classes,
and a constant that all instances read but none change are all
class attributes, and each reads better when you declare the
sharing. Tally.total is the first of these. For the
third, a class-level constant, Final[int] from Static
Types says more than ClassVar[int]: it declares
the value shared and not reassignable. Use
ClassVar when you intend the shared value to
change, as Tally.total does. The bug is not the
class attribute. It is writing one where you meant a per-object
default.
Subclasses inherit a ClassVar declared on a base
class like any other class attribute. A subclass that doesn’t
declare its own copy reads straight through to the base’s value,
via the normal method
resolution order. A subclass that assigns its own value
creates a separate class attribute, independent of the base and
of sibling subclasses:
# class_var_inheritance.py
from typing import ClassVar
class Base:
shared: ClassVar[int] = 0
class Left(Base):
pass
class Right(Base):
shared = 100 # Its own class attr, separate from Base's
print(Left.shared, Right.shared)
#: 0 100
# Only affects subclasses that haven't overridden
Base.shared = 9
print(Left.shared, Right.shared)
#: 9 100
# Creates Left's own attribute, doesn't touch Base
Left.shared = 5
print(Base.shared, Left.shared, Right.shared)
#: 9 5 100Left has no shared of its own, so
it tracks Base.shared until something assigns to
Left.shared directly. Right overrides
shared at class-definition time, so it never sees
changes made through Base. ClassVar
leaves all of that alone: it tells the type checker that
shared belongs to the class, and says nothing about
whether subclasses share storage. Attribute lookup on a subclass
is the shadowing rule from class_attribute_confusion.py,
one level up: Left reads through to
Base until an assignment gives Left
its own copy, the way a reads through to
Stars until a.rating = 1. A subclass
stands to its base class as an instance stands to its class.
Right writes shared = 100 without
repeating the annotation. A subclass overriding a
ClassVar inherits the name but not the type
checker’s guard. ty rejects
Left().shared = 5 and accepts
Right().shared = 5, so restating
ClassVar[int] on an override keeps that check.
type(self)
Forks the Counter“A subclass stands to its base class as an instance stands to
its class” cuts both ways. class_var.py increments
Tally.total through the literal class name. Writing
that same increment through type(self), a common
idiom for reaching “my own class” from a method, forks the
counter once the base class has subclasses, the same way
Right forked shared:
# classvar_fork.py
from typing import ClassVar
class Base:
total: ClassVar[int] = 0
def __init__(self) -> None:
type(self).total += 1 # Looks like Base.total += 1
class Sub(Base):
pass
Base()
Sub()
Sub()
print(Base.total, Sub.total)
#: 1 3type(self) is Base for the one
Base() call and Sub for both
Sub() calls. Base() increments
Base.total to 1. The first
Sub() reads through to that 1, adds
one, and the assignment creates Sub.total = 2 on
Sub alone, the same shadowing Right
demonstrated in class_var_inheritance.py.
The second Sub() increments that separate copy to
3. Base.total never moves past
1, and ty reports no diagnostic: the
augmented assignment is a valid ClassVar[int]
update either way, and nothing in the annotation says which
class name should receive it. Write the increment through the
literal class name, as class_var.py does,
whenever a ClassVar must count across every
subclass rather than fork one counter per subclass. Pattern
Refactoring’s registry sidesteps this by mutating
Trash.registry in place, never reassigning it
through cls.
For real per-object defaults, write a constructor with
default arguments, or use a @dataclass, which turns
the class-attribute syntax into instance attribute defaults.
Each object then gets its own storage:
# real_defaults.py
from dataclasses import dataclass
class A:
def __init__(self, x: int = 100) -> None:
self.x = x # An instance attribute, one per object
@dataclass
class B:
x: int = 100 # Becomes a constructor default
a = A()
a.x = -1
print(a.x, A().x) # The change in a does not leak
#: -1 100
print(B().x, B(7).x)
#: 100 7
print(vars(B)["x"], vars(B())["x"])
#: 100 100real_defaults.py’s
A and inside_objects.py’s
A both start x at 100,
and the two behave in opposite ways. In inside_objects.py the
100 lives on the class and every instance reads it.
In real_defaults.py it is a
default argument, and self.x = x runs on every
construction, giving each object its own storage before anything
can read it. The difference is not the value but where you write
it. Python still builds the default value once, at definition
time (see Default
Arguments), so a mutable default argument brings
the sharing straight back. 100 is immutable, so
this default is safe.
A @dataclass reads the annotated class-body
declarations as a template and generates a constructor from
them. The annotation marks a field. Without the decorator, the
same annotated assignment stays a shared class attribute, as
Cart showed. If you write x = 100 with
no x: int, @dataclass sees no
field:
# dataclass_no_annotation.py
from dataclasses import dataclass, fields
@dataclass
class B:
x = 100 # No annotation, so not a field
print(fields(B))
#: ()
b = B()
print(vars(b), b.x)
#: {} 100
b.x = -1
print(vars(b), B().x) # The same shadowing as Stars
#: {'x': -1} 100The name stays an ordinary shared class attribute, the
generated __init__() takes no x, and
neither the runtime nor the type checker complains.
b.x = -1 shadows the class attribute for that one
instance, and an assignment through the class would still change
every instance that has not shadowed it, the hazard
Stars demonstrated. The annotated field in real_defaults.py also
leaves a class attribute behind, as its last line shows:
vars(B) still holds x = 100. The
difference is the generated __init__(), which
assigns self.x on every construction, so each
object shadows the class attribute immediately and never reads
the shared one. Data
Classes as Types covers the details.
Every attribute question in this chapter reduces to one:
which dictionary holds the value? Assignment answers it, and the
answer depends on whether you assign through self
or through the class name. Decide which you want, then write the
declaration that says so: ClassVar for shared, a
constructor default or a @dataclass field for
per-object.
class_attribute_confusion.py,
add a third instance c = Stars() after the
Stars.rating = 9 line, and print
c.rating. Predict its value before running, then
explain why it differs from a.rating.class_var_inheritance.py,
add a third subclass class Middle(Base): pass (no
override, like Left) and print
Middle.shared alongside the others at each step.
Confirm Middle tracks Base the way
Left does.real_defaults.py, create
b = B() and assign b.x = -1. Then
create a second instance, b2 = B(), and confirm
b2.x is still 100.Tally from class_var.py so
total is a plain (non-ClassVar) class
attribute instead, then have an instance assign to
self.total directly. Using vars() as
in inside_objects.py, explain
what that assignment creates, and where.Cart from shared_mutable.py as a
@dataclass with
items: list[str] = field(default_factory=list),
importing field from dataclasses. Data
Classes as Types covers default_factory; this
exercise needs only the one expression given here. Repeat the
append and confirm b.items stays
empty. Then try the same class with
items: list[str] = [] and report what
@dataclass does about it.inside_objects.py, add
del a.x after the final print, then
print vars(a) and a.x again. Predict
both before running. Then run del a.x a second time
and explain the exception, given what vars(A) still
holds.counter_near_miss.py,
print vars(a) and vars(Tally)["total"]
after constructing both instances, and use them to explain the
1 1 0 output. Then fix the class so the shared
counter moves, without changing the ClassVar
declaration, and explain what ty reports when you
remove the # type: ignore from the broken
version.class_var_inheritance.py
so shared is ClassVar[list[int]] = []
and Left and Right both call
.append() on it. Predict what
Base.shared holds afterwards, then check. Give
Right its own list with shared = [] in
its body and repeat.