Skip to content

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.

class Team:
    members = []

a, b = Team(), Team()
a.members.append('ada')
b.members           # ['ada']

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

  1. 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 through self.
  2. Print vars(obj) and vars(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.
  3. Find a class-level counter incremented with self.count += 1 and print the class attribute after three instances. Section 2 says it is still 0.
  4. Drop the annotation from one field of a @dataclass of yours and print fields(). Then append to it through one instance and read it through another.
  5. Add __slots__ to a small class and run its tests. The first AttributeError is either a typo nothing had caught or an attribute some other code was adding from outside.

See also