learn
Weeks 11–13 · About 30 hours

Building your own kinds of thing

Classes let you bundle information together with the actions that work on it — and one object holding a reference to another is the idea every data structure ahead is built from.

Week 16 opens with “implement a from scratch”. A linked list is a class holding a reference to another object of the same class. Nothing more. If that sentence feels comfortable by the end of these three weeks, the next six months are navigable. If it does not, trees and graphs will be incomprehensible, and no amount of extra practice on them will help — the missing piece will be here.

Classes and objects

A class is a recipe for making values that keep related data and behavior together.

Reach for it whenUse a class instead of loose dictionaries and functions when many values share rules and stateful behavior; use plain data when no behavior needs an identity.

Think of it likeThe class is the recipe for a cake; each cake you bake from it is an object. Same instructions, different cakes.

You write class BankAccount: and then, indented inside it, the functions this kind of thing can do. Those functions are called methods.

The special method __init__ runs automatically whenever you make a new object, and its job is to set up the starting information. The double underscores mean Python calls it for you rather than you calling it by name.

Syntax you can use
class Dog:
Defines a new kind of thing. Capitalised by convention.
def __init__(self, name):
The constructor. It runs when you write Dog("Rex").
self.name = name
Stores a value on this particular object.
def bark(self):
A method. self is the first parameter of every one, and Python passes it for you.
rex = Dog("Rex")
Creates an instance. There is no new keyword.
isinstance(rex, Dog)
Asks whether an object is of a given type.
class Puppy(Dog):
Inherits everything from Dog, and may override parts.
super().__init__(name)
Calls the parent's version, so its setup still happens.
methodmethodAccountAda $8Lin $3
Notice that two instances keep separate state while both obtain behaviour from the same class.
Several ways to see it
Its shapeThe class holds behaviour; each instance holds state

Instances usually carry their own attribute dictionary, while methods live once on the class. Looking up `account.deposit` finds the class function and binds that particular account as `self`.

What the machine doesConstruction is a call followed by initialization

Calling a class creates an instance and then passes it to `__init__`. `__init__` configures an already-created object and must return `None`; it is not a factory result.

How it breaksA class attribute can accidentally become shared state

A list written in the class body belongs to the class and is visible through every instance. Create mutable per-object values inside `__init__` so one object's update cannot leak into another.

In the wild
  • Django models — define database-backed records as classes whose instances represent rows.
  • SQLAlchemy ORM — maps Python classes and their attributes to database tables and columns.
Commonly gets misread as
  • "A class is the object." Correction: the class is the recipe; each instance is a separate object made from it.
  • "self is a Python keyword." Correction: it is the conventional parameter name for the current instance.

Why objects exist at all

An object keeps one thing's changing state beside the operations that are allowed to change it.

Reach for it whenUse an object instead of passing the same bundle through many functions when it has an identity and rules; prefer plain functions for stateless transformations.

Without classes you might keep a balance in one variable and write separate functions that change it. Nothing stops some other part of the program setting that balance to minus a million.

With a class, the balance lives inside the object and the only way to change it is through methods you wrote — a withdraw method that refuses to overdraw, for instance. The rule and the data sit together.

Syntax you can use
from dataclasses import dataclass
Generates __init__, __repr__ and __eq__ from the fields you declare.
@dataclass class Point: x: int y: int
A whole class in four lines, with equality and printing that already work.
@property def area(self):
A computed attribute, read as shape.area with no parentheses.
@staticmethod
A function that lives on the class but needs no instance.
@classmethod def from_row(cls, row):
An alternative constructor, receiving the class instead of an instance.
Several ways to see it
Why it existsRelated state needed one enforceable boundary

Passing a balance, owner, and transaction list separately lets them drift apart or be combined incorrectly. An object gives those values one identity and puts the allowed transitions beside them.

Against the alternativeA dictionary stores fields; an object protects rules

A dictionary is often enough for passive records and flexible external data. Choose an object when operations must preserve invariants, such as forbidding a withdrawal that would violate an account's policy.

How it breaksNouns alone produce empty wrappers

Turning every record into a class adds ceremony without improving its behaviour. If a thing only carries values, a tuple, dictionary, or dataclass may state that honestly and more cheaply.

