Stop writing classes (unless you really need them)
I still remember the day my code review made our senior developer spill coffee all over his mechanical keyboard. My crime? Adding a shiny new EmailValidator class with exactly two methods: __init__ and validate_email.
"A class? For THIS?" he asked, wiping coffee from his beard. "You just wrote 15 lines to do what a 3-line function could handle."
He was right. And I've spent years since unlearning my reflex to wrap everything in classes.
If you're nodding along because you've created a StringFormatter class just to format strings, this article is for you. Let's talk about Python's dirty little secret: you probably don't need most of those classes you're writing.
When you reach for a class but shouldn't
Picture this: You need to validate user input. Your object-oriented instincts kick in:
class InputValidator:
def __init__(self, min_length=3, max_length=50):
self.min_length = min_length
self.max_length = max_length
def validate(self, text):
return self.min_length <= len(text) <= self.max_length
# Using it:
validator = InputValidator(min_length=5)
is_valid = validator.validate("hello world")
Seems reasonable, right? But look at all that ceremony for something that could be:
def validate_input(text, min_length=3, max_length=50):
return min_length <= len(text) <= max_length
# Using it:
is_valid = validate_input("hello world", min_length=5)
The function is simpler, more direct, and doesn't force you to create an object just to perform a single operation.
Here's another classic example - the infamous "bag of functions" class:
class StringUtils:
@staticmethod
def reverse(s):
return s[::-1]
@staticmethod
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
This isn't object-oriented programming. It's just functions wearing a trench coat pretending to be a class. Drop the disguise:
def reverse(s):
return s[::-1]
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
When your class methods don't use self, that's Python whispering, "This should just be a function."
And if you're reaching for a class just to group related functions, that's what modules are for!
Functions: Python's secret weapon
Most Python newcomers don't realize just how powerful functions really are in this language. They're not just procedures or methods - they're full-fledged objects with superpowers.
Here's something that blows people's minds: in Python's C implementation, functions ARE classes. That's right - when you define a function in Python, you're actually creating an instance of a class (specifically, a PyFunction_Object in the C code).
This means Python functions can do things that make other languages jealous:
def greet(name):
"""Say hello to someone"""
return f"Hello, {name}!"
# Functions have attributes
greet.description = "A friendly greeting function"
greet.default_name = "friend"
# Functions can be passed around like any other object
def run_twice(func, arg):
return func(arg), func(arg)
# Functions can be stored in data structures
greeting_funcs = [
greet,
lambda name: f"Hey there, {name}!",
lambda name: f"Howdy, {name}!"
]
# Functions can be returned from other functions
def make_greeter(prefix):
def custom_greeter(name):
return f"{prefix}, {name}!"
return custom_greeter
spanish_greeter = make_greeter("Hola")
print(spanish_greeter("Carlos")) # "Hola, Carlos!"
That last example shows one of Python's most powerful features: closures. The spanish_greeter function "remembers" the value of prefix from when it was created.
This often eliminates the need for a class. When you think you need a class to "remember" some initial configuration, a closure might be cleaner:
# Instead of this class:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def multiply(self, x):
return x * self.factor
# Use this function factory:
def create_multiplier(factor):
def multiply(x):
return x * factor
return multiply
# Usage:
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
And let's not forget decorators - Python's elegant way of modifying or enhancing functions:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5) # Automatically logs the call and return value
With all these capabilities, functions can handle most of what you'd use simple classes for, with less code and cognitive overhead.
The hidden costs of unnecessary classes
When you create classes needlessly, you're not just writing extra code - you're creating future problems. Let's look at some real costs:
More mental overhead
Every class creates a mini-universe with its own rules. Each new class asks your brain to track:
Compare the mental load of tracking a class like this:
class DataProcessor:
def __init__(self, source_path, output_path):
self.source_path = source_path
self.output_path = output_path
self.data = None
self.processed = False
def load(self):
with open(self.source_path, 'r') as f:
self.data = f.readlines()
def process(self):
if not self.data:
self.load()
self.data = [line.strip().lower() for line in self.data]
self.processed = True
def save(self):
if not self.processed:
self.process()
with open(self.output_path, 'w') as f:
f.writelines(self.data)
Versus this functional approach:
def load_data(source_path):
with open(source_path, 'r') as f:
return f.readlines()
def process_data(data):
return [line.strip().lower() for line in data]
def save_data(data, output_path):
with open(output_path, 'w') as f:
f.writelines(data)
The functional version is easier to follow because each function has a clear input and output with no hidden state.
Testing becomes painful
Classes with internal state are harder to test. With the DataProcessor class, testing the process method properly means setting up the right state first.
But with pure functions, testing is straightforward:
def test_process_data():
input_data = ["Line 1 ", " LINE 2", "line 3"]
expected = ["line 1", "line 2", "line 3"]
assert process_data(input_data) == expected
No setup, no state, no surprises.
Flexibility suffers
When logic is locked in a class, it's harder to compose and reuse. What if you want to process data from a different source, not a file?
With the class approach, you'd need to modify the class or create a new one. With functions, just add another function:
def load_data_from_api(url):
response = requests.get(url)
return response.text.splitlines()
# Then use with the existing process_data function
data = load_data_from_api("https://example.com/data")
processed = process_data(data)
Classes tempt us to keep adding more methods and more state, leading to bloated "god objects" that do too much. Functions push us toward simplicity and composition.
When classes actually make sense
Look, I'm not a class-hating anarchist. Classes shine in specific situations:
When you need to maintain complex state
If you're tracking multiple related attributes that change together through well-defined operations, classes make sense:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
self.balance += amount
self.transactions.append(("deposit", amount))
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
self.transactions.append(("withdraw", amount))
def transfer(self, amount, target_account):
self.withdraw(amount)
target_account.deposit(amount)
self.transactions.append(("transfer", amount, target_account.owner))
This works as a class because:
Trying to replace this with functions would be messy and error-prone.
When implementing interfaces or protocols
When you're working with frameworks or libraries expecting specific object interfaces, classes are appropriate:
class CustomWidget(tk.Frame):
def __init__(self, master):
super().__init__(master)
# Set up widget...
def update_display(self):
# Update the widget
Many Python frameworks (Django, Flask, Tkinter, etc.) rely on class inheritance to hook into their systems.
When leveraging dunder methods
Python's special methods let you create objects that work seamlessly with language features:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# Now you can use natural syntax
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__
This kind of operator overloading and custom behavior is a legitimate reason to use classes.
Building better code with both approaches
The real skill isn't picking functions or classes - it's knowing when to use each. This is where principles like SOLID come in handy.
SOLID principles (originally defined for OOP) can guide your thinking even when writing functional code, For a deep dive into these principles, check out Building Bulletproof Code: SOLID Principles to Level Up Your Skills.
The truth is that good Python code often mixes both approaches:
# Functions for core operations
def load_config(path):
with open(path) as f:
return json.load(f)
def validate_config(config):
required = ["api_key", "endpoint", "timeout"]
return all(key in config for key in required)
# A class where it makes sense
class APIClient:
def __init__(self, config):
if not validate_config(config):
raise ValueError("Invalid configuration")
self.api_key = config["api_key"]
self.endpoint = config["endpoint"]
self.timeout = config["timeout"]
self.session = requests.Session()
def get_data(self, resource_id):
response = self.session.get(
f"{self.endpoint}/{resource_id}",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=self.timeout
)
return response.json()
Here, the functions handle simple transformations while the class manages complex state (the HTTP session and configuration).
Python's own standard library follows this pattern. Look at the json module - it offers functions like dumps() and loads() for simple operations, but also provides the JSONEncoder class for more complex customization.
So the next time you start typing class Whatever:, pause and ask yourself:
Classes aren't evil. They're just overused. Use them when they truly pull their weight - not just because you can.
Remember: in Python, simplicity is a feature, not a bug!
Full Stack Developer | Intern at High On Swift | Coimbatore Institute of Technology | Decision and Computing Science Student.
2moLove this, Agni