Classes & OOP in Python
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This cover opens the mechanics post by promising to demystify the parts of classes that feel like magic: __init__, self, and method lookup across inheritance. The framing — that the quirks stop being mysterious once you watch Python build an object step by step — sets the tone for a precise, machine-level explanation rather than hand-waving.
The magnifying-glass framing signals the engine-room post. Where the concept post gave the shape and the why post gave the motivation, this one gives the exact rules — object creation, attribute lookup order, super() — that let you predict any class's behavior without running it.
The __init__ explanation corrects a near-universal misconception: __init__ is not the constructor that creates the object. By the time __init__ runs, the object already exists — self is a real, blank instance. __init__'s job is purely to initialize it, setting up its attributes. The actual allocation happens earlier, in __new__, which you almost never need to override.
This distinction matters because beginners who think __init__ 'makes' the object are confused when self already works inside it, or when they encounter __new__ in advanced code. Holding 'object created first, then initialized' straight explains the whole creation sequence and why you assign to self.something rather than returning a new object.
The pipeline diagram makes the object-creation sequence explicit as ordered stages: you call the class like a function, __new__ produces a blank object, __init__ fills in its attributes via self, and the ready object is returned to you. This is the runtime reality behind the single line Dog('Rex').
Laying it out as a pipeline reinforces that two distinct steps hide behind one call. The egg icon for __new__ and the writing icon for __init__ capture the split: one hatches a blank instance, the other writes its initial state. Seeing the stages separated explains why __init__ takes self as its first argument — the object it's about to fill already exists by then.
The self-passing rule is the mechanical heart of how methods work, so this slide states it plainly: rex.bark() is shorthand that Python rewrites as Dog.bark(rex). It looks up bark on the class and passes the instance in as the first argument. That's the entire reason every method declares self — it's the slot that catches the object.
Understanding this rewrite demystifies a whole class of errors. Forget self in a method signature and Python still tries to pass the instance, so you get a 'takes 0 arguments but 1 was given' error. Knowing that the dotted call and the explicit class call are literally the same operation makes method dispatch concrete rather than magical.
This code slide proves the self-passing rule by showing both forms produce identical output. rex.bark() and Dog.bark(rex) return the same string because they are the same call — the first is just sugar for the second. Seeing them side by side makes the automatic passing of self impossible to dismiss as magic.
The practical value is diagnostic. When a method complains about argument counts, the fix is almost always a missing self, and understanding the Dog.bark(rex) form explains exactly why: the instance is always passed, so the signature must have a slot to receive it. Running both lines and confirming they match cements the equivalence.
Instance versus class attributes is a frequent source of subtle bugs, so the rule deserves emphasis: an attribute assigned on self belongs to one object, while an attribute declared in the class body is shared by every instance. Attribute lookup checks the instance first and only falls back to the class if the instance doesn't have it.
This lookup order is exactly what makes class-level mutable defaults dangerous. A shared list declared in the class body is found by every instance through the same fallback, so they all see and mutate the one object. The slide flags this as a classic trap and the closing mistake demonstrates it in full — but the mechanism is right here in the lookup order.
This code slide makes the instance-versus-class distinction concrete. species is declared in the class body, so both dogs report the same value — they share that one class attribute. name is assigned on self in __init__, so each dog has its own. Printing both for two objects shows the shared value identical and the per-object values different.
The takeaway is to be deliberate about where an attribute lives. Genuinely shared constants — a species name, a default rate — belong in the class body. Anything that varies per object, and especially anything mutable, belongs on self inside __init__. This choice is the difference between intended sharing and the accidental sharing the mistakes post warns about.
The attribute-lookup rule is the key to understanding inheritance, so this slide spells out the search order: when you access rex.x, Python looks in the instance's own namespace first, then the instance's class, then each parent class up the inheritance chain, stopping at the first match. This ordered search is the Method Resolution Order, the MRO.
This single mechanism explains how a subclass can use a method it never defined — the method is simply found on a parent during the climb. It also explains overriding: if the subclass defines its own version, that's found first and wins. Internalizing 'instance, then class, then parents, first match wins' lets you predict exactly which method runs for any call.
The flow diagram visualizes the lookup climb for a method the instance and its immediate class don't define. The search starts at the instance rex, finds no match, moves to class Dog, still no match, then reaches the parent class Animal where speak() is found and run. The first match along the chain wins.
Seeing the search as a directed climb makes the MRO tangible. It's the same upward traversal the tree diagram in the concept post hinted at, now shown as an active search rather than a static structure. This picture is what makes super() in the next slide make sense — calling the parent means jumping deliberately to a specific point up this same chain.
This code slide demonstrates super(), the mechanism for a subclass to reuse its parent's behavior. Dog's __init__ calls super().__init__(name) to run Animal's setup — which assigns self.name — before adding its own breed attribute. The result is an object with both name and breed correctly set.
The pattern is essential because without the super() call, Dog's __init__ would completely replace Animal's, and name would never get set. super() lets a subclass extend rather than discard its parent's initialization. It walks the same MRO chain from the lookup slides, delegating to the next class up, which is why it composes correctly even in more complex inheritance graphs.
The tips slide consolidates the post's mechanics into five rules you can recall on demand: __init__ fills in an already-created object, the dotted method call rex.m() becomes Class.m(rex), attribute lookup goes instance then class then parents, the first match in the MRO wins, and super() delegates to the parent class.
Together these rules let you predict any class's behavior — where its data lives, which method a call will run, and how inheritance resolves — by inspection alone. That predictive power is the real deliverable of the post: not just writing classes, but reasoning about exactly what Python will do with them before you run the code.
The closing mistake targets the single most common mechanical OOP bug, and it follows directly from the lookup order: a mutable attribute declared in the class body is shared by every instance. Define tags = [] in the class body and appending to one object's tags mutates the list all objects see, because they all resolve tags to the same class-level object.
The fix is mechanical and consistent: create per-instance mutable state inside __init__ with self.tags = []. The bug is insidious because it's silent — everything looks fine until two objects mysteriously share data. Understanding that it traces straight back to the instance-then-class lookup order is what lets you both avoid it and recognize it instantly when it appears.
This CTA moves from theory to practice. Having built a precise mental model of how Python creates objects and resolves attributes, the reader is ready to build real classes that exercise these mechanics — inheritance, properties, and the dunder methods that hook into Python's syntax.
The teaser promises a hands-on, code-heavy tour, signaling that the Code Example post turns the mechanical understanding into the concrete patterns you'll type when defining your own classes.