Lesson 0015 · System Designer · Module 1
Classes vs Functions
Not everything needs a class. The decision is not style — it's about what has to change: stateless behavior belongs in functions; state and polymorphism belong in classes.
Mission tie-in: "decide when to use classes and when simple functions are enough" is the first System Designer skill — the one AI gets wrong in both directions.
Knowledge: the two habitats
Functions are the natural home for behavior without state: a transformation from input to output, no history, no configuration of its own. Pure functions — same input, same output, no side effects — are the most composable, testable units software has.
def sanitize(word: str) -> str:
return word.strip(".,!?").lower()
def words(text: str) -> list[str]:
return [sanitize(w) for w in text.split()]
def word_frequencies(words: list[str]) -> Counter[str]:
return Counter(words)
# three stateless steps compose into a pipeline with no ceremony:
freqs = word_frequencies(words(raw_text))
Classes earn their keep when there is state to own (something that must stay consistent across calls) or polymorphism (several behaviors behind one interface — lesson 0004's strategy). A class with no state and one method is usually a function wearing a costume:
class TextStats: # stateless costume
def compute(self, text: str) -> Counter[str]:
return Counter(text.lower().split())
# the function version is the same thing with less ceremony:
def compute_text_stats(text: str) -> Counter[str]:
return Counter(text.lower().split())
The test is a change question, not a religion: what will vary here? A new behavior variant → a protocol + implementations (classes earn it). New arguments to a transformation → a function with parameters (functions earn it). Lifetime and consistency across calls → state, and a class owns it.
† Arjan's framing: "start with the data" — name the data flow first, and the class/function decision often answers itself. Classes are also a fine way to group functions with shared configuration (a "callable bundle") — that's still composition, not ceremony.| You see this | What it costs | The move |
|---|---|---|
| __init__ that only stores callables | A function factory wearing a class costume. | A closure, or functools.partial. |
| a method that never reads self | It is a function that happens to live in a class. | Move it out; @staticmethod is the smell, not the fix. |
| a growing mode="…" parameter | Polymorphism by string, without the interface. | A protocol plus one implementation per mode. |
| 20 free functions sharing one module dict | State with no owner (lesson 0019’s phone line). | One class owning the dict, methods around it. |
Skill: name the habitat
A transformation with no state and one method is best as:
Three payment behaviors behind one interface justify:
A counter that must stay consistent across calls needs:
Practice on your own code
Find a class with no state (no self. assignments in __init__ that vary) and convert it to a function. Then find one place where a function keeps gaining conditional branches and sketch the protocol that would house them.
Reveal: a worked conversion
# before
class Slugifier:
def make(self, title: str) -> str:
return "-".join(title.lower().split())
# after — same behavior, no costume
def make_slug(title: str) -> str:
return "-".join(title.lower().split())
Your win
You can classify any unit by its habitat: stateless transformations → functions; owned state and polymorphism → classes. The costume cases get undressed.
Read and watch deeper
- Python docs — Functional Programming HOWTO: pure functions and composition, the function habitat in depth.
- A Philosophy of Software Design, Ousterhout — ch. 4 "Modules Should Be Deep": a well-named function is a module; a class is one module shape among several.
- Fluent Python, Ramalho — ch. 7 (functions as objects): what Python functions can do that classes can't, and vice versa.
- Watch: ArjanCodes YouTube — search "the mighty function".
- Reference: Glossary — deep module, higher-order function.
Bring a borderline class to your agent-teacher and run the habitat test together.