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 7

Time 5:00–5:50

The bag spec: classes and instances

Lost-baggage report

LOST-007

One sticker, stuck on every case

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.

7-1. A spec is a bag too: class objects (7 min)

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 holdsHow to look
Class (spec)Names created in the class body: class attributes, methodsvars(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".
  • When reading an attribute, Python looks in the instance's __dict__ first, and if it is not there, in the class. If it's not on the bag, you open the spec.
  • A class's __dict__ is a read-only window called a mappingproxy. To change a class attribute, assign to the class directly, as in Case.size = "L".

7-2. Class attributes everyone shares (10 min)

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 False
  • stickers = [] 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).
  • It is the same shape of bug as the default argument in Lesson 4: "one mutable object, created once, shared by many".

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)

In the first version the list exists only in the class Case box, and the two instance boxes hold just owner. With __init__, each instance gets its own list; in the last version, only a has its own list.

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)
# S
  • Change the class attribute, and every instance that does not hide it (b) sees the change immediately.
  • Remove the instance's name with 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__.

7-3. The reading order: instance, then class (7 min)

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.

1

Data descriptor on the class (__get__ + __set__)?

2

In the instance __dict__?

3

Non-data descriptor on the class (only __get__, like a function)?

4

A plain class attribute?

5

Is there __getattr__?

6

AttributeError

Result: b. →

owner and size are found at step 2, the instance __dict__; stickers at step 4, the class. label is a function at step 3, and weight is nowhere, so it raises AttributeError.
  • 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.

7-4. Bound methods: a new one every time you take it out (6 min)

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 function
  • Every time you write bag.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.
  • How a function turns into a method when you take it out is explained in Lesson 10 (descriptors).

Tag board

Step 1 / 0

Next to run: 0

Preparing the lab…

Just changed

Sealed bag (immutable)

m1 and m2 are different bound method boxes, but their __self__ points to the same instance and their __func__ to the same function in class Bag.

7-5. Handling names as strings: 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.
  • You can attach new attributes to a Python object at any time. Handy, but a typo (bag.colour = ...) also quietly becomes a new attribute.

7-6. Hardening the spec: __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 attributes

Since 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'>}

Case closed (10 min)

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

Exercises

  1. Leaving 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.)
  2. If a class with __slots__ = ("owner",) has a class attribute size = "M", can you read s.size on an instance? What about s.size = "L"?
  3. Write a loop that uses 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.

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