The Single Responsibility Principle says a class should have only one reason to change. See it in action: take a Python AuthService that also owns its logging, find why that's a problem, and refactor it into clean, focused classes.
Here’s a quick challenge to start. Before we talk about the Single Responsibility Principle, take a look at this Python class and see if you can spot the issue:
import datetime
class AuthService:
log_file = "log.txt"
def login(self, username: str, password: str) -> bool:
print(f"Checking credentials for {username}...")
# Simulated authentication
if username == "admin" and password == "1234":
self.log_event(f"User {username} logged in successfully.")
return True
self.log_event(f"Failed login attempt for {username}.")
return False
def log_event(self, message: str):
with open(self.log_file, "a") as file:
file.write(f"{datetime.datetime.now()} - {message}\n")At first glance this looks perfectly normal. The class checks a username and password, then writes a log message whether the login succeeds or fails. Technically, it works.
The problem is that AuthService is doing more than one thing. It’s supposed to handle authentication — but it also owns the logging. It stores the log file name, opens the file, writes the message, and controls the log format.
That means the class now has more than one reason to change:
AuthService.AuthService.Two unrelated concerns are tangled together in one class. That’s the issue.
Pull the logging out into its own class, and let AuthService use it instead of being it:
import datetime
class Logger:
log_file = "log.txt"
def log_event(self, message: str):
with open(self.log_file, "a") as file:
file.write(f"{datetime.datetime.now()} - {message}\n")
class AuthService:
def __init__(self, logger_obj: Logger):
self.logger = logger_obj
def login(self, username: str, password: str) -> bool:
print(f"Checking credentials for {username}...")
if username == "admin" and password == "1234":
self.logger.log_event(f"User {username} logged in successfully.")
return True
self.logger.log_event(f"Failed login attempt for {username}.")
return FalseNow the responsibilities are separated. Logger handles logging, and AuthService handles authentication.
AuthService can still log events, but it no longer needs to know how the log is written. It just calls self.logger.log_event(...), and all the logging details stay inside Logger. So if logging changes later, you change Logger; if authentication changes later, you change AuthService. Each class has one reason to change.
This works through composition — AuthService is given a Logger to use rather than creating one internally. Because the logger is passed in (a small dose of dependency injection), it’s easy to swap in a different logger later, including a fake one in your tests.
You create a Logger, pass it into AuthService, and let the auth service use it:
# Usage
logger = Logger()
auth = AuthService(logger)
auth.login(username="admin", password="1234")
auth.login(username="user", password="wrongpassword")AuthService uses that logger when a login succeeds or fails — without owning the logging implementation itself.
That’s the point of the Single Responsibility Principle: a class should have only one reason to change, which in practice means it should do one thing.
When a class takes on multiple responsibilities, it becomes harder to maintain, harder to test, and harder to change without breaking something unrelated. By separating concerns into focused classes, you get code that is cleaner, easier to test in isolation, and more scalable as the project grows.
SRP is the “S” in SOLID, the five object-oriented design principles. We cover it — along with composition over inheritance and the rest of SOLID — with worked examples in Lesson 6: Object-Oriented Programming of our free Software Engineering course, and we go deeper on writing focused classes in Lesson 7: Clean Code and Best Practices.
AuthService owned both authentication and logging.Logger) and composing it in, rather than baking it into AuthService.Hopefully this makes the idea a little clearer. If you want to build the foundations underneath it, start with our free Software Engineering course, and strengthen the Python it’s written in with Python for Data Analytics.