Why Python's `__init__` Is Overloaded
Many Python developers overstuff `__init__` with configuration, validation, and side effects. This article argues for a cleaner constructor that only sets instance variables, improving testability, subclassing, and code clarity.
You've probably seen it a hundred times—someone writes a Python class and stuffs everything into __init__. Configuration, validation, logging, even calling other methods. It's the Swiss Army knife that somehow ended up in a toolbox full of hammers.
Let's be honest: __init__ wasn't designed to do all that. It's supposed to initialize a new object, not run your entire application startup sequence.
The Problem With Overloaded __init__
When you shove too much into __init__, a few things happen that nobody talks about in tutorials:
- Testing becomes painful. Every time you create an instance in a test, you trigger all that initialization logic. Want to test a simple method? Hope you've mocked half your class first.
- Subclassing gets ugly. If you inherit from a class with a bloated
__init__, you either reuse that mess or override it completely. Neither option feels great. - It breaks the single responsibility principle. Your object's constructor shouldn't know how to validate a database connection or send an email. That belongs elsewhere.
A Real-World Example
At PythonSkillset, we once had a ReportGenerator class whose __init__ did six things: loaded config, connected to a database, fetched data, validated it, created a logger, and called generate(). Every time we wanted a report, we got all that baggage.
The fix? We moved the database connection to a separate method. The data validation became a standalone function. The logger was set up once at the application level. __init__ now just stores the report name and a reference to a config object.
Here's what that looked like:
# Before — everything in __init__
class ReportGenerator:
def __init__(self, report_name):
self.config = load_config()
self.db = connect_to_database(self.config)
self.data = self.db.fetch_data(report_name)
self.validate_data(self.data)
self.logger = setup_logger()
self.generate()
# After — pure constructor
class ReportGenerator:
def __init__(self, report_name, config, db):
self.report_name = report_name
self.config = config
self.db = db
The second version is easier to test, easier to reuse, and doesn't scream "side effects" every time you instatiate it.
What Should __init__ Actually Do?
Just enough to make the object usable. Set instance variables. Maybe validate a few simple parameters. That's it.
- If you need to connect to a service, do that in a separate method or pass it in.
- If you need to validate data, do that before creating the object.
- If you need to run logic, call a method explicitly—don't hide it in the constructor.
A Better Pattern
Think of __init__ as the front door of your house. It should let you in, maybe take your coat, but not cook you dinner and do your taxes.
Use factory functions or class methods for complex initialization:
class DatabaseConnection:
def __init__(self, host, port, credentials):
self.host = host
self.port = port
self.credentials = credentials
@classmethod
def from_config(cls, config_path):
config = load_config(config_path)
return cls(config.host, config.port, config.credentials)
Now __init__ stays clean, and you have flexibility in how objects are created.
The Bottom Line
__init__ isn't evil. It's just a constructor. Treat it like one, and your code will thank you. Less magic, more clarity. Your future self—and anyone else maintaining your code—will appreciate it.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.