Coming from Java, the instinct is "wrap everything in a class." Python culture pushes the other way: functions and modules first, classes when there's actual state. A class with one method, no instance attributes worth speaking of, that gets instantiated only to call its method — should be a function.
The signs you don't need a class
(1) Only one method besides __init__. (2) __init__ just stores the arguments without transformation. (3) The class is instantiated, the method called, the instance discarded. (4) The class has no state-mutating methods. All four together: write a function. Three out of four: probably a function.
Modules ARE namespaces
In Java, you might use a class with static methods just to group related functions. In Python, a module IS a namespace. Group related functions in my_module.py; callers do from my_module import f, g. No class needed. The Python stdlib's os, math, json modules are exactly this.
When classes ARE the right answer
When you have stateful objects with multiple methods that operate on shared state. When you want polymorphism (multiple implementations of the same interface). When you're modeling something that has identity over time (a connection, a session, a stateful processor). Classes are the right tool here — but each of these conditions is real, not assumed.
Principle: Start with a function. Promote to a class only when the function develops persistent state, multiple methods that share state, or polymorphic behavior. Demoting from class to function is harder than the reverse — start simple and grow only when the simpler form genuinely doesn't fit.
Code
Class that should be a function·python
# UNPYTHONIC — class with only __init__ and one method
class EmailValidator:
def __init__(self, email):
self.email = email
def is_valid(self):
return "@" in self.email and "." in self.email.split("@")[1]
result = EmailValidator("alice@example.com").is_valid()
# PYTHONIC — function
def is_valid_email(email):
return "@" in email and "." in email.split("@")[1]
result = is_valid_email("alice@example.com")
# Same logic, simpler call site, easier to test, easier to reuse
Module as namespace — no class needed·python
# UNPYTHONIC — class with only static methods to group helpers
class StringHelpers:
@staticmethod
def slugify(s):
return s.lower().replace(" ", "-")
@staticmethod
def truncate(s, n):
return s if len(s) <= n else s[:n-3] + "..."
from my_helpers import StringHelpers
result = StringHelpers.slugify("Hello World")
# PYTHONIC — module-level functions
# string_helpers.py
def slugify(s):
return s.lower().replace(" ", "-")
def truncate(s, n):
return s if len(s) <= n else s[:n-3] + "..."
from string_helpers import slugify, truncate
result = slugify("Hello World")
When classes earn their keep — actual state·python
# Stateful object with multiple methods that share state — class is right
class Counter:
def __init__(self):
self.count = 0
self.history = []
def increment(self):
self.count += 1
self.history.append(("increment", self.count))
def reset(self):
self.history.append(("reset", self.count))
self.count = 0
def report(self):
return f"count={self.count}, history={len(self.history)} ops"
# Three methods sharing two attributes that mutate over time.
# Function would be awkward — Counter is the right shape.
Closures — sometimes a lighter alternative·python
# Need state but not multiple methods? Closure works
def make_counter(start=0):
count = [start] # list to allow mutation in closure
def increment():
count[0] += 1
return count[0]
return increment
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
# Or with nonlocal
def make_counter_v2(start=0):
count = start
def increment():
nonlocal count
count += 1
return count
return increment
Take a class URLBuilder that has __init__(self, base) storing base, and a single method build(path: str) -> str that returns f"{base}/{path}". Convert it to a function. Discuss when (if ever) the class form would be the right choice. Then take this class to its other extreme: extend it with state and multiple methods until the class form becomes clearly correct.
Progress
Progress is local-only — sign in to sync across devices.