Software Design Reference System Designer · Flexible & Composable Code

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.
Field notes · what it looks like in real code
You see thisWhat it costsThe move
__init__ that only stores callablesA function factory wearing a class costume.A closure, or functools.partial.
a method that never reads selfIt is a function that happens to live in a class.Move it out; @staticmethod is the smell, not the fix.
a growing mode="…" parameterPolymorphism by string, without the interface.A protocol plus one implementation per mode.
20 free functions sharing one module dictState 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

Bring a borderline class to your agent-teacher and run the habitat test together.