Python isn't Java. Functions are first-class citizens, modules behave like singletons, dataclasses give you data containers with three lines, and the PSF tutorial doesn't even introduce classes until chapter 9. So the question every Python developer eventually faces isn't "should I learn OOP?" (the answer's yes — see the absolute beginner's guide). It's "should I use OOP for THIS code?" Jack Diederich's canonical 2012 PyCon talk "Stop Writing Classes" remains the best answer 13 years later: most over-engineered class hierarchies are just functions wearing a costume. This guide turns Diederich's principles into a 4-question decision flowchart and covers function, dataclass, module, class, and full-OOP as five distinct landing places. It's one of 10 explainers in our Python OOP complete guide.
Key Takeaways
Python is multi-paradigm — the PSF tutorial introduces functions and modules first, classes only in chapter 9. Mix paradigms freely.
Diederich's rule: if your class has only two methods and one is __init__, you probably meant to write a function.
5 landing places: function (no state), dataclass (mostly data), module (singleton), class (state + behavior + multiple instances), full OOP (inheritance + ABC + polymorphism).
Four questions decide which — persistent state, complexity, multiple instances, extensibility. The flowchart picks for you.
Refactoring is the test. If you can collapse a class to a function or a dataclass without losing clarity, you should.
Four questions, five outcomes. Most code lands at function or dataclass; full OOP is the rarer endpoint.
The Diederich Rule
In "Stop Writing Classes", Jack Diederich (Python core developer) refactored a real production codebase. The starting point: a mail-handling library with 20 classes scattered across 22 modules. After his refactoring pass: 1 class with 15 lines. After his SECOND pass: 1 function with 3 lines. The functional change between the original and final version: zero. The codebase was doing the same work, more clearly, in 5% of the original code.
His central thesis stuck: "Classes must be nouns but not every noun must be a class." The followup rule that comes up constantly: if your class has only two methods and one is __init__, you probably meant to write a function.
The talk is 26 minutes. It's still the canonical reference for this question. The four-question flowchart above is the operational version of his guidance.
Outcome 1: Use a Function
If your code has NO persistent state across calls, a function does the job with the least ceremony.
# OVER-ENGINEERED: a class with no state
class TemperatureConverter:
def __init__(self):
pass
def celsius_to_fahrenheit(self, c):
return c * 9 / 5 + 32
converter = TemperatureConverter()
result = converter.celsius_to_fahrenheit(100)
# IDIOMATIC: just a function
def celsius_to_fahrenheit(c):
return c * 9 / 5 + 32
result = celsius_to_fahrenheit(100)
The class version is a function wearing a costume. __init__ does nothing; self is unused. Strip the costume.
Common signs you wrote a function that wants to be a function:
Your __init__ is empty or just sets one attribute
You have one "real" method besides __init__
You only ever instantiate the class to immediately call the method
You never use self.something across methods
Outcome 2: Use a Dataclass (or NamedTuple)
If your class is mostly DATA with light behavior, dataclass (Python 3.7+, PEP 557) generates __init__, __repr__, and __eq__ for you. Twenty lines of boilerplate become one decorator.
# OVER-ENGINEERED: 20 lines of boilerplate
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
# IDIOMATIC: dataclass
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
frozen=True also gives you immutability and a working __hash__. The full @dataclass deep dive covers all the options.
When dataclass fits: 3-6 fields, mostly data access, maybe one or two computed properties. When NamedTuple fits better: immutable, 2-4 fields, want tuple unpacking semantics. When a regular class fits better: rich behavior, complex methods, inheritance trees.
Outcome 3: Use a Module
A Python module IS a singleton. If you'll only ever create ONE instance, module-level functions and variables do the job with zero ceremony.
# OVER-ENGINEERED: a class instantiated once
class Logger:
def __init__(self):
self.history = []
def log(self, msg):
self.history.append(msg)
print(msg)
logger = Logger()
logger.log("Starting up")
# IDIOMATIC: module-level state
# In logger.py:
_history = []
def log(msg):
_history.append(msg)
print(msg)
# In other files:
from logger import log
log("Starting up")
The underscore prefix on _history signals "internal." Module-level variables persist across calls just like instance attributes do, and they're only created once.
Reach for a class only when you need MULTIPLE instances with separate state. One Logger for the whole app? Module. Multiple UserSession objects with separate per-user state? Class.
Outcome 4: Use a Class
The sweet spot for regular OOP: state and behavior bundle together, AND you need multiple instances, AND the structure is stable (no inheritance hierarchy needed).
class GameSession:
def __init__(self, player_id):
self.player_id = player_id
self.score = 0
self.lives = 3
self.level = 1
def gain_points(self, n):
self.score += n
if self.score >= self.level * 1000:
self.level_up()
def level_up(self):
self.level += 1
self.lives += 1
def lose_life(self):
self.lives -= 1
return self.lives > 0 # True if still alive
# Create distinct sessions per player
alice_session = GameSession("alice")
bob_session = GameSession("bob")
alice_session.gain_points(500) # Alice's score only
State (score, lives, level) and behavior (gain_points, level_up, lose_life) are tightly coupled. Each player needs distinct state. The class fits.
Outcome 5: Use Full OOP (Inheritance, ABC, Polymorphism)
The deepest endpoint — needed when you're building something extensible: a framework, a plugin system, or a library where users will subclass your code.
The abstract base class defines the contract; concrete classes implement it; the checkout function works with any processor via polymorphism. See the dedicated guides on ABC and Protocols and super().__init__() and MRO.
The Refactoring Test
The Diederich talk's most useful debugging technique: when you have a class, ASK if it can collapse. Concretely:
Could this be a function? Try writing it as one. If the function version is shorter and clearer, the class was overkill.
Could this be a dataclass? If your class is 80% data access, swap to @dataclass and see.
Could this be a module? If you only ever create one instance, the class is a glorified namespace.
Could this lose a few methods? Often half your methods could be functions taking the data as an argument.
If the answer is yes to any of these, refactor. Code that reads better with fewer abstractions usually IS better.
Decision Examples
Worked decisions for common Python tasks:
Task
Right tool
Reason
Calculate tax on a price
Function
No persistent state
Represent a 3D point (x, y, z)
Dataclass (frozen)
Pure data + equality
App-wide configuration
Module
Singleton, one instance
Database connection (shared)
Module + get_connection()
Singleton, lazy init
Per-user shopping cart
Class
State + behavior, multiple instances
Form validators (plugin system)
ABC + concrete classes
Extensible by users
HTTP request handler
Function (in Flask/FastAPI)
Stateless, framework handles routing
State machine (open/closed/paid)
Class with enum
State transitions matter
Iterator that yields lazily
Generator function
Stateful but cleaner with yield
API client library you publish
Class (extensible)
Users will subclass to customize
The Anti-Pattern Catalog
Six patterns that signal "you wanted a function":
1. The Empty __init__
class Calculator:
def __init__(self):
pass
def add(self, a, b):
return a + b
If __init__ does nothing, the class is doing nothing. Write a function.
2. The One-Method Class
class CsvLoader:
def __init__(self, path):
self.path = path
def load(self):
with open(self.path) as f:
return f.readlines()
# Usage:
data = CsvLoader("file.csv").load()
The class exists only to immediately call .load(). load_csv("file.csv") is shorter and clearer.
This is just a module. Put the functions at module level in string_utils.py and import them.
5. The Class as Namespace
class Constants:
PI = 3.14159
E = 2.71828
GOLDEN = 1.61803
Modules already are namespaces. Put these at module level in constants.py.
6. Inheritance Without Polymorphism
class AnimalBase:
def __init__(self, name):
self.name = name
class Dog(AnimalBase):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
class Cat(AnimalBase):
def __init__(self, name, color):
super().__init__(name)
self.color = color
# But you never use polymorphism. You always know it's a Dog or a Cat.
If you never write a function that takes AnimalBase and works with any subclass, the inheritance hierarchy was decorative. Use @dataclass for both. See composition vs inheritance.
What Python Specifically Makes Easier
Python's features that bias the decision FROM "always use OOP" toward "use the simplest tool":
First-class functions. You can pass functions as arguments, return them, store them in lists. Many languages need classes for this; Python doesn't.
Module-level state. Modules give you singleton-like behavior with less ceremony than a Singleton pattern.
Comprehensions. List, dict, set comprehensions handle most data transformations without iterator classes.
Generators.yield creates stateful iterators without writing iterator classes.
Decorators. Modify function behavior without inheritance trees.
Dataclasses + Protocols. Get data containers and duck typing without the full Java-style ceremony.
None of this means "never use classes." It means: classes are one tool among many in Python, not the default tool.
Frequently Asked Questions
When should I use OOP in Python?
Use OOP when state and behavior naturally bundle together AND you'll create multiple instances of the same shape. Specific situations: entities with both data and methods (a Player class with health, position, and move methods), multiple variants sharing structure (User, AdminUser, GuestUser), extensible frameworks (a plugin system), and long-lived stateful objects (a database connection pool). Skip OOP when there's no persistent state (use a function), when it's just a data container (use dataclass or NamedTuple), or when only one instance ever exists (use a module). Jack Diederich's canonical 'Stop Writing Classes' PyCon 2012 talk gives the rule of thumb: if your class has only two methods and one is __init__, you probably meant to write a function.
What is Jack Diederich's 'Stop Writing Classes' rule?
Jack Diederich's PyCon 2012 talk argues that Python developers over-use classes. His central rule: "Classes must be nouns but not every noun must be a class." If your class has only two methods and one is __init__, you probably meant to write a function. The talk includes a real refactoring of MuffinMail's API code that went from 20 classes scattered across 22 modules down to 1 class in 15 lines, then to 1 function in 3 lines. The pattern: most over-engineered class hierarchies are just functions wearing a costume.
When should I use a dataclass instead of a regular class?
Use dataclass (PEP 557, available since Python 3.7) when your class is mostly data with little behavior — typically 3-6 fields with maybe one or two computed properties. The dataclass decorator generates __init__, __repr__, and __eq__ for you, eliminating ~20 lines of boilerplate. The threshold question: "Could I just use a namedtuple, but I need mutability or a few methods?" That's a dataclass. If your class has rich behavior beyond data access — multiple complex methods, inheritance trees, polymorphism — use a regular class. Dataclass is OOP-lite, perfect for data records that need to be objects but don't need the full ceremony.
What's the difference between a module and a class with one instance?
A module IS a singleton in Python. If you're creating a class that you'll only ever instantiate once (a configuration manager, a logging system, an API client), a module with module-level functions and module-level variables does the same job with less code. The "Stop Writing Classes" talk uses this example: a Logger class that was used as Logger().log(msg) everywhere became log(msg) at module level, removing all the ceremony. Modules are lazy-initialized on first import, support cross-call state via module-level variables, and don't need a self argument on every function. Reach for a class when you genuinely need multiple instances with separate state.
Is Python a purely OOP language?
No. Python is multi-paradigm. Functions are first-class objects (you can pass them around, return them, store them in lists), comprehensions are functional in spirit, and the standard library is full of module-level functions. Unlike Java where every line of code must live inside a class, Python lets you mix paradigms freely. The PSF tutorial doesn't even introduce classes until chapter 9 — the first 8 chapters cover data types, control flow, functions, and modules. Pythonic code uses OOP where it fits and skips it where it doesn't, often within the same file.
The Bottom Line: Pick the Simplest Tool That Works
Python gives you five landing places: function, dataclass, module, class, full OOP. Most code belongs in the upper-left of that range. Diederich's 2012 PyCon talk made the case that Python developers default to the lower-right and over-engineer. The four-question flowchart keeps you honest. The refactoring test — "could this be a function?" — is your reality check. Pick the simplest tool that works; collapse back to it whenever you can. The next entry in this series covers staticmethod vs classmethod vs instance method for when you've committed to a class and need to choose method types. Or browse the full Python OOP guide.
Build OOP Judgment Through Real Practice
CodeGym's Python track teaches you to make the function-vs-class call through 800+ hands-on tasks structured to surface the trade-off. Each task is scoped so you experience why simpler abstractions usually win, before climbing to inheritance, ABC, and metaclasses. AI validators grade your solutions; AI mentors explain when you over-engineered. First level free; full plan on the pricing page.
GO TO FULL VERSION