The Quiet Terminal

← Beginner

Classes in one page: a minimal, honest example

2026-08-03 · 3 min read

Classes were sold to me as "how real programs are organized", which left me feeling every script was unfinished until it had one. The lesson took years to land: a class earns its place when a few values and the functions that work on them keep traveling together. Here's the smallest example I still find honest — a single to-do item.

The class

class Task:
    def __init__(self, title):
        self.title = title
        self.done = False

    def mark_done(self):
        self.done = True

    def __str__(self):
        mark = "x" if self.done else " "
        return f"[{mark}] {self.title}"

Three methods. __init__ runs the moment a task is created, and its only job is setting up the starting state: every task gets its own title and its own done flag, switched off. mark_done flips the flag. __str__ decides what print shows.

Using it — and self, plainly

t = Task("water the plants")
print(t)            # [ ] water the plants

t.mark_done()
print(t)            # [x] water the plants

Without __str__, print(t) shows something like <Task object at 0x7fc5425b7c40> — true and useless. With it, the object prints the way you'd write it on paper.

self is the part that confused me longest, and it's simpler than it looks: self is the particular task you called the method on. t.mark_done() is Python's shorthand for Task.mark_done(t) — the name before the dot gets passed in as self. That's the whole trick. Read self as "this one", and methods stop feeling like magic.

And each instance keeps its own state:

a = Task("water the plants")
b = Task("renew library card")
b.mark_done()
print(a.done, b.done)     # False True

Marking b done didn't touch adone lives on the instance, not on the class. That, more than any syntax, is what a class buys you: a shape of data that carries its behavior around with it.

When not to bother

Now the part I wish someone had told me. Here's a bit of my old code where a class would have been pure overhead:

def eur_to_usd(amount, rate=1.08):
    return round(amount * rate, 2)

def usd_to_eur(amount, rate=1.08):
    return round(amount / rate, 2)

Nothing here remembers anything between calls, so there's nothing for a class to hold. I've read — and written — beginner code with a CurrencyConverter class that had two methods and no attributes: a suitcase with nothing in it. If your data is just data, use a dict; if your functions don't share state, keep them as functions.

A middle case I run into more than I expected: data that's just data but keeps needing the same little transform everywhere. A helper function that takes the dict and returns the fixed-up dict is fine for a while. The moment three different places need it and two of them have drifted out of sync, that's the real signal — not "it might be a class someday", but "the behavior already lives with the data, informally".

My rule, paid for in unwrapping my own over-engineered code: reach for a dict when you're only storing values, and for a class when the values and the behavior genuinely belong together — like a task that knows how to print itself and flip its own flag. Most of my "we might need a class someday" bets lost. The honest class above fits in one page, and one page is usually enough.