A class attribute is shared¶
Level: 201 · for Python programmers, especially ones coming from ABAP, Java or C#
One line: A name assigned in a class body is one object, stored on the class and seen through every instance, so a list there belongs to every object of the class at once — while a write through an instance lands on the instance, which is why c.count = 1 leaves Counter.count at 0 and c.count += 1 does both things in one statement.
The body of a class statement runs once, top to bottom, when the statement is reached, and every name it binds goes into the class's own namespace. An instance starts with an empty namespace of its own. Reading a.members looks in the instance first and, finding nothing, falls through to the class; writing a.members = … always goes to the instance. Those two rules are the whole page. Together they mean that members = [] in a class body is a single list that every instance reads, and that instance data comes only from assignments to self, which is the opposite of what the line looks like to a reader from Java, C# or ABAP, where a field written in the class body is a field of each object.
The run¶
Verified output of a_class_attribute_is_shared_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. ONE LIST ON THE CLASS, SEEN THROUGH EVERY INSTANCE
class Team:
members = []
def join(self, name):
self.members.append(name)
a, b = Team(), Team()
a.join('ada')
b.members ['ada'] nobody joined b: the list belongs to the class
a.members is Team.members True
vars(a) {} the instance holds nothing of its own
2. A READ FALLS THROUGH TO THE CLASS; A WRITE DOES NOT
class Counter:
count = 0
c = Counter()
c.count 0 not on c, so found on the class
c.count = 1
Counter.count 0 the write went to c, not through to the class
vars(c) {'count': 1}
c.count 1 and now the read stops at c
del c.count
c.count 0 delete c's own, and the class's shows again
c.count += 1
(Counter.count, vars(c)) (0, {'count': 1}) += reads through, then writes to c
3. THE FIX: MAKE THE OBJECT IN __init__
class Team:
def __init__(self):
self.members = []
def join(self, name):
self.members.append(name)
a, b = Team(), Team()
a.join('ada')
b.members [] each __init__ ran self.members = [] once, for its own instance
a.members is b.members False
4. WHEN SHARING IS THE POINT
class Node:
made = 0
def __init__(self):
Node.made += 1
for _ in range(3): Node()
Node.made 3 written through the class name, so it is one counter
vars(Node()) {} and no instance ever gets its own copy
5. A DATACLASS FIELD IS PER INSTANCE; AN UNANNOTATED NAME IS NOT
@dataclass ValueError
class Bag:
items: list = []
@dataclass
class Bag:
items = []
x, y = Bag(), Bag()
x.items.append(1)
y.items [1] no annotation, so not a field: a plain class attribute
fields(Bag) () the dataclass machinery never saw it
6. __slots__: A NAMESPACE THAT IS NOT A DICT
class P:
__slots__ = ('x',)
p = P()
p.x = 1
p.y = 2 AttributeError
vars(p) TypeError no __dict__ to hold a stray attribute
class Q: ValueError
__slots__ = ('x',)
x = 0
What the run shows¶
Section 1: one list on the class, seen through every instance. a.join('ada') appended to self.members, which resolved to the class's list, so b.members shows ['ada']. a.members is Team.members is True, and vars(a) is {}: the instance owns nothing, and every read of members falls through to the one list.
Section 2: a read falls through; a write does not. c.count is 0 by falling through. c.count = 1 writes into c's own namespace, so Counter.count stays 0, vars(c) is {'count': 1}, and from then on the read stops at c. del c.count removes the instance's entry, and the class's 0 shows again. The last row is += is not + on a class attribute: the read falls through to the class, the write goes to the instance, and the result is (0, {'count': 1}). With an int that makes a private copy; with a list, as that page shows, it grows the shared list and makes the instance entry.
Section 3: the fix. self.members = [] inside __init__ runs once per instance, so b.members is [] and a.members is b.members is False. Per-instance state is made by assignment to self, and nowhere else.
Section 4: when sharing is the point. Node.made += 1, written through the class name rather than through self, updates the one counter, and after three instances it is 3; vars(Node()) is still {}. A class attribute is Python's static field, and the class body is the place to declare a constant or a count that belongs to the class. Writing type(self).made += 1 instead would give a subclass a counter of its own on its first write, which is sometimes what you want and sometimes the bug.
Section 5: a dataclass field is per instance; an unannotated name is not. items: list = [] raises ValueError before the class exists, because the generated __init__ would hand the one list to every instance; A default is computed once shows the default_factory fix. Drop the annotation and the trap is back with no error: items = [] is not a field, fields(Bag) is (), and y.items is [1] after x appended. The dataclass machinery reads annotations, and a bare assignment in the body is exactly what it was in section 1.
Section 6: __slots__. With __slots__ = ('x',) an instance has no __dict__: p.y = 2 raises AttributeError and vars(p) raises TypeError. Each slot is a descriptor stored on the class under the slot's name, which is why x = 0 beside __slots__ = ('x',) raises ValueError when the class is created: the class attribute and the slot would need the same name in the same namespace.
If you are coming from ABAP¶
ABAP says which kind of attribute it is at the declaration: DATA for one per instance, CLASS-DATA for one per class. Python has only the second kind in a class body, and makes the first by assigning to self in __init__, so a Python class body reads like a list of CLASS-DATA lines and the instance attributes are found only by reading the constructor. Java and C# are the same: static is the keyword, and its absence means per-instance, which is the intuition section 1 defeats. ABAP also has no fall-through: an instance attribute and a static one cannot share a name, so section 2's shadowing has no ABAP counterpart. (Not machine-checked — CI cannot run ABAP.)
If you are coming from Rust¶
A Rust struct has fields, one set per value, and nothing that lives on the type except associated constants, which cannot change, and associated functions. Shared mutable state has to be declared as a static behind a Mutex or an atomic, and the declaration says so; there is no way to write section 1 by accident. The Rust library's What a struct is ↗ and const and static ↗ are the two halves of that split.
Try it¶
- Search a codebase you maintain for a class body that assigns a list, dict or set:
grep -rnE '^ [a-z_]+ = (\[\]|\{\}|set\(\)|dict\(\)|list\(\))' --include='*.py'. For each hit, look for a method that appends to it throughself. - Print
vars(obj)andvars(type(obj))for an instance of a class of yours. Anything you thought was per-instance and appears only in the second dict is shared. - Find a class-level counter incremented with
self.count += 1and print the class attribute after three instances. Section 2 says it is still0. - Drop the annotation from one field of a
@dataclassof yours and printfields(). Then append to it through one instance and read it through another. - Add
__slots__to a small class and run its tests. The firstAttributeErroris either a typo nothing had caught or an attribute some other code was adding from outside.
See also¶
+=is not+: section 2's last row in full, with anintand with a list- A default is computed once: the same one-object rule for functions, and the dataclass fix
obj.xis a search: the lookup order behind sections 2 and 6, once written@dataclasswrites the methods: what a field is and is not- Class and instance variables ↗ in the Python tutorial, which teaches this with a dog and its tricks, and
__slots__↗ in the language reference - What a struct is ↗ and
constandstatic↗ in the Rust library