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 6:40–7:30
Lost-baggage report
LOST-009
Reported by
Han, developer for air-cargo regulations
What went missing
The hazmat check for PowerBank, the spec for portable batteries. It inherits from both the 'carry-on' rules and the 'hazmat' rules, yet the check results have no hazmat item.
To close the case
Reveal the order in which PowerBank reads its rules (the MRO), and fix it so both rules run.
Haneulseom International Airport · Baggage Center
Han has created a new spec for portable batteries. It inherits from both the carry-on spec and the hazmat spec, so Han expected both checks to run. But the checklist shows only the size check and no hazmat check.
Supervisor Do Miran lays the two spec sheets on top of each other. "When there are two parent specs, the order they're read in is fixed. Let's start with that order."
class CarryOn(Item): means "whatever CarryOn doesn't have, look for in Item". This lookup order is written in the class's __mro__ (Method Resolution Order).
class Item:
kind = "item"
def label(self):
return f"[{self.kind}]"
class CarryOn(Item):
kind = "carry-on"
b = CarryOn()
print(b.label())
# [carry-on]
print(CarryOn.__mro__)
# (<class '__main__.CarryOn'>, <class '__main__.Item'>, <class 'object'>)
print("label" in vars(CarryOn), "label" in vars(Item))
# False TrueReading b.label() works just as in Lesson 7. It is not in the instance __dict__, so Python scans __mro__ from the front. CarryOn has no label, and it is found in Item. But self.kind inside label is again looked up starting from the instance, so you get CarryOn.kind, which is "carry-on".
Overriding is not special syntax. Write the same name in the child spec and, in the MRO, the first match wins. If you want to build on the parent's version, call it with super().
class Fragile(Item):
kind = "fragile"
def label(self):
return super().label() + "!"
print(Fragile().label())
# [fragile]!Every class's MRO ends with object. Basic handles like __repr__ and __eq__ all come from there (Lesson 8).
The reporter's spec inherits from two specs. Both inherit from Item, so drawn as a picture it forms a diamond.
class Item:
rule = "item"
tag = "plain"
class CarryOn(Item):
rule = "carry-on"
class Hazmat(Item):
rule = "hazmat"
tag = "red"
class PowerBank(CarryOn, Hazmat):
pass
print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank.rule, PowerBank.tag)
# carry-on redrule was found in CarryOn first. The parent on the left inside the parentheses comes first.CarryOn has no tag, so it was found in the next slot, Hazmat. The lookup never goes down to Item's "plain".Item appears only once, and after both children are done. It is not depth-first, digging all the way down the left path.In the explorer below, swap the order of the parents, and also click the last spec, where no order can be built.
MRO explorer
Attribute
Lookup order (__mro__)
Not found anywhere
super() is the next slot, not the parent (10 min)Now for the reporter's actual code. Each rule returns its check items and appends the items of the rules above it.
class Item:
def check(self):
return ["item"]
class CarryOn(Item):
def check(self):
return ["size"] + Item.check(self)
class Hazmat(Item):
def check(self):
return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
pass
print(PowerBank().check())
# ['size', 'item']Whoever wrote CarryOn thought "my parent is Item" and called Item.check(self) directly. Used on its own, that is correct. But in PowerBank's MRO, the slot after CarryOn is not Item but Hazmat. Calling Item directly skipped Hazmat.
super() is not "my parent". It is the slot after the current class in the MRO of type(self). The very same CarryOn.check has a different next slot depending on who calls it.
class CarryOn(Item):
def check(self):
return ["size"] + super().check()
class PowerBank(CarryOn, Hazmat):
pass
print(PowerBank().check())
# ['size', 'fire', 'item']
print(CarryOn().check())
# ['size', 'item']
print(super(CarryOn, PowerBank()).check())
# ['fire', 'item']PowerBank(), the chain runs CarryOn → Hazmat → Item.CarryOn() alone, the MRO is CarryOn, Item, object, so super() is simply Item.super(CarryOn, obj) spells out "search the MRO of obj starting after CarryOn". A bare super() fills in those two values for you.Compare the two versions on the tag board below. Watch the call boxes pile up, and the number of the line being run.
Tag board
Step 1 / 0
Next to run: 0
Preparing the lab…
Just changed
Sealed bag (immutable)
The chain holds only if everyone calls super(). If even one class leaves it out, the slots after it never run. The same goes for __init__. When each spec takes different arguments, each one takes out its own and passes the rest on to the next slot with **kw.
class Item:
pass
class CarryOn(Item):
def __init__(self, cm=55, **kw):
self.cm = cm
super().__init__(**kw)
class Hazmat(Item):
def __init__(self, wh=100, **kw):
self.wh = wh
super().__init__(**kw)
class PowerBank(CarryOn, Hazmat):
pass
print(vars(PowerBank(cm=20, wh=99)))
# {'cm': 20, 'wh': 99}CarryOn.__init__ takes out only cm and passes wh=99 along. The next slot, Hazmat, receives it, and the final object.__init__() ends with no arguments.
Pin says
→ If a method calls its parent class by name (Item.check(self)), multiple inheritance can skip the specs in between. When you build on the next one, always use super().
Python builds the MRO with a method called C3 linearization. It keeps three promises.
Here is the calculation for PowerBank(CarryOn, Hazmat). Line up the parents' MROs and the list of parents side by side.
| Remaining lists | Picked | Why |
|---|---|---|
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat] | CarryOn | The first head. It is not in the tail of any other list |
[Item, object] [Hazmat, Item, object] [Hazmat] | Hazmat | Item is in the tail of the second list, so it waits |
[Item, object] [Item, object] | Item | It is no longer in any tail |
[object] [object] | object |
The result is PowerBank followed by CarryOn, Hazmat, Item, object. Expand "C3 steps" in the explorer and you will see the same process in four lines. On each line, the first entry is the class picked at that step, and the rest are the lists remaining at that point.
If not all the promises can be kept, the class is not created.
class Item:
pass
class Hazmat(Item):
pass
class Odd(Item, Hazmat):
pass
# TypeError: Cannot create a consistent method resolution order (MRO) for bases Item, HazmatBy the parentheses order, Item must come before Hazmat (promise 2), and by the child-first rule, Hazmat must come before Item (promise 1). No ordering satisfies both. The fix is simple: put the more specific parent on the left.
isinstance, and composition (6 min)A mixin is a small class not meant to be used on its own; it mixes one feature into other classes. Its name ends in Mixin, and it goes on the left of the parent list. That way it is found first in the MRO.
class ReprMixin:
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
return f"{type(self).__name__}({fields})"
class Tag(ReprMixin):
def __init__(self, code, kg):
self.code, self.kg = code, kg
print([Tag("ICN", 12)])
# [Tag(code='ICN', kg=12)]MRO explorer
Attribute
Lookup order (__mro__)
Not found anywhere
isinstance(obj, C) checks whether C is in the MRO of type(obj). issubclass asks the same thing about two classes. So they are broader than type(x) is C.
class Item:
pass
class Hazmat(Item):
pass
class PowerBank(Hazmat):
pass
pb = PowerBank()
print(isinstance(pb, Item), issubclass(PowerBank, Hazmat), isinstance(pb, (int, Hazmat)))
# True True True
print(type(pb) is Hazmat)
# FalseUse inheritance when "X is a kind of Y". When "X has a Y", composition (holding another object as an attribute) is better. If the reporter's rules were held as a list of rule objects instead of inherited, the order would be visible and there would be no MRO to puzzle over.
class Rule:
def __init__(self, name, test):
self.name, self.test = name, test
class Inspector:
def __init__(self, *rules):
self.rules = list(rules)
def failed(self, item):
return [r.name for r in self.rules if not r.test(item)]
size = Rule("size", lambda it: it["cm"] <= 55)
fire = Rule("fire", lambda it: it["wh"] <= 100)
print(Inspector(size, fire).failed({"cm": 20, "wh": 160}))
# ['fire']Inherit from abc.ABC and mark a method with @abstractmethod, and it becomes "a slot every child must fill". A child that doesn't fill it cannot be instantiated. __init_subclass__ is a handle called on the parent every time a child class is created, so it is used to build a registry of specs.
from abc import ABC, abstractmethod
class Rule(ABC):
registry = []
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
Rule.registry.append(cls.__name__)
@abstractmethod
def check(self, item): ...
class SizeRule(Rule):
def check(self, item):
return item["cm"] <= 55
class FireRule(Rule):
pass
print(Rule.registry, SizeRule().check({"cm": 40}))
# ['SizeRule', 'FireRule'] True
FireRule()
# TypeError: Can't instantiate abstract class FireRule without an implementation for abstract method 'check'Replace Item.check(self) in CarryOn with super().check(), and print the checklist again along with the MRO.
class Item:
def check(self):
return ["item"]
class CarryOn(Item):
def check(self):
return ["size"] + super().check()
class Hazmat(Item):
def check(self):
return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
pass
print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank().check())
# ['size', 'fire', 'item']fire is back on the checklist. Han nods. "So I shouldn't have called the parent. I should have called whoever's next." The supervisor binds the two spec sheets together again. "Python decides the order. All we have to do is not break the chain."
Closing report — LOST-009. CarryOn called its parent by name and skipped Hazmat, the next slot in the MRO. Solved by making every spec call super().
class A: pass, class B(A): pass, class C(A): pass, class D(C, B): pass, work out the MRO with a C3 table first, then check it with D.__mro__.Hazmat.check is changed to return ["fire"] (so it doesn't call super()).Inspector from 9-5, then change the order of the rules and see how the order of the result list changes.The supervisor's notebook
Attributes are looked up in Cls.__mro__ order. Overriding just means the first match wins.
In multiple inheritance, a shared parent comes once, after all its children (C3 linearization).
super() is not the parent but the next slot in the MRO of type(self). Everyone must call it for the chain to hold.
In an __init__ chain, take out your own arguments and pass the rest on with **kw.
If no MRO can be built, the class is never created. Put the more specific parent on the left.
Mixins go on the left. isinstance looks at the MRO. If it 'has a', prefer composition over inheritance.
← Previous
Lesson 8 · Standard airport procedures: dunder methods
Next →
Lesson 10 · The checkpoint: how attribute access really works
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.