RizTech Academy logo
RizTech Academy
Object-Oriented PythonLesson 7 of 715 min

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:

  1. Converting Celsius to Fahrenheit
  2. A shopping cart that items are added to and removed from
  3. The settings loaded from a config file
  4. Formatting a number as Indian rupees
  5. A database connection that must be opened and closed
  6. A collection of string-cleaning helpers
  7. A single running total in one script
  8. A user record loaded from an API
  9. A game of noughts and crosses
  10. Checking whether a password is strong enough

Then:

  1. Find a class in your own code with one method and no state. Rewrite it as a function and compare the call sites.
  2. Write the ExpenseProcessor class version and call report() before load(). Confirm it silently returns nothing useful.
  3. 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