R

ReflexKR

Reflex Docs

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

Name Tag Airport: Python's Object Model
Lesson 9

Time 6:40–7:30

The spec behind the spec: inheritance and MRO

Lost-baggage report

LOST-009

One of the two rules went missing

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."

9-1. Parent specs: inheritance and overriding (7 min)

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 True

Reading 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).

9-2. Multiple inheritance and the diamond (8 min)

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 red
  • rule 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

C3 steps
With (CarryOn, Hazmat), rule is found in CarryOn; with (Hazmat, CarryOn), in Hazmat. (Item, Hazmat) cannot produce an MRO, so the class is never created at all.

9-3. 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']
  • For PowerBank(), the chain runs CarryOn → Hazmat → Item.
  • For 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)

With super(), the line marker goes 6 → 9 → 3 and three check boxes pile up; the self in all three is the same PowerBank bag. With Item.check(self), it jumps straight 6 → 3 and there are only two boxes. The __class__ cell attached to functions that use super() tells them 'which class am I inside right now'.

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().

9-4. C3 linearization: the rule that sets the order (6 min)

Python builds the MRO with a method called C3 linearization. It keeps three promises.

  1. A child always comes before its parents.
  2. The order of parents written in the parentheses is kept.
  3. The MRO order of each parent is kept too.

Here is the calculation for PowerBank(CarryOn, Hazmat). Line up the parents' MROs and the list of parents side by side.

Remaining listsPickedWhy
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat]CarryOnThe first head. It is not in the tail of any other list
[Item, object] [Hazmat, Item, object] [Hazmat]HazmatItem is in the tail of the second list, so it waits
[Item, object] [Item, object]ItemIt 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, Hazmat

By 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.

9-5. Mixins, 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

C3 steps
With LabelMixin on the left, describe is found in the mixin; on the right, Item's is found first and the mixin's describe is hidden. prefix, which does not overlap, is found either way.

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)
# False

Use 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']

9-6. Abstract specs and registering child specs (3 min)

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'

Case closed (10 min)

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().

Exercises

  1. For 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__.
  2. In the "Case closed" code, predict what the checklist becomes if only Hazmat.check is changed to return ["fire"] (so it doesn't call super()).
  3. Add one more rule for weight to the 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.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex