The Quest Begins (The "Why")
Honestly, I still remember the first time I looked at a Python class that was doing everything. It was a User model that validated emails, hashed passwords, talked to the database, sent welcome emails, and even logged audit trails. I thought I was being clever—everything a user needed in one place.
Fast forward a few weeks, and a tiny tweak to the password‑hashing algorithm broke the email‑sending logic. Why? Because the class was a tangled mess of responsibilities. Changing one thing meant I had to relearn half the file just to feel safe pushing a commit. I felt like Neo dodging bullets, except the bullets were my own bugs, and I kept getting hit.
That moment sparked my quest: find a principle that lets me change one piece of code without rewriting the whole saga. Enter SOLID. Today I’m going to share the single best practice that transformed how I write Python: the Single Responsibility Principle (SRP).
The Revelation (The Insight)
The SRP is dead simple in theory: a class should have one, and only one, reason to change. If you can list more than one responsibility for a class, it’s probably doing too much.
Why does this matter? Because software is a living thing. Requirements shift, bugs appear, and new features pop up like side quests in an RPG. When a class wears many hats, any change forces you to retest every hat it wears. SRP gives you clear boundaries, making your code easier to understand, test, and refactor. It’s like giving each character in your story a distinct role—no one’s trying to be the hero, the villain, and the comic relief all at once.
When I first applied SRP, the relief was instant. My test suite ran faster, my pull requests got smaller, and I stopped feeling like I was debugging a spaghetti monster every time I touched a file.
Wielding the Power (Code & Examples)
Let’s see the before and after. Imagine we’re building a simple user management system.
The “Before” – A Class With Too Many Jobs
class User:
def __init__(self, email: str, password: str):
self.email = email
self.password = password
self.hashed_pw = None
# Validation – responsibility #1
def validate_email(self) -> bool:
return "@" in self.email and "." in self.email
# Password hashing – responsibility #2
def hash_password(self, salt: str = "static_salt"):
import hashlib
self.hashed_pw = hashlib.sha256(
(self.password + salt).encode()
).hexdigest()
# Persistence – responsibility #3
def save(self):
# Pretend this talks to a DB
print(f"Saving user {self.email} to the database")
# Notification – responsibility #4
def send_welcome_email(self):
print(f"Sending welcome email to {self.email}")
# Audit logging – responsibility #5
def log_audit(self, action: str):
print(f"Audit: {action} performed on {self.email}")
Look at that! One class juggling validation, hashing, saving, emailing, and auditing. If I want to change how passwords are hashed, I have to open this file, risk breaking the email logic, and remember to run all the tests. It’s a maintenance nightmare.
The “After” – Splitting Responsibilities
Now we give each concern its own class, each with a single reason to change.
# 1. Pure data holder – no behavior
class User:
def __init__(self, email: str, password: str):
self.email = email
self.password = password
self.hashed_pw = None
# 2. Validation – only validates
class UserValidator:
@staticmethod
def validate_email(email: str) -> bool:
return "@" in email and "." in email
# 3. Security – only handles password hashing
class PasswordHasher:
def __init__(self, salt: str = "static_salt"):
self.salt = salt
def hash(self, plain_text: str) -> str:
import hashlib
return hashlib.sha256(
(plain_text + self.salt).encode()
).hexdigest()
# 4. Persistence – only talks to the DB
class UserRepository:
def save(self, user: User):
# In real life: INSERT into users table
print(f"Saving user {user.email} to the database")
def find_by_email(self, email: str) -> User | None:
# Pretend we fetch a row
print(f"Fetching user {email}")
return User(email, "dummy") # simplified
# 5. Communication – only sends emails
class EmailService:
def send_welcome(self, to: str):
print(f"Sending welcome email to {to}")
# 6. Auditing – only logs actions
class AuditLogger:
def log(self, user_email: str, action: str):
print(f"Audit: {action} performed on {user_email}")
Now the flow looks like this:
# Usage
email = "alice@example.com"
raw_pw = "superSecret123"
if not UserValidator.validate_email(email):
raise ValueError("Bad email")
user = User(email, raw_pw)
hasher = PasswordHasher()
user.hashed_pw = hasher.hash(raw_pw)
repo = UserRepository()
repo.save(user)
EmailService().send_welcome(user.email)
AuditLogger().log(user.email, "USER_CREATED")
Each class now has one reason to change:
- Tweak validation? Touch
UserValidator. - Switch to bcrypt? Edit
PasswordHasher. - Change DB schema? Go to
UserRepository. - Update email template? Modify
EmailService. - Adjust audit format? Update
AuditLogger.
The benefits? My tests are now laser‑focused. I can mock EmailService without pulling in the DB, and I can swap out the hasher without touching a single line of validation code. The code reads like a story where each character does exactly what they’re supposed to do—no more guessing who’s responsible for what.
Why This New Power Matters
Adopting SRP didn’t just make my code cleaner; it changed how I think about design. I started spotting god classes everywhere and felt an itch to refactor them the moment I saw them. My pull requests became smaller, reviews faster, and the dreaded “works on my machine” bug all but disappeared.
When you give each class a single job, you also make it easier to reuse. Need a password hasher for an admin tool? Import PasswordHasher. Need to send a different kind of email? Drop in a new EmailService subclass. The system becomes a set of LEGO bricks—snap them together in new ways without worrying about hidden connections.
And the best part? You’ll spend less time debugging and more time building cool features. That feeling when a change works on the first try? Pure victory—like finally defeating that end‑game boss after hours of practice.
Your Turn – Start Your Own Quest
Here’s a challenge: pick a class in your current project that feels like it’s doing too much. List every responsibility it has, then extract each one into its own small, focused class. Write a test for each new class before you touch the old one. Notice how the anxiety of making a change drops dramatically.
Give it a try, share your before/after snippets in the comments, and let’s keep leveling up our code together. Happy coding! 🚀
Top comments (0)