Classes and objects
what they are and when they are really needed
What it is
A class is a description of how an object is built: what data it holds and what it can do. An object is a concrete instance made from that description.
To read code it is enough to understand four things: class, __init__,
self and the dot. Everything else is built on top of those.
What it looks like in code
class Cart:
def __init__(self, user_id: int) -> None:
self.user_id = user_id
self.items: list[dict] = []
def add(self, name: str, price: float) -> None:
self.items.append({"name": name, "price": price})
def total(self) -> float:
return sum(item["price"] for item in self.items)
cart = Cart(user_id=7)
cart.add("coffee", 100.0)
cart.add("bread", 45.0)
print(cart.total()) # 145.0
print(cart.user_id) # 7The four things to know
class Cart: is a description. Like def, on its own it runs nothing.
__init__ runs at the moment the object is created. The line cart = Cart(user_id=7) calls exactly that. This is where the starting data is set.
self is the object itself. It is the first argument of every method, but
you don't pass it when calling: cart.add("coffee", 100.0) has two arguments
although the definition has three. Python supplies self for you.
The dot accesses the object's data or methods: cart.items,
cart.total(). With parentheses it is a method call; without, it is reading
data.
What makes a class different from a pile of functions
A class keeps state between calls. cart.items lives in the object: add an
item and it stays there. With plain functions you would have to pass that list
back and forth by hand.
Hence the rule for reading: find where the state changes. Everything
starting with self. and standing to the left of an = is a change to the
object. These are the side effects of article 2.5, just inside a class.
Dataclass — a class for data only
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
qty: int = 1
item = Item("coffee", 100.0)
print(item) # Item(name='coffee', price=100.0, qty=1)@dataclass writes __init__ and a readable printout for you. It appears
often, and that is a good sign: a class with no methods, only data, reads
easily.
Inheritance
class PremiumCart(Cart):
def total(self) -> float:
return super().total() * 0.9PremiumCart(Cart) means "the same as Cart, but with changes".
super().total() calls the parent's version.
For reading, inheritance is awkward because the code spreads across two
places: to understand what PremiumCart does you have to open Cart too.
Which is why more than two levels deep is almost always evil.
What to tell the AI
What does this object keep between calls, and which methods
change that state?Is this class justified, or would two functions be simpler?
Answer straight.When a class is inherited:
Which methods does PremiumCart override and which does it
take from the parent? A list.What to look for
A class with no state. If __init__ saves nothing and every method could
be a plain function, the class is superfluous. The AI creates these readily —
they look "proper".
self. assigned outside __init__. A field that appears in the middle of
another method is easy to miss while reading: the object suddenly acquires
data it did not have before.
Deep inheritance. Three levels or more and understanding one line means opening four files.
A mutable value in a class field. Same as with default arguments (article
2.5): a list declared at class level rather than in __init__ is shared by
every object.
Cheat sheet
Four things:
class·__init__·self· the dot.__init__runs when the object is created.You don't pass
selfwhen calling; Python supplies it.A class earns its place through state between calls; without it, functions are enough.
Look for
self.x = ...— those are changes to the object.Inheritance deeper than two levels almost always hurts reading.