Home Session 04
Objects · Session 04

Classes & Objects

Think of a class as a recipe and an instance as one cake. self just means “this cake.” Once you see where the ingredients live versus where the instructions live, object-oriented Python stops feeling magical.

~45 minutes
5 interactive demos
Builds on Sessions 01–03
Learning goal

Data on the cake. Instructions on the recipe.

Each instance keeps its own attributes. Methods are shared on the class — they are not copied onto every object. self is just that instance being passed in. Reading a name walks instance → class → parent classes.

class instance bound method __dict__ __init__ MRO
Recommended path
1 Watch construction. Class first, then instance, then __init__ writing attributes.
2 Ask where the name lives. Instance dict, class dict, or a bound method object?
3 Reuse earlier rules. Storing a list on self is still aliasing. Inheritance is just a longer lookup.
Interactive lab

Follow a class from definition to a method call

Watch the recipe appear, then one cake, then the cake getting its own labels. The memory panel shows class, instance, and call frames. Each instance has a __class__ link back to its type, and every arrow you see in a __dict__ row is a reference to another box on the same panel.

Tip: when you see self, find the instance it points at. Same address means same object.
1 Choose a demo
2 Step with or Play
3 Watch class vs instance dicts
4 Read the model below
Call stack — names, including self
Heap — class, instance, method. __class__ is the type link
next back Space play R reset
python
1 / 1
Memory
stack heap
1

Initial state — only the global frame

Press Play or use Next to watch a class become an instance.

What is self?

In plain words self is not a magic keyword. It means “the object I am talking about right now.” When you write p = Point(3, 4), Python hands that new point into __init__ as self so the function can label it.

It is the conventional name for the instance this method is running on. Python passes it for you on p = Point(3, 4) or g.hello("Ada"). You could spell the first parameter anything; the runtime only cares that it is bound to the instance.

Class object

The indented body runs first and makes the function objects. Then Python builds the class from that namespace and binds the name Point to it.

Instance + __init__

Calling the class allocates an instance, then calls __init__(self, ...) with that instance as self.

Instance __dict__

self.x = 3 writes into the instance's attribute mapping. The class is unchanged.

Model
You could name it anything
The first parameter of an instance method is just a parameter. Python programmers write self by convention. The runtime does not require that spelling. What matters is that it is bound to the instance.
Attribute lookup

Instance first, then class

In plain words Reading a name is like asking “does this cake have that label?” If not, ask the recipe. If the recipe inherited from another recipe, keep asking. Writing a name usually just sticks a new label on this cake — it does not edit the shared recipe.

Reading a.lives searches a.__dict__, then the class, then the method resolution order (MRO). Writing a.score = 10 does not search — it writes into the instance dict.

Reading

Walk outward

Instance dict → class dict → bases. That is why every Player() can read lives even when the instance dict is empty.

Writing

Usually stay local

a.score = 10 creates an instance attribute. It does not change Player.score or b.score.

Care
a.lives = 1 is not the same as Player.lives = 1
The first shadows lives only on a. The second changes the shared class attribute. If lives were a mutable list stored on the class, a.lives.append(1) would mutate the shared list — lookup finds the class object, then mutation changes it.
Edge
Two places the simple rule bends
“Instance first” is the right default and it covers almost everything you will write. Two exceptions are worth knowing. If the class defines the name as a @property, the class wins and the instance dict is never consulted — that is why a.lives = 1 would raise AttributeError: property 'lives' of 'Player' object has no setter instead of quietly shadowing it. Properties are data descriptors, and those outrank the instance; plain methods are non-data descriptors and do not, so a value forced into a.__dict__['speak'] would still win over the class function. The other exception: a class that declares __slots__ can have no instance __dict__ at all — its attributes sit in fixed slots. The search order is unchanged; the box we keep drawing is simply not there.
Methods

Accessing a function on an instance binds self

In plain words The greeting function lives on the recipe, shared by every greeter. When you say g.hello, Python staples g onto that function so you do not have to pass it yourself. That staple is why the definition has two parameters and the call looks like it passes one.

g.hello is a bound method: a small object that remembers the function and the instance. Calling it supplies self automatically.

Expression What you get Who fills self?
Greeter.hello The raw function on the class You must pass the instance: Greeter.hello(g, "Ada")
g.hello A bound method The method object already holds g
g.hello("Ada") A call through that bound method Python, automatically
Try this in a REPL

