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
Time 7:30–8:20
Lost-baggage report
LOST-010
Reported by
Oh, maintenance engineer for the weighing system
What went missing
The scale records. A weight check was added, yet a bag weighing -10 kg shows up in the records.
To close the case
Find the route that bypassed the check, and make every route pass through the checkpoint.
Haneulseom International Airport · Baggage Center
To stop negative weights, Oh made Bag check the weight when a bag is created. Yet the records contain a bag weighing -10 kg. Supervisor Do Miran flips through the entry log. "We put a checkpoint at the main gate, and it came in through the side door."
Today we look at the checkpoint Python passes through the moment obj.x is read or written. It is also the full picture of the reading rule we called "instance, then class" in Lesson 7.
@property: putting a checkpoint on an attribute (9 min)Here is the reporter's code. The check exists only in __init__.
class Bag:
def __init__(self, kg):
if kg < 0:
raise ValueError("negative weight")
self.kg = kg
b = Bag(20)
b.kg -= 30
print(b.kg)
# -10The bag passed the check when it was created, but the later b.kg -= 30 writes straight into the instance __dict__. That is the side door. To put a check on writing the attribute itself, use @property.
class Bag:
def __init__(self, kg):
self.kg = kg
@property
def kg(self):
return self._kg
@kg.setter
def kg(self, value):
if value < 0:
raise ValueError(f"negative weight: {value}")
self._kg = value
b = Bag(20)
print(b.kg, vars(b))
# 20 {'_kg': 20}
b.kg -= 30
# ValueError: negative weight: -10b.kg calls the first kg function, and writing b.kg = v calls the @kg.setter function. From the outside it is used like a plain attribute.self.kg = kg in __init__ goes through the setter too. The check needs to live in just one place._kg. The name kg is a property object that lives on the class, not on the instance.Without a setter, the attribute is read-only. That suits values computed from other attributes.
class Box:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self):
return self.w * self.h
x = Box(3, 4)
print(x.area, type(Box.__dict__["area"]).__name__)
# 12 property
x.area = 5
# AttributeError: property 'area' of 'Box' object has no setterPin says
→ A single underscore like _kg is only a 'please don't touch' agreement, not a lock. Nothing stops b._kg = -5. The checkpoint guards the normal route (b.kg = …).
The reporter's bag has not only a weight (kg) but also a length (cm). Copying a property for every attribute multiplies the checking code. Instead, you can build one checkpoint and install it on several attributes. That is a descriptor.
A descriptor is an object with at least one of __get__ and __set__ (or __delete__). Put it on a class as a class attribute, and Python calls those methods whenever that name is read or written on an instance.
class Positive:
def __set_name__(self, owner, name):
print(f"set_name: {owner.__name__}.{name}")
self.name = name
def __get__(self, obj, owner=None):
if obj is None:
return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
# set_name: Bag.kg
# set_name: Bag.cm
b = Bag(20, 55)
print(b.kg, b.cm, vars(b))
# 20 55 {'kg': 20, 'cm': 55}
print(type(Bag.kg).__name__)
# Positive
b.cm = -1
# ValueError: cm must be >= 0: -1| When | What Python calls |
|---|---|
| The class has just been created | Positive.__set_name__(checkpoint, Bag, "kg") |
Reading b.kg | Positive.__get__(checkpoint, b, Bag) |
Reading Bag.kg | Positive.__get__(checkpoint, None, Bag) |
Writing b.kg = 20 | Positive.__set__(checkpoint, b, 20) |
__set_name__ (3.6) tells the descriptor the name it was attached to, the moment the class statement ends. That is why the same Positive can be installed on both kg and cm.__dict__.__dict__ under the same name "kg", reading it goes through the checkpoint rather than __dict__. That ordering is the topic of 10-4.@property is also a descriptor, one Python ships ready-made.Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
In Lesson 7 we saw that bag.m is bag.m is False. The reason is that functions are descriptors too. A function has __get__ but no __set__, which makes it a non-data descriptor.
class Bag:
def weigh(self):
return 20
b = Bag()
f = Bag.__dict__["weigh"]
print(type(f).__name__, hasattr(f, "__get__"), hasattr(f, "__set__"))
# function True False
m = f.__get__(b, Bag)
print(type(m).__name__, m.__self__ is b, m())
# method True 20
print(b.weigh is b.weigh, b.weigh == b.weigh, Bag.weigh is f)
# False True TrueEvery time b.weigh is read, Python calls f.__get__(b, Bag) and builds a new bound method: a small object that bundles the function with b. So is is False and == is True. This bundle is also why b.weigh() can be called without passing self. Read from the class (Bag.weigh), there is no instance to bundle, so the plain function comes out.
staticmethod and classmethod are descriptors whose __get__ changes what gets bundled.
class Bag:
@staticmethod
def kg_to_lb(kg):
return round(kg * 2.2046, 1)
@classmethod
def empty(cls):
return cls()
class Suitcase(Bag):
pass
print(Bag.kg_to_lb(20), Bag().kg_to_lb(20))
# 44.1 44.1
print(type(Suitcase.empty()).__name__, Suitcase.empty.__self__ is Suitcase)
# Suitcase True
print(type(Bag.__dict__["empty"]).__name__)
# classmethodWhat __get__ bundles | First argument | |
|---|---|---|
| Plain function | The instance | self |
staticmethod | Nothing | None |
classmethod | The class (type(obj)) | cls |
classmethod receives the class it was called on, so Suitcase.empty() builds a Suitcase. It is used for "alternative constructors" that build the right spec even under inheritance.
When reading obj.x, Python (precisely, object.__getattribute__) looks in this order.
| Order | Where it looks | Example |
|---|---|---|
| 1 | A data descriptor on the class (MRO), one with __set__ or __delete__ | property, Positive |
| 2 | The instance __dict__ | b.label = "ICN" |
| 3 | A non-data descriptor on the class (only __get__) | functions, staticmethod, classmethod |
| 4 | A plain class attribute | kind = "bag" |
| 5 | __getattr__ (if defined) | |
| 6 | AttributeError |
Data descriptors come before the instance __dict__, so the checkpoint is called before any value inside the bag. That is why it was fine for Positive in 10-2 to store its value in __dict__ under the same name. Functions, on the other hand, come after __dict__, so using the same name on an instance hides the method.
Pick the attributes one at a time in the flow chart below. It shows at which step the answer comes out.
Checkpoint flow
1
class Positive:
2
def __set_name__(self, owner, name):
3
self.name = name
4
def __get__(self, obj, owner):
5
return obj.__dict__[self.name]
6
def __set__(self, obj, value):
7
obj.__dict__[self.name] = value
8
class Bag:
9
kg = Positive()
10
kind = "bag"
11
def weigh(self):
12
return self.kg
13
def __repr__(self):
14
return "Bag()"
15
b = Bag()
16
b.kg = 20
17
b.label = "ICN"
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. →
This time we deliberately put the same names into the instance __dict__. The last line is a "side door" that writes straight into __dict__ to dodge the checkpoint.
Checkpoint flow
1
class Bag:
2
kind = "bag"
3
def weigh(self):
4
return 20
5
@property
6
def tag(self):
7
return "T-" + self.kind
8
def __getattr__(self, name):
9
return f"<no {name}>"
10
b = Bag()
11
b.weigh = "shadowed"
12
b.kind = "suitcase"
13
b.__dict__["tag"] = "fake"
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. →
property without a setter is still a data descriptor. A property always has __set__, and without a setter it raises AttributeError inside it. That is why __dict__ cannot hide it.b.x = v) has a shorter order. If the class has a descriptor with __set__, Python calls it; otherwise it writes into the instance __dict__.Pin says
→ To memorize the order in one line: 'data descriptor > instance __dict__ > non-data descriptor like a function > class attribute > __getattr__'.
__getattr__, __getattribute__, and type (5 min)These two similarly named handles are called at different times.
__getattribute__: called on every attribute read, always. The whole order from 10-4 lives inside object.__getattribute__. Overriding it easily leads to infinite recursion, so it is rarely done.__getattr__: the last rescue, called only when the order above found nothing.class Loud:
def __init__(self):
self.kg = 20
def __getattribute__(self, name):
print("getattribute", name)
return super().__getattribute__(name)
def __getattr__(self, name):
print("getattr", name)
return None
x = Loud()
v = x.kg
# getattribute kg
v = x.color
# getattribute color
# getattr colorFinally, if classes are objects, what is the type of a class? It is type. A class statement is really the same as calling type(name, bases, attribute dict).
class Bag:
kind = "bag"
print(type(Bag()).__name__, type(Bag), type(type))
# Bag <class 'type'> <class 'type'>
Dyn = type("Dyn", (Bag,), {"kind": "dyn"})
print(Dyn.kind, [c.__name__ for c in Dyn.__mro__])
# dyn ['Dyn', 'Bag', 'object']Inheriting from type to change how classes themselves are created is called a metaclass. For most jobs, __init_subclass__ (Lesson 9) and descriptors are enough, so for this course just remember the name.
Move the check into the Positive checkpoint, and creating, changing and decreasing with -= all pass through the same checkpoint.
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
return self if obj is None else obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
b = Bag(20, 55)
for change in (lambda: setattr(b, "kg", -1), lambda: Bag(-3, 40)):
try:
change()
except ValueError as e:
print("blocked:", e)
# blocked: kg must be >= 0: -1
# blocked: kg must be >= 0: -3
b.kg -= 30
# ValueError: kg must be >= 0: -10Setting it with setattr, creating a new bag, decreasing with -=: all are blocked. b.kg -= 30 is two steps, reading b.kg (__get__) and writing back the result (__set__), so there is no way around the checkpoint.
Oh replays the scale records. No more negative weights come through. The supervisor says, "Better than locking doors is making every route go through the checkpoint."
Closing report — LOST-010. The weight check existed only in init, so the later b.kg -= 30 bypassed it. Solved by moving the check into a data descriptor (Positive) so every write passes through the checkpoint.
Positive so it takes a lower bound (kg = AtLeast(0), cm = AtLeast(10)). Use the value received in __init__ for the check.b.weigh = "x", at which step is b.weigh answered? What about b.kg = "x"?__getattr__, use print to check whether __getattr__ is called when reading an attribute set in __init__.The supervisor's notebook
@property puts a check on reading and writing an attribute. Keep the check in the setter. No setter means read-only.
A descriptor is a class attribute with __get__/__set__. __set_name__ tells it its name, so it can be reused on many attributes.
A function is a non-data descriptor with only __get__. Every read of b.m creates a new bound method.
staticmethod bundles nothing; classmethod bundles the class.
Read order: data descriptor > instance __dict__ > non-data descriptor > class attribute > __getattr__.
__getattribute__ always runs, __getattr__ only when nothing was found. The class of a class is type.
← Previous
Lesson 9 · The spec behind the spec: inheritance and MRO
Next →
Lesson 11 · Unclaimed bags: memory and GC
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.