In the wild
  • Requests Session — keeps cookies, headers, and connection-pool behavior together across HTTP calls.
  • PyTorch nn.Module — holds parameters, child modules, and forward behavior as one trainable object.
Commonly gets misread as
  • "Objects only model physical things." Correction: sessions, jobs, parsers, and connections also have identity and state.
  • "More classes mean better design." Correction: objects earn their cost only when they protect useful boundaries.

Making objects printable

String representations are the views an object gives people for reading it and developers for diagnosing it.

Reach for it whenDefine __str__ instead of forcing callers to format a common label, and __repr__ when debugger and log output needs an unambiguous form.

print(my_account) with no __str__ gives you something like <__main__.BankAccount object at 0x7f9>. That is a memory location, and it tells you nothing.

Define __str__ to return a piece of text, and printing shows that instead. There is a sibling called __repr__ meant for programmers debugging, which is what shows up when an object appears inside a list.

Syntax you can use
def __str__(self):
What print(obj) and str(obj) show. For humans.
def __repr__(self):
What the prompt and containers show. For you, debugging — aim for something unambiguous.
return f"Dog({self.name!r})"
!r inserts the repr, so strings keep their quotes.
def __eq__(self, other):
Defines what == means for your type.
def __len__(self):
Makes len(obj) work. Same idea for __iter__, __contains__ and __getitem__.
Several ways to see it
Against the alternativeStr addresses the user; repr addresses the investigator

`__str__` may be friendly and omit internal detail, while `__repr__` should identify the type and the state needed to distinguish instances. Containers use their elements' repr, so a useful repr improves nested debugging too.

The Python you typeA constructor-shaped repr reveals the contract

`Book(title='Dune', pages=412)` makes field names and values explicit and is often close to code that could recreate the object. That shape is more diagnostic than a sentence such as 'a nice book'.

How it breaksRepresentations must not leak secrets

Objects appear in logs, exceptions, notebooks, and debugger panes without a deliberate print call. Redact tokens, passwords, and personal data in repr because developer-facing output is still output.

In the wild
  • IPython — uses rich representation methods and repr output to display the value of an expression.
  • pytest — uses object representations when showing differing values in assertion failures.
Commonly gets misread as
  • "__str__ and __repr__ must match." Correction: one may favor readers while the other favors diagnosis.
  • "repr should hide important fields." Correction: it should expose enough state to recognize the object.

Composition — objects holding other objects

Composition builds a larger object by giving it smaller objects that each do one job.

Reach for it whenUse composition instead of inheritance when the relationship is 'has a' or behavior should be replaceable; use inheritance for a genuine substitutable 'is a'.

Think of it likeA train. Each carriage is coupled to the next one. No carriage knows about the whole train — it only knows what is directly behind it.

A Library object can hold a list of Book objects. Nothing new is needed for this: an object is just a value, so it can be stored like any other value.

Now make an object that holds a reference to another object of the same class. A Node with a value, and a next that points at another Node. Chain three of them together and you have built a linked list, weeks before anyone calls it that.

Syntax you can use
self.engine = Engine()
Composition: an object holding another object, and the default choice.
def start(self): return self.engine.start()
Delegation. Forward the call to the part that owns the job.
def __init__(self, store): self.store = store
Dependency injection: the collaborator is passed in, so a test can pass a fake.
class Engine(Protocol):
From typing. Describes what a collaborator must provide, with no inheritance.
LibraryBook ABook BAuthor
Notice that containment is represented by references between separate objects, not by copying one object inside another.
Several ways to see it
Its shapeThe object graph says who owns whom

Draw each object as a node and each attribute reference as an edge. A library can point to many books without becoming a kind of book; the direction of the edge expresses the relationship.

Against the alternativeHas-a survives change better than is-a

Inheritance commits a child to the parent's public contract and lifecycle. Composition lets you replace one collaborator, such as a file store with a database store, while preserving the containing object's role.

How it breaksDeep chains expose another object's insides

Code like `order.customer.address.city.name` couples a caller to five layouts. Ask the nearest object for the fact or action you need so internal rearrangements do not ripple outward.

In the wild
  • PyTorch nn.Module — registers child modules as attributes to assemble larger neural networks.
  • FastAPI — assembles an application from router, middleware, dependency, and response components.
