Programming with AI
ENRU
2.7

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)      # 7

The 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.9

PremiumCart(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

Prompt
What does this object keep between calls, and which methods
change that state?
Prompt
Is this class justified, or would two functions be simpler?
Answer straight.

When a class is inherited:

Prompt
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 self when 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.