Name Tag Airport: Python's Object Model
·
Course overview
1
A tag is not the bag: names and objects
2
Sealed bags: mutable and immutable
3
Bags inside bags: containers and copying
4
Handing over a bag: functions and arguments
5
Zones and passes: scope
6
Carry-on bags: closures
7
The bag spec: classes and instances
8
Standard airport procedures: dunder methods
9
The spec behind the spec: inheritance and MRO
10
The checkpoint: how attribute access really works
11
Unclaimed bags: memory and GC
12
Locker numbers: hashing, equality and the big picture
Lost-baggage report
LOST-007
Reported by
Moon, logistics manager for an orchestra tour
What went missing
A 'FRAGILE' sticker was put on one instrument case, and now every case built from the same spec carries that sticker.
To close the case
Find where the sticker list actually lives, and give each case its own sticker list.
Haneulseom International Airport · Baggage Center
In Supervisor Do Miran's desk drawer there is a stack of bag specs. From a single spec sheet you can turn out as many bags of the same shape as you like. "A spec is a sheet of paper, and each bag is a bag. Both are things."
Moon only put 'FRAGILE' on the violin case, yet the same sticker shows up on the cello case. The supervisor opens not the case but the spec. That is where the sticker was stuck.
The class statement is an assignment that creates a class object and binds it to a name (just like def in Lesson 1). A class is also an object with a type, and that type is type.
class Case:
size = "M"
def __init__(self, owner):
self.owner = owner
print(type(Case), isinstance(Case, type))
# <class 'type'> True
b = Case("cello")
print(type(b).__name__, type(b) is Case)
# Case True
print(vars(b))
# {'owner': 'cello'}
print([k for k in vars(Case) if not k.startswith("__")])
# ['size']
print(b.size, "size" in vars(b))
# M False| What it holds | How to look | |
|---|---|---|
| Class (spec) | Names created in the class body: class attributes, methods | vars(Case), Case.__dict__ |
| Instance (bag) | Attributes of that bag alone, usually set in __init__ with self.x = ... | vars(b), b.__dict__ |
b's __dict__ holds only owner. Even so, b.size reads as "M".__dict__ first, and if it is not there, in the class. If it's not on the bag, you open the spec.__dict__ is a read-only window called a mappingproxy. To change a class attribute, assign to the class directly, as in Case.size = "L".Here is the reporter's spec.
class Case:
stickers = []
def __init__(self, owner):
self.owner = owner
a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(b.stickers, Case.stickers)
# ['FRAGILE'] ['FRAGILE']
print(a.stickers is b.stickers, "stickers" in vars(a))
# True Falsestickers = [] runs once, in the class body. There is just one list, stuck to the spec.a.stickers is not in a's __dict__, so it is found in the class, and append changes that one and only list (changing the bag).Anything each instance should have for itself is attached to self in __init__. Compare the three versions below.
Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
If you assign to the same name on an instance, as in the third version, the class attribute is left alone and a new name appears in the instance __dict__. In that instance, the class attribute is hidden (the same shape as shadowing built-in names in Lesson 5).
class Case:
size = "M"
a = Case()
b = Case()
a.size = "L"
print(a.size, b.size, vars(a))
# L M {'size': 'L'}
Case.size = "S"
print(a.size, b.size)
# L S
del a.size
print(a.size)
# Sb) sees the change immediately.del a.size, and the class value shows through again.Don't forget that += is an assignment too. Using += on a numeric class attribute creates a new name on the instance.
class Gate:
count = 0
g = Gate()
g.count += 1
print(g.count, Gate.count, vars(g))
# 1 0 {'count': 1}g.count += 1 is g.count = g.count + 1. The right side reads 0 from the class, and the left side puts 1 into g's __dict__. For a number that all instances count together, assign to the class directly, as in Gate.count += 1.
Pin says
→ Put a list or a dict in the class body and every instance shares it. Keep only things that really are shared, like constants and defaults, on the class, and attach anything that differs per bag to self in __init__.
Let's use the checkpoint flow to see the path Python takes when reading b.x. In this lesson you only need step 2 (instance __dict__) and step 4 (a plain class attribute). Steps 1, 3 and 5 come in Lesson 10.
The b below has size both on the class and in its own __dict__.
Checkpoint flow
1
class Case:
2
size = "M"
3
stickers = []
4
def __init__(self, owner):
5
self.owner = owner
6
def label(self):
7
return "case of " + self.owner
8
9
b = Case("cello")
10
b.size = "L"
b.
Data descriptor on the class (__get__ + __set__)?
In the instance __dict__?
Non-data descriptor on the class (only __get__, like a function)?
A plain class attribute?
Is there __getattr__?
AttributeError
Result: b. →
size exists on the class too, but step 2 finds it first, so it is 'L'. The Case.size in parentheses on the result line means "the class has the same name too".label is a function on the class. Functions are special objects caught at step 3, so what you get out is not a plain function but a bound method. That is the topic of the next section.A method is an ordinary function stored on the class. When you take it out through an instance, Python builds a new object that binds the function to the instance: a bound method.
class Bag:
def __init__(self, owner):
self.owner = owner
def label(self):
return "bag of " + self.owner
bag = Bag("kim")
print(bag.label is bag.label, bag.label == bag.label)
# False True
print(bag.label.__self__ is bag, bag.label.__func__ is Bag.label)
# True True
print(bag.label(), Bag.label(bag))
# bag of kim bag of kim
print(type(bag.label).__name__, type(Bag.label).__name__)
# method functionbag.label, a new bound method is created. That is why is is False. They bind the same function to the same instance, so == is True.__self__ is the bound instance, and __func__ is the original function on the class.bag.label() is the same as Bag.label(bag). self is simply the first argument, filled in for you by the bound method.Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
vars, getattr, setattr, hasattr (5 min)Sometimes an attribute name has to be decided at run time, as a string. Think of keys from a config file, or a column name picked on screen.
class Bag:
def __init__(self, owner):
self.owner = owner
bag = Bag("kim")
setattr(bag, "color", "red")
print(getattr(bag, "color"), hasattr(bag, "weight"), getattr(bag, "weight", 0))
# red False 0
print(vars(bag))
# {'owner': 'kim', 'color': 'red'}
vars(bag)["tag"] = "T1"
print(bag.tag, vars(bag) is bag.__dict__)
# T1 True
bag.weight
# AttributeError: 'Bag' object has no attribute 'weight'setattr(obj, "x", v) is the same as obj.x = v, and getattr(obj, "x") the same as obj.x. The third argument of getattr is the default returned when the attribute is missing.hasattr actually tries getattr and decides by whether an AttributeError is raised.vars() on an instance is the real __dict__, not a snapshot. That differs from locals() inside a function in Lesson 5.bag.colour = ...) also quietly becomes a new attribute.__slots__ and other new features (5 min)If you list attribute names in __slots__, instances get no __dict__ and can use only the listed names. In CPython, instances also use less memory because each one has no dictionary.
class Slim:
__slots__ = ("owner",)
def __init__(self, owner):
self.owner = owner
s = Slim("kim")
print(s.owner, hasattr(s, "__dict__"))
# kim False
s.color = "red"
# AttributeError: 'Slim' object has no attribute 'color' and no __dict__ for setting new attributesSince 3.13, classes have __static_attributes__. It collects, in sorted order, the names assigned with self.name = ... inside the class's methods.
class Trunk:
def __init__(self, owner):
self.owner = owner
self.tags = []
def pack(self):
self.weight = 10
print(Trunk.__static_attributes__)
# ('owner', 'tags', 'weight')Finally, deferred evaluation of annotations in 3.14 (PEP 649, 749). Annotations in a class body are not evaluated when the class is created, but the first time __annotations__ is read. So the class is created even if an annotation uses a name that is not defined yet. Up to 3.13, the friend: Missing line raised a NameError.
class Partner:
friend: Missing
print("created", Partner.__name__)
# created Partner
class Missing:
pass
print(Partner.__annotations__)
# {'friend': <class '__main__.Missing'>}Peel the sticker list off the spec and give each case its own. The shared value (the standard size) stays on the class.
class Case:
size = "M"
def __init__(self, owner):
self.owner = owner
self.stickers = []
a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(a.stickers, b.stickers)
# ['FRAGILE'] []
print(vars(a))
# {'owner': 'violin', 'stickers': ['FRAGILE']}
print(Case.__static_attributes__)
# ('owner', 'stickers')Now 'FRAGILE' goes only on the violin case. After Moon leaves, Pin brings up the records of the ghost bags on conveyor 13. The two bags were built from the same spec, and the partner attribute of one points to the other, while the other's partner points right back. The supervisor circles that line with her pen.
Closing report — LOST-007. The sticker list was a class attribute, so every case shared the same list. Solved by creating a new list for each case with self.stickers = [] in __init__.
Case.stickers = [] on the class, what happens to b.stickers and vars(a) if you run a.stickers += ["FRAGILE"]? (Hint: the += from Lesson 2 and the += from 7-2 happen at the same time.)__slots__ = ("owner",) has a class attribute size = "M", can you read s.size on an instance? What about s.size = "L"?getattr to fetch and print the three attributes ["owner", "color", "weight"] in turn, printing "-" for any that are missing.The supervisor's notebook
Classes are objects too. type(Case) is type. The class statement is also an assignment that binds a name.
Instances have their own __dict__. Reading goes instance → class.
Class attributes are shared by every instance. Put a mutable object there and a change in one place shows up everywhere.
Assigning the same name on an instance hides the class attribute. += is an assignment too.
bag.m creates a new bound method every time (is is False, == is True). __self__, __func__.
getattr / setattr / hasattr handle names as strings. vars(obj) is the real __dict__.
__slots__ blocks new attributes. __static_attributes__ (3.13), deferred evaluation of annotations (3.14).
← Previous
Lesson 6 · Carry-on bags: closures
Next →
Lesson 8 · Standard airport procedures: dunder methods
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.