Skip to content

Instances as Runtime Objects

Page Maps

graph LR
  family["Python Programming"]
  program["Python Meta-Programming"]
  section["Runtime Objects Object Model"]
  page["Instances as Runtime Objects"]
  capstone["Capstone evidence"]

  family --> program --> section --> page
  page -.applies in.-> capstone
flowchart LR
  orient["Orient on the page map"] --> read["Read the main claim and examples"]
  read --> inspect["Inspect the related code, proof, or capstone surface"]
  inspect --> verify["Run or review the verification path"]
  verify --> apply["Apply the idea back to the module and capstone"]

The first three core pages explain functions, classes, and modules as runtime objects. This page finishes that floor by making instance behavior equally concrete.

The sentence to keep is:

an instance is a runtime object with its own storage model, and attribute access works by combining that storage with the class lookup rules above it.

Once that sentence feels ordinary, __dict__, __slots__, method binding, and descriptor-based frameworks all become easier to review accurately.

The sentence to keep

When an instance attribute behaves unexpectedly, ask:

is the value coming from instance storage, slot storage, a descriptor on the class, or a fallback lookup hook?

That question narrows most instance-level confusion quickly.

How instances are created

Instance creation usually happens in two steps:

  1. cls.__new__ creates the instance object
  2. cls.__init__ initializes it

That does not mean every class customizes both methods. It means instance creation is a runtime protocol, not a single magical operation hidden behind ClassName().

The default storage model is __dict__

For ordinary classes, each instance carries a mutable dictionary of its own attributes:

class Example:
    pass


item = Example()
assert item.__dict__ == {}

item.x = 1
assert item.__dict__ == {"x": 1}

This is the default model most tools expect:

  • dynamic attributes are allowed
  • object state is easy to inspect with vars(obj) or obj.__dict__
  • generic serializers and debuggers often depend on that dictionary being present

That flexibility is useful, but it is not the only storage model Python supports.

__slots__ changes where state lives

Declaring __slots__ tells Python to allocate a fixed layout for named attributes:

class Point:
    __slots__ = ("x", "y")

Typical consequences:

  • Python does not create a per-instance __dict__ automatically
  • only the named slot attributes are allowed
  • instance storage becomes more memory efficient for large numbers of similar objects

That can be helpful, but it also changes the ergonomics and tool compatibility of the instances you create.

One picture of dictionary-backed and slotted instances

graph LR
  dictBacked["Dictionary-backed instance"]
  dictBacked --> dictClass["__class__ -> Class"]
  dictBacked --> dictStore["__dict__ -> {x: 10, y: 20}"]

  slotted["Slotted instance"]
  slotted --> slotClass["__class__ -> Class"]
  slotted --> slotX["slot x"]
  slotted --> slotY["slot y"]
  slotted --> noDict["no __dict__ unless requested"]

Caption: descriptor rules still decide semantics; slots only change where instance state is stored.

Read lookup combines class rules and instance storage

Instance attribute lookup is not "check the dictionary and stop." Under the default attribute machinery, the effective order is:

  1. data descriptor on the class or one of its bases
  2. instance storage
  3. non-data descriptor or plain class attribute
  4. __getattr__ fallback
  5. AttributeError

For this page, the key detail is that step 2 may mean one of two things:

  • a value in obj.__dict__
  • a value stored in a slot field

That is why this page sits after the class-object page. Instance behavior depends on class-level lookup rules and descriptor precedence.

Dictionary-backed and slotted instances behave differently

class Regular:
    def __init__(self, a, b):
        self.a = a
        self.b = b


class Slotted:
    __slots__ = ("a", "b")

    def __init__(self, a, b):
        self.a = a
        self.b = b


regular = Regular(1, 2)
slotted = Slotted(1, 2)

assert regular.__dict__ == {"a": 1, "b": 2}
print(hasattr(slotted, "__dict__"))  # Often False

The important difference is not only memory. It is also what kinds of runtime inspection and extension remain possible.

Pure slots reject dynamic attributes