Commonly gets misread as
  • "Composition means copying code into one class." Correction: the outer object delegates work to objects it holds.
  • "Inheritance is always more object-oriented." Correction: composition usually couples parts less tightly.

A gentle first look at recursion

is a function solving one small case by asking another copy of itself to solve a smaller case.

Reach for it whenUse recursion instead of a loop when data is naturally nested, such as folders or trees; use a loop when depth can be large or state is simple.

Think of it likeBeing in a queue and wanting to know your position. You ask the person in front what number they are, and add one. They ask the person in front of them. The person at the very front knows they are first without asking — that is the stopping rule.

Every recursive function needs two things. A : the simplest situation, answered directly without calling itself. And a recursive case: the function calling itself on something smaller, moving towards that base case.

Factorial is the standard first example. The factorial of 5 is 5 times the factorial of 4. The factorial of 1 is just 1 — no further calls. Each step is smaller than the last, and there is a floor to hit.

Syntax you can use
def countdown(n): if n == 0: return countdown(n - 1)
A function calling itself, with a base case that stops it.
return 1 if n <= 1 else n * factorial(n - 1)
The recursive step. Every call must move toward the base case.
import sys; sys.setrecursionlimit(10000)
Raises the default limit of about a thousand frames.Usually the wrong fix. Deep recursion in Python normally wants a loop and an explicit stack.
from functools import cache
Decorate with @cache and repeated calls with the same arguments are remembered instead of recomputed.
sum(3)waitsum(2)waitsum(1)base
Notice that calls accumulate on the way down and return in the opposite order once the base case is reached.
Several ways to see it
Its shapeThe answer is nested inside a smaller answer

A recursive definition has one edge that stops and another that points to the same shape at a smaller size. Factorial removes one number; a nested list walk removes one layer or one item.

What the machine doesEvery unfinished call stays on the pile

A call pauses at the recursive line while a smaller call runs, preserving its local values and return location. Results then travel back in reverse order as those frames finish.

How it breaksSmaller must be measurable

A base case is useless if the recursive step never approaches it. Name the quantity that decreases on every call—length, depth, or remaining range—and test the smallest valid and invalid inputs.

In the wild
  • Git — follows parent commit links repeatedly when walking project history.
  • Python json module — descends through nested objects and while encoding or decoding JSON.
Commonly gets misread as
  • "Recursion stops by itself." Correction: a base case must explicitly stop further calls.
  • "Recursion uses no extra storage." Correction: each unfinished call occupies a .

Reading about this stage is not the same as finishing it. Type every one of these from a blank file rather than copying — the writing is the part that teaches.

BankAccount class

Methods to deposit, withdraw and check the balance. Withdrawing more than the balance must be refused rather than allowed to go negative. Make two accounts and confirm that depositing into one does not change the other — that is self doing its job.

Book and Library classes

A Book has a title and an author. A Library holds a list of Book objects, and can add one, list them all, and find one by title. Your first object that contains other objects.

Chain three Node objects by hand

Write a Node class holding a value and a next that starts as None. Make three of them. Set the first one's next to the second, and the second's next to the third. Then write a loop that starts at the first and follows next until it runs out, printing each value. You have just built and traversed a linked list. Sit with that for a moment — in week 16 this gets a name and a full set of operations, but the idea is exactly what you did here.

Recursive factorial and list-sum

Write factorial recursively. Then write a function that adds up a list recursively: the sum of a list is its first item plus the sum of the rest, and the sum of an empty list is 0. Write each one with a loop as well, and compare which version you find easier to read.

Exit checkYou can write a class from scratch, create two instances of it, and explain out loud what self refers to. You have manually chained node objects together and walked the chain with a loop.

If you cannot do this, repeat the stage rather than moving on. Nothing later gets easier by skipping it.

Worth knowing before you start

  • This is the second place beginners commonly quit. self feels arbitrary until it suddenly does not. Push through rather than skipping ahead — everything from week 16 onwards assumes it.
  • Do not move on until the Node chaining exercise makes sense. It is the single most load-bearing exercise in the first thirteen weeks.

This stage corresponds to Phase 0 — Classes, Objects & Recursion Warm-Up in the source roadmap.