← All articles
Software EngineeringPython

The Single Responsibility Principle: One Class, One Reason to Change

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.

What’s Wrong Here?

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:

  • If you want to change how authentication works, you edit AuthService.
  • If you want to change how logging works — a new format, a different file, sending logs to a service — you also edit AuthService.

Two unrelated concerns are tangled together in one class. That’s the issue.

A before-and-after diagram. Before: a single AuthService class owns authentication and logging, giving it two reasons to change. After: AuthService handles authentication only and delegates to a separate Logger class, so each class has one reason to change.
Before, AuthService owns two concerns and has two reasons to change. After, each class has exactly one job.

The Fix: Separate the Responsibilities

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 False

Now 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 compositionAuthService 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.

Using It

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.

Why This Matters

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.

Summary

  • A class with two jobs has two reasons to change — the original AuthService owned both authentication and logging.
  • The Single Responsibility Principle says each class should have only one reason to change.
  • Fix it by extracting the second concern into its own class (Logger) and composing it in, rather than baking it into AuthService.
  • Passing the dependency in makes the code easier to swap, mock, and test.
  • The result is cleaner, more testable, and more scalable code — the payoff of SRP and the rest of SOLID.

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.

More from the blog