When a function is the better answer
Having spent six lessons on classes, this one argues against them. That is not a contradiction — knowing when a tool does not apply is part of knowing the tool, and over-using classes is a far more common problem in practice than under-using them.
Python does not require classes. Plenty of excellent Python is modules of functions. Much of the standard library is.
The class that should be a function
class EmailValidator:
def __init__(self, email: str):
self.email = email
def validate(self) -> bool:
return "@" in self.email and "." in self.email.split("@")[-1]
if EmailValidator(email).validate():
...
Versus:
def is_valid_email(email: str) -> bool:
return "@" in email and "." in email.split("@")[-1]
if is_valid_email(email):
...
The class adds a construction step, an object that exists for one line, and a worse name at the call site. It carries no state between calls and has one method.
A class with one method and no state is a function with extra steps. Names
ending in -er or -Manager — Validator, Processor, Handler,
Calculator — are worth a second look, because they often describe a verb
pretending to be a noun.
The class that should be a dictionary
class Config:
def __init__(self):
self.host = "localhost"
self.port = 8000
self.debug = False
If nothing validates, computes or enforces anything, that is:
config = {"host": "localhost", "port": 8000, "debug": False}
The dictionary is easier to serialise, merge, and load from a file. The class
buys you config.host over config["host"] and nothing else.
Note the middle ground: if you want attribute access, a frozen dataclass gives it for three lines and adds immutability. The argument here is against a handwritten class, not against structure.
The class that should be a module
class MathUtils:
@staticmethod
def add(a, b): return a + b
@staticmethod
def subtract(a, b): return a - b
Every method static, no state. This is Java habits arriving in Python, where they are not needed: the module is already the namespace.
# math_utils.py
def add(a, b): return a + b
def subtract(a, b): return a - b
import math_utils
math_utils.add(2, 3)
Same grouping, one less layer. If every method is a @staticmethod, you wrote
a module.
The class that exists to hold one function's state
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
Fine if several counters are needed independently. If there is exactly one, and it is used in one place, a local variable in a function is simpler and has a smaller surface.
The question to ask: do I need more than one of these, with separate state? No is a strong hint you do not need a class.
Getters and setters for their own sake
class User:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, value):
self._name = value
Six lines to do what self.name does in one. This is correct in languages where
adding a property later breaks callers. In Python it does not — user.name is
written the same whether name is a plain attribute or a property.
Start with a plain attribute. Add a property when you actually need behaviour. You lose nothing by waiting, which is the point the methods lesson made and the reason it is worth repeating.
So when is a class right
The signals from the first lesson, restated as questions:
Is there state that several operations share and change? A bank account, a game, an open connection. Yes is a strong argument for a class.
Are there rules that must always hold? A balance that cannot go negative, a category from a fixed set. A class is how you make a wrong state impossible.
Do you need several independent instances? Many accounts, many baskets.
Do you keep passing the same group of values together? Three functions whose first parameter is always the same dictionary.
Does a framework require it? Django models, custom exceptions.
If none of those apply, functions and dictionaries are likely the better answer.
A worked comparison
Processing a CSV of expenses. The class version:
class ExpenseProcessor:
def __init__(self, path: str):
self.path = path
self.rows = []
self.total = 0
def load(self): ...
def clean(self): ...
def calculate(self): ...
def report(self): ...
processor = ExpenseProcessor("data.csv")
processor.load()
processor.clean()
processor.calculate()
print(processor.report())
That looks organised and has a real problem: the methods must be called in the
right order, and nothing enforces it. Call report() first and you get zero,
silently. The object spends its life in a series of half-finished states, and
each method secretly depends on which others have run.
The function version:
def load(path: str) -> list[dict]: ...
def clean(rows: list[dict]) -> list[Expense]: ...
def total(expenses: list[Expense]) -> float: ...
def report(expenses: list[Expense]) -> str: ...
expenses = clean(load("data.csv"))
print(report(expenses))
print(f"Total: {total(expenses)}")
Each function takes what it needs and returns something new. The order is visible in the code rather than implied. Every function can be tested on its own with a literal list — no object construction, no setup — which is exactly what module 10 wants.
Expense is still a class here. The data is a class; the process is
functions. That division is usually the right one.
The honest summary
Use classes for things: an expense, an account, a connection, a game.
Use functions for actions: validating, formatting, calculating, converting.
Use dictionaries for data without rules, especially data from outside your program.
Use modules to group related functions.
When unsure, start with a function. Promoting a function to a class later is straightforward; untangling an unnecessary class hierarchy is not.
Check your work
The ten.
| Answer | |
|---|---|
| 1 Celsius to Fahrenheit | function |
| 2 Shopping cart | class — state plus operations |
| 3 Settings from a file | dictionary |
| 4 Formatting rupees | function |
| 5 Database connection | class — a lifecycle to manage |
| 6 String helpers | module of functions |
| 7 A single running total | a local variable |
| 8 User from an API | dictionary |
| 9 Noughts and crosses | class — state and rules together |
| 10 Password strength | function |
The ExpenseProcessor. Calling report() before load() returns nothing
useful, silently. The methods must be called in the right order and nothing
enforces it — the object spends its life in half-finished states, and each
method secretly depends on which others have run.
The function version makes that mistake impossible, because each function takes what it needs and returns something new. The order is visible in the code rather than implied.
Expense is still a class in that version. The data is a class; the
process is functions. That division is usually the right one.
The honest summary. Classes for things. Functions for actions. Dictionaries for data without rules. Modules to group related functions.
When unsure, start with a function. Promoting one to a class later is straightforward; untangling an unnecessary class hierarchy is not.
Practice
For each, decide function, dictionary, module or class, and say why:
- Converting Celsius to Fahrenheit
- A shopping cart that items are added to and removed from
- The settings loaded from a config file
- Formatting a number as Indian rupees
- A database connection that must be opened and closed
- A collection of string-cleaning helpers
- A single running total in one script
- A user record loaded from an API
- A game of noughts and crosses
- Checking whether a password is strong enough
Then:
- Find a class in your own code with one method and no state. Rewrite it as a function and compare the call sites.
- Write the
ExpenseProcessorclass version and callreport()beforeload(). Confirm it silently returns nothing useful. - Rewrite it as functions and confirm the same mistake is now impossible.
That is module nine. You can write classes, know what the syntax does, and — more usefully — know when not to reach for one.
Next module: testing and code quality, where the functions you extracted in module 5 finally get checked automatically.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship