A field in the class body looks, to a C++ or Java programmer, like storage in each object. In Python it is one variable the class holds, and an object’s own fields come from assignments through
self.
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. Apart
from __slots__, which comes up below, no syntax in
a Python class body allocates a per-object field. Assigning
through self inside a method creates that storage,
as self.x = x in a constructor does.
# 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 writes to the instance, creating the instance
attribute on first assignment. Assigning through the class name,
as Stars.rating = 9 does, changes the shared value.
vars() returns an object’s own attribute
dictionary, its __dict__, so inspecting the class
with vars(A) and the instance with
vars(a) shows the split:
# inside_objects.py
class A:
x = 100
a = A()
print(vars(a), a.x) # Empty, so a.x comes from the class
#: {} 100
a.x = 1
print(vars(a), a.x) # Assignment created it on the instance
#: {'x': 1} 1
print(vars(A)["x"]) # The class entry is unchanged
#: 100The listing subscripts
vars(A) instead of printing it whole, because a
class’s dictionary also holds entries the compiler adds, such as
__module__ and __doc__. That
dictionary is a read-only mappingproxy, so writing
to it raises a TypeError. To change the entry,
assign through the class name, or call
setattr(A, "x", 5) when the name arrives as a
string. The instance dictionary is a plain dict
holding what the code assigned, and that alone.
Slots can replace that instance
dictionary. A class gets them by declaring __slots__,
and a data class gets them from slots=True. Each
instance then holds one slot per declared name in place of a
__dict__, provided every base class declares slots
too. An instance of such a class cannot shadow a class
attribute. Assigning to the class attribute’s name on the
instance raises an AttributeError, because the
instance has no dictionary to hold the new attribute.
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: the instance first, then the class. Because
the class dictionary holds the method, assigning
a.show = something shadows the method for
a alone. The inspection helper
display_object() that Classes introduces
reports attributes and methods separately, but the same class
dictionary holds both.
Some class attributes follow a
different rule. A @property
stored on the class takes precedence over the instance
dictionary for its name, so reading that name calls its getter
and assigning to it calls its setter. The setter stores its
value in the instance dictionary under a different name, as
Circle’s radius setter stores
_radius.
A class attribute behaves like a default until someone assigns to an attribute of the same name on one instance. After that, every other object reads the new value when the class attribute changes, while the object that assigned keeps its own value. The program reads the wrong value far from the line that caused it, often in a different function that does not mention 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 rerate(rating: int) -> None:
Stars.rating = rating # Meant for every Stars
def show(star: Stars) -> None:
print(star.rating) # Reads far from where it shadowed
a, b = Stars(), Stars()
sell(a)
rerate(9)
show(a) # The new rating does not reach a
#: 1
show(b)
#: 9Someone debugging the stale 1 finds
rerate() and show() both correct. The
cause is the assignment inside sell(), which gave
a its own rating before the class
attribute changed. Neither rerate() nor
show() calls sell(), so finding the
assignment means tracing every earlier call that used a
Stars instance.
The shadowing rule confines a change to one object only while the shared value is immutable:
a.items.append("apple") reads
items, finds a’s own dictionary empty,
falls back to the class, and mutates the one list stored there.
Shadowing starts with an assignment. .append()
leaves a’s dictionary empty, so
b.items reads the same list and holds the apple
too. The type checker also accepts the line, since
a.items.append("apple") is a correct call on a
list[str].
An augmented assignment both
mutates and assigns. a.items += ["pear"] mutates
the shared list through __iadd__(), so
b.items holds the pear too. The augmented
assignment then stores that same list on a,
creating an instance attribute that shadows the class list.
Because both attributes refer to one list, the two printed
values match.
A per-object list belongs in
__init__(), as Real Per-Object Defaults
shows, or in a @dataclass field with a default_factory.
When you intend 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 shadows 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 total on the class and no label.
label first appears in
display_object(a), on the instance. An assignment
in the class body creates a class attribute, as class_attribute_confusion.py
shows. total: ClassVar[int] = 0 has the
= 0, so it exists on Tally before any
instance exists. label: str has no =,
so it stays a declaration. The annotation goes into
Tally.__annotations__ beside total’s
entry and records that a Tally will carry a
label. display_object() reports
attributes that exist, so the declaration stays out of its
report.
Constructing a runs
self.label = label, which creates a real
label attribute on a, not on
Tally. display_object(a) reports
total too, by fallback. Reading an attribute checks
the instance first, then the class, the rule Stars
demonstrates in class_attribute_confusion.py.
The total that a reports is
Tally.total.
The [CV] tag marks an attribute the class
stores. It stands for “class variable,” another name for a class
attribute. 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. In class_var.py,
label: str states that every Tally
carries a label attribute of type str
and leaves the assignment to other code. If no method assigns
self.label, no attribute exists, on the instance or
the class. The type checker takes the annotation as the
attribute’s declaration. It never checks whether some method
sets the attribute, so it accepts every read of
label. The first code that reads label
raises an AttributeError.
The annotation on label is optional here. If you
delete it, the type checker still infers label: str
correctly from self.label = label, because the
checker gives the attribute the type of the parameter assigned
to it. The annotation stays for symmetry with
total, so both names appear at the top of the
class, instead of label appearing first inside the
constructor. Simulation
shows the case that requires the annotation. There, code outside
the class makes the first assignment, and the assignment inside
move() reads the attribute it sets, so the bare
annotation is the type checker’s one source for its type.
ClassVar With No Value Declares TooClassVar declares that the class holds an
attribute, and leaves creating it to an assignment. A
ClassVar with no value stays a declaration, the
same as label in class_var.py:
# declared_classvar.py
from typing import ClassVar
from exceptions import expected
class Registry:
count: ClassVar[int] # Declared, no value
with expected(AttributeError):
print(Registry.count)
#: [AttributeError] type object 'Registry' has no attribute
#: 'count'
Registry.count = 0 # The assignment creates it
print(Registry.count)
#: 0
print(Registry().count) # Found by fallback
#: 0count: ClassVar[int] records that a count
belongs to Registry. No attribute exists until
something assigns one, so the first read raises an
AttributeError.
The type checker (ty) accepts that first read,
for the reason it accepts a read of label. It
checks the declaration’s type and ignores which code runs first.
Pyright accepts the read too. Its optional
reportUninitializedInstanceVariable rule checks
instance variables, not a ClassVar.
An annotation states the type, and ClassVar adds
where the attribute belongs. The assignment creates the
attribute. That holds for label: str, which
self.label = label creates on each instance, for
total: ClassVar[int] = 0, and for the
count that Registry.count = 0 creates
in declared_classvar.py.
A ClassVar with no value usually appears in a
base class, declaring what its subclasses must supply:
# required_classvar.py
from typing import ClassVar
from exceptions import expected
class Shape:
sides: ClassVar[int] # Subclasses supply it
class Square(Shape):
sides = 4
class Blob(Shape):
pass
print(Square.sides)
#: 4
with expected(AttributeError):
print(Blob.sides)
#: [AttributeError] type object 'Blob' has no attribute
#: 'sides'A Protocol
declares its attributes in the same form as Shape,
a bare annotation, since it describes what a class must have and
leaves the value to the class.
ty 0.0.95 enforces
what the declaration states about the value. A subclass that
writes sides = "four" draws
invalid-assignment on that line, since the checker
compares the value with the declared ClassVar[int].
A subclass that redeclares the attribute as
sides: ClassVar[str] = "four" draws
invalid-attribute-override. A subclass that narrows
the type, as sides: ClassVar[bool] = True does,
draws invalid-mutable-override. A mutable
attribute’s type is invariant,
so a subclass must keep the type it inherits.
One mistake passes the checker. The checker accepts
Blob as written, because it trusts the declaration
that sides exists. Treat the base declaration as
documentation that a checker reads, not as a guarantee that the
attribute exists.
ClassVar
CatchesClassVar is a hint for the type checker. It
records that total belongs to the class, so the
type checker reports an assignment to total through
an instance as an error. That assignment is the shadowing class_attribute_confusion.py
performs unchecked on Stars.rating. Python’s
attribute lookup ignores the hint. @dataclass does
read it at runtime, and leaves a ClassVar out of
the fields it generates.
At runtime an assignment through self creates an
instance attribute with or without ClassVar:
# self_increment.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. The write through
self is why class_var.py increments
through the class name, Tally.total += 1.
With ClassVar, the type checker reports this
mistake before the program runs. The checker rejects the
augmented form as it rejects a direct
self.total = 5. For a write like
a.total = 99, ty reports “Cannot
assign to ClassVar total from an instance of type
Tally”. Inside __init__() the message
names the type of self in place of
Tally. 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. The bug
is not the class attribute. It is writing one where you meant a
per-object default. Three kinds of value commonly belong in
class attributes: a count of every object created, a registry
mapping names to classes, and a constant that all instances read
but none change. Declare the sharing for each, so the type
checker reports an instance assignment that would shadow it.
Tally.total is the first of these.
For the third kind, a class-level
constant, Final[int]
says more than ClassVar[int].
Final[int] declares the value both shared and
constant. Use ClassVar when you intend the shared
value to change, as Tally.total does.
Subclasses inherit a
ClassVar declared on a base class like any other
class attribute. A subclass reads through to the base’s value,
via the normal method
resolution order, until it assigns its own. That assignment
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(Base.shared, Left.shared, Right.shared)
#: 0 0 100
Base.shared = 9
print(Base.shared, Left.shared, Right.shared)
#: 9 9 100
Left.shared = 5
print(Base.shared, Left.shared, Right.shared)
#: 9 5 100Attribute 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.
ClassVar leaves that lookup alone. It tells the
type checker that shared belongs to the class.
Attribute lookup alone decides whether subclasses share
storage.
Right writes shared = 100 and
leaves the annotation to Base. The override is
still a ClassVar. The type checker applies the
base’s declaration to it, so the checker rejects
Right().shared = 5 as it rejects
Left().shared = 5.
type(self)
Forks the CounterThe mistake in self_increment.py has a
subclass form. class_var.py increments
Tally.total through the literal class name. Methods
often write type(self) to name the instance’s
class. Writing the same increment through
type(self) forks the counter when the base class
has subclasses, the same way Right forks
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() # [1]
print(Base.total, Sub.total)
#: 1 1
Sub() # [2]
print(Base.total, Sub.total)
#: 1 2
Sub() # [3]
print(Base.total, Sub.total)
#: 1 3type(self) is
Base for the one Base() call and
Sub for both Sub() calls.
Base() ([1]) increments
Base.total to 1.
Sub.total reads through to that 1. The
first Sub() ([2]) reads the same
1 and adds one. The assignment creates
Sub.total = 2 on Sub alone, the same
shadowing Right demonstrates in class_var_inheritance.py.
The second Sub() ([3]) increments that
separate copy to 3.
The type checker accepts every line. The augmented assignment
is a valid ClassVar[int] update either way, and the
annotation leaves open 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. A @classmethod
that writes cls.total += 1 forks the same way,
because cls is the class that received the
call.
Pattern
Refactoring’s registry avoids the fork by mutating one
dictionary in place.
Trash.registry[cls.__name__] = cls assigns into the
dictionary, not to an attribute of cls, so every
subclass shares the one registry.
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
print(vars(A()), A(7).x) # Each instance holds x
#: {'x': 100} 7
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.
The two behave in opposite ways. In inside_objects.py the
class holds the 100 and every instance reads it. In
real_defaults.py the
100 is a default argument,
__init__(self, x: int = 100) -> None.
self.x = x runs on every construction, giving each
object its own storage before anything can read it.
vars(A()) prints {'x': 100}, where inside_objects.py’s
vars(a) prints {} until
a.x = 1 runs.
Python still builds the default
value once, at definition time, so every construction that
relies on a mutable default argument shares one object.
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. Without the decorator, an
annotated assignment such as B’s
x: int = 100 stays a shared class attribute, as
Cart shows. The annotation marks a field. If you
write x = 100 with no x: int,
@dataclass leaves x out of
fields(B):
# 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, so the
generated __init__() is
__init__(self). Neither Python nor the type checker
reports an error. An assignment through the class changes every
instance that falls back to the class, the hazard
Stars demonstrates.
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 runs
self.x = x on every construction. Each object
therefore shadows the class attribute immediately and always
reads its own value. Data
Classes as Types shows the __init__(),
__repr__(), and __eq__() that
@dataclass generates.
Every attribute question in this chapter reduces to one:
which dictionary holds the value? Assignment answers it.
Assigning through self writes the instance’s
dictionary, and assigning through the class name writes the
class’s dictionary. Decide which you want, then write the
declaration that says so: ClassVar for shared, a
constructor default or a @dataclass field for
per-object.
Try each exercise before opening its solution.
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
shadowing shared, 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 assign a.total = 99 through
an instance. 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.self_increment.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 the type checker reports when you
remove the # type: ignore from the broken
version.class_var_inheritance.py
so shared is ClassVar[list[int]] = [],
delete Right’s shared = 100, and have
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.Ticket with a bare annotation
seat: str and an __init__() that
stores a holder and leaves seat
unassigned. Run ty on it, then read
seat on an instance inside
expected(AttributeError). Assign seat
from outside the class and print vars() of the
instance before and after.classvar_fork.py, print
vars(Sub).get("total") before the first
Sub() call and after each one, and use the three
values to say which class’s dictionary supplies each
Sub.total the listing prints. Then change the
increment to Base.total += 1 and predict every line
before running.