class Point:
    __slots__ = ("x", "y")


p = Point()
p.x = 1
p.y = 2

try:
    p.z = 3
except AttributeError as exc:
    print("Expected:", exc)

That restriction is sometimes exactly what you want. It is also a reason many generic tools and quick debugging tricks stop working on heavily slotted designs.

Hybrid classes can bring __dict__ back

If you need fixed slots and dynamic attributes together, you can explicitly include "__dict__":

class Hybrid:
    __slots__ = ("x", "y", "__dict__")


h = Hybrid()
h.x = 1
h.note = "ok"

assert h.x == 1
assert h.__dict__ == {"note": "ok"}

This is a good reminder that __slots__ is a storage design choice, not a moral upgrade. You use it when it buys something concrete.

Inheritance changes the picture

Slot behavior becomes easier to reason about if you keep one rule in mind:

if a base class already provides a __dict__, subclasses effectively keep dictionary storage even when they add their own slots.

That means a deep inheritance chain can quietly regain dynamic attributes even when later classes declare __slots__.

Again, the point is not trivia. The point is to make instance layout a reviewable runtime fact instead of a half-remembered optimization story.

Methods connect instances back to class-stored functions

Instances also matter because instance attribute access is how ordinary methods become bound methods:

class Service:
    def run(self):
        return "ok"


svc = Service()
bound = svc.run

assert bound.__self__ is svc
assert bound.__func__ is Service.run

This is another reason Module 01 keeps returning to ordinary object relationships. The instance does not "contain the method implementation." It participates in a binding step that links the instance to a function stored on the class.

Guided lab: compare storage from evidence

Run:

$ python3 -m labs.runtime_objects |
  python3 -c 'import json, sys; print(json.load(sys.stdin)["instance"])'
{'regular_storage': {'title': 'queue lag'}, 'slotted_has_dict': False, 'slotted_rejects_undeclared_attribute': True, 'slotted_title': 'queue lag'}

Do not reduce this packet to "__slots__ is faster." It proves a narrower and more useful set of claims:

  • the regular object exposes its assigned title through vars(regular)
  • the pure slotted object has no instance dictionary
  • the declared title slot works
  • an undeclared attribute is rejected

The lab does not measure memory or speed, so it cannot support a performance claim. That limit is part of good evidence reading.

Change one storage contract

Add "__dict__" to SlottedIncident.__slots__, then predict all three observations that should change or stay fixed. The slotted object should gain a dictionary and accept the new severity attribute, while its title still works. Update the test only if you intend to teach the hybrid storage contract permanently; otherwise restore the original class and run make lab-test.

Transfer to the incident-plugin runtime

Create ConsoleNotifier() and inspect vars(instance). You will see private-looking storage names such as _prefix rather than the public field names alone. Module 01 can make two honest observations:

  • this concrete plugin instance is dictionary-backed
  • its storage shape is influenced by class-owned attribute behavior

Do not infer the descriptor policy from the dictionary alone. Modules 07 and 08 will explain why those storage names exist and which class objects own validation. For now, the capstone demonstrates that a larger application still rests on an observable instance storage model.

Review rules for instance-object reasoning

When reviewing instance-heavy designs, keep these questions close:

  • where does instance state actually live: __dict__, slots, or both?
  • does a descriptor on the class outrank the instance storage you thought you were using?
  • is __slots__ justified by a measured need, or is it adding constraint for style points?
  • will generic tooling, debugging, serialization, or monkeypatching break on these instances?
  • can you explain one method access as a binding relationship instead of as "class magic"?

What to practice from this page

Try these before moving on:

  1. Build one dictionary-backed class and one slotted class, then compare their ability to accept a new ad hoc attribute.
  2. Add "__dict__" back into a slotted class and explain what changed.
  3. Write down the full lookup story for one instance attribute that ends up coming from a class descriptor instead of instance storage.

If those feel ordinary, the last core can connect modules, classes, instances, functions, and bound methods into one runtime graph.

Continue through Module 01