27 · Classes, instances, attributes, methods
Define your first class with __init__ and methods, understand instance vs. class attributes, and spot the mutable-default footgun that bites almost everyone.
A **class** is a blueprint; an **instance** is a concrete object built from that blueprint. `__init__(self, ...)` runs when you call `ClassName(...)` — its job is to attach **instance attributes** with `self.x = x`. Every method receives `self` as its first argument so it can access and modify the instance it belongs to.
Without this:
Without classes you're forced to pass around loose dictionaries or tuples — no behaviour attached, no guarantees about what keys exist. Every sklearn estimator, every PyTorch module, and every config object in production ML code is a class.
A class bundles data (attributes) and behaviour (methods) into a single reusable object. You define a class with class Name: and give it an initialiser (__init__) that Python calls automatically when you write MyClass(arg1, arg2). Inside __init__, self refers to the new instance being created — self.x = x stores the value as an instance attribute visible to every method on that instance.
There are two kinds of attributes:
- Instance attributes — set inside
__init__onself. Each instance gets its own copy. - Class attributes — defined at the class body level (outside any method). All instances share the same object — which is a footgun when that object is mutable (list, dict).
Python naming conventions: _single_leading_underscore signals "protected, don't touch from outside"; __double_leading_underscore triggers name mangling (Foo.__bar becomes _Foo__bar), making accidental overrides in subclasses less likely. Magic (dunder) methods like __repr__ and __str__ let you control how an object looks when printed.
Python (in browser)
`__repr__` is the developer-facing string — it should be unambiguous and ideally reproduce the object when pasted into a Python shell. `__str__` is the user-facing string; if it's absent Python falls back to `__repr__`. Writing just `__repr__` gives you both for free in most contexts.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Each call to `LinearModel(...)` allocates a brand-new object. `m1.bias = 5.0` modifies only `m1` — `m2` is completely unaffected. This is the whole point of instance attributes.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Both `a.history` and `b.history` point to the **same list**. Appending through either instance mutates the shared object. This is almost never what you want.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
What does `__init__` return?
Build a class from scratch in the Arena. Focus on `__init__`, instance attributes, and at least one meaningful method.
- `class Name:` defines a blueprint. `__init__(self, ...)` attaches instance attributes via `self.attr = value`. Each instance gets its own copy of instance attributes.
- Class-level attributes are shared across all instances — dangerous with mutable defaults (lists, dicts). Move defaults into `__init__` to keep instances independent.
- `__repr__` returns the developer-facing string. Naming: `_protected` is convention; `__mangled` prevents subclass name collisions.
Every sklearn estimator is a class: `LinearRegression()` creates an instance, `.fit(X, y)` and `.predict(X)` are methods, `coef_` and `intercept_` are instance attributes written by `fit`. PyTorch `nn.Module` subclasses store layer weights as instance attributes. Experiment configs (learning rate, batch size, seed) are best represented as dataclass instances — lightweight classes with auto-generated `__init__` and `__repr__`.
If you remove it: Without classes you'd pass mutable dicts through every function in the pipeline — no type safety, no encapsulation, and no way to attach behaviour (like `.predict()`) next to the data it operates on.