After g = Greeter(), print g.hello and Greeter.hello. Then compare g.hello.__self__ is g. You should see True.

type and __class__

Every instance knows its class

In plain words A cake does not carry a photocopy of the whole recipe. It carries a note that says “I was made from Point.” When you ask it to do something it does not have on itself, it follows that note back to the recipe.

That note is type(p) / p.__class__. Methods stay on the class. Lookup starts on the instance, then follows this pointer.

p
->
Point() @ 0x7f720010 __class__ -> Point __dict__ { x, y }
Point
->
class Point @ 0x7f7100a0 __init__ -> function total -> function

In plain words Making a point is two jobs: first get a blank cake tin, then write the ingredients on it. You almost never handle the tin yourself. Point(3, 4) does both, then hands you the finished cake.

Those two jobs are __new__ (allocate) and __init__ (initialize). The instance already exists by the time __init__ gets it as self, and Point(3, 4) hands you that instance — not whatever __init__ returned.

Allocate

When you would override __new__

Almost never. The cases that exist are real, though: subclassing an immutable type such as int, str or tuple, where the value has to be decided at allocation time, and singleton-style classes that return an existing object instead of a fresh one.

Initialize

__init__ must return None

This is enforced, not just a convention. Slip a return self or return 5 into __init__ and Python raises TypeError: __init__() should return None, not 'int'. Its job is to fill the object in, not to produce one.

Inheritance

A subclass does not copy methods

In plain words A puppy does not get a photocopied list of every parent trick. If it does not know speak, it asks its parent recipe. The trick still runs on this puppy — not on some generic animal.

class Dog(Animal) creates a new class and records Animal in __bases__. The MRO is the search list: Dog → Animal → object. d.speak walks that list, then binds self to d.

1 · d instance __dict__ miss
2 · Dog class namespace — pass copied nothing miss
3 · Animal finds speak, then binds self to d hit
Model
self is still the subclass instance
The function was found on Animal, but the bound method remembers d. That is how a base method can read subclass attributes: it receives the actual instance. If Dog later defines its own speak, lookup stops there.
Try this in a REPL

After the inheritance demo, print Dog.__mro__ and d.speak.__func__ is Animal.speak. You should see the search order and True.

Pitfalls

The bugs this model prevents

In plain words If something “weird” happens with a class, ask three everyday questions: did I write on this one object or on the shared recipe? Did I share a list by accident? Did I think each object got its own copy of the methods?

Those map to lookup vs assignment, mutable class attributes, and the fact that methods live on the class — plus aliasing through self.

A list on the class is one list for every instance

class Team: members = [] then a.members.append("Ada") mutates the class attribute. Lookup found the shared list; mutation changed it. Put self.members = [] inside __init__ instead.

self.items = items does not copy

Demo 4 is Session 03 in a new costume. If the caller still holds the list, append on the instance is visible outside. Copy when you need a private container.

Methods are not copied onto each instance

Creating a thousand Point objects does not create a thousand __init__ functions. Instances share the class. That is why __class__ matters.

What you should be able to draw

A class (the recipe) with shared methods. Instances (the cakes) with their own __dict__ and a __class__ arrow back. A bound method that staples the instance onto the function. A subclass with __bases__ that asks the parent instead of copying methods. And self in a call frame pointing at the same instance the caller used.

Check your understanding

Can you predict the memory?

Ask: is this on the cake, on the recipe, or stapled onto a method?

1. After p = Point(3, 4), where does the name x live?
self.x = x copies a reference into the instance dict. The __init__ locals disappear when the call returns.
2. a and b are two Player() instances. You run Player.lives = 2. What do they see?
Lookup misses on the instance and finds lives on the class. An instance assignment would shadow that name for that instance only.
3. Why can you call g.hello("Ada") when hello is defined as hello(self, name)?
Attribute access on the instance creates a bound method. The call only needs the remaining arguments.
4. class Team: members = [] then a = Team(); b = Team(); a.members.append("Ada"). What is b.members?
Reading a.members misses on the instance and finds the class list. append mutates that one object. Put self.members = [] in __init__.
5. d is a Dog() and speak is defined only on Animal. Where does d.speak come from?
Inheritance is a search path (__class__, then __bases__ / the MRO). The bound method still remembers the subclass instance.