2.7 Classes and objects
Checked against the Python 3 documentation, August 2026
What this is and why it exists
Classes let you model a thing — a dataset, an experiment, a pipeline stage — as something with state and behaviour, instead of a tangle of nested dictionaries whose keys nobody remembers. You also need them to read the libraries ahead: a PyTorch module and a scikit-learn estimator are both classes with a contract you are expected to follow. And this topic carries its own trap, which is reaching for a class when a function would have been clearer.
The vocabulary
- Class — the description; instance — one object made from it.
- Attribute — a value belonging to an object.
- Instance attribute — set per object, usually in the initialiser.
- Class attribute — set on the class and shared by every instance.
- Method — a function belonging to a class, taking the instance as its first parameter.
- The self parameter — that first parameter; the instance the method was called on.
- staticmethod — a function living in the class that needs neither the instance nor the class.
- classmethod — one that receives the class, commonly used to build alternative constructors.
The mental model
An object is state plus the operations that belong to that state. The initialiser runs when the object is created and sets the state; methods use it. Every method receives the instance as its first parameter, conventionally named self, and that is not ceremony — it is why a method knows which object it is working on.
The distinction that trips people is instance against class attributes. An attribute assigned inside the initialiser belongs to that object. An attribute assigned in the class body belongs to the class and is shared by every instance — which is fine for a constant and a disaster for a mutable default, for exactly the reason a mutable default argument was a disaster in the functions topic: one list, shared by everything, accumulating across objects that have nothing to do with each other. The rule is the same: constants can live on the class, and anything mutable is created per instance in the initialiser.
Two decorators give you the other kinds of method. A staticmethod is a plain function that happens to live in the class because that is where it belongs conceptually; it takes neither the instance nor the class. A classmethod receives the class instead of the instance, and its most useful role is the alternative constructor — a method that builds an instance from a different starting point, such as a row of a file, and returns it. When you find yourself writing a function whose whole job is to construct one of your objects, that is a classmethod asking to exist.
Python's encapsulation is a convention rather than a rule. A single leading underscore means "internal, do not rely on this"; there is no enforcement, and none is wanted. What matters is the promise the name makes: a reader seeing an underscore knows you may change it, and a reader seeing a plain name knows you will not without warning. That is the whole system, and it works because the alternative — fighting the language to hide things — costs more than it saves.
Then the trap, and it is worth being blunt about. If there is no state to hold, a function is better than a class. A class with an initialiser that stores three arguments and one method that uses them is a function with extra steps: harder to call, harder to test, and one more concept between the reader and the work. Ask what the object remembers between calls. If the honest answer is nothing, write the function.
In code
Checked against the Python tutorial's Classes chapter.
class Experiment:
"""One training run: its settings, its results, and how it reports itself."""
max_epochs = 100 # a shared constant, deliberately
def __init__(self, name, learning_rate):
self.name = name
self.learning_rate = learning_rate
self.losses = [] # per instance: mutable state must never be shared
@classmethod
def from_row(cls, row):
return cls(name=row["name"], learning_rate=float(row["lr"]))
@staticmethod
def is_valid_rate(rate):
return 0 < rate < 1
def record(self, loss):
self.losses.append(loss)
def best(self):
return min(self.losses) if self.losses else None
def __repr__(self):
return f"Experiment({self.name!r}, lr={self.learning_rate})"Read the two comments as the lesson: max_epochs is shared on purpose because it is a constant, and losses is created in the initialiser because it is mutable. Had losses been written in the class body, every experiment would append to the same list and the bug would look like data corruption rather than a class-attribute mistake.
from_row is the alternative constructor: it takes a class rather than an instance, so it can build and return one. And __repr__ is a small courtesy with a large payoff — it is what you see when you print the object or look at a list of them while debugging, and without it you get an address that tells you nothing.
What you should now be able to explain or do
Write a class with an initialiser, an instance attribute and a method, and say what self is. Explain the difference between an instance and a class attribute, and state the rule about mutable ones. Write a classmethod that builds an instance from a row of data, and say why it takes the class. Say what a leading underscore promises and what enforces it. Look at a class you wrote and decide honestly whether a function would be clearer.
Check yourself
What is self?
The instance the method was called on, passed as the first parameter. It is how a method knows which object's state it is working with.
You put an empty list in the class body and every object seems to share it. What happened?
They do share it — an attribute in the class body belongs to the class, not to any instance. Mutable state belongs in the initialiser, created fresh per object.
What is a classmethod most useful for?
An alternative constructor. It receives the class, so it can build an instance from a different starting point — a row, a file, a dictionary — and return it.
What does a leading underscore enforce?
Nothing. It is a promise to the reader that the name is internal and may change. Python trusts the convention rather than hiding the attribute.
When should you not write a class?
When the object remembers nothing between calls. An initialiser that stores three values and one method that uses them is a function with extra steps.
Go deeper
We haven't checked most of these for screen reader use yet.
- Week 8 Object-Oriented Programming · Harvard CS50 · Videovideo, with transcript