Single Responsibility Principle

RoadmapsPython

Overview

The Single Responsibility Principle (`SRP`) states that a class or module should have one, and only one, reason to change. For example, an `OrderValidator` class should check if data is correct, but should not save the order to a database or send an email. Think of it like a restaurant kitchen: the chef cooks, the waiter serves, and the dishwasher cleans. If the chef has to wash dishes, cooking stops when the sink breaks. In Python, violating `SRP` often leads to `God objects`—massive, brittle classes that handle too many concerns at once. For example, a `GodManager` class might include: - `parse_request()`: Parses incoming HTTP data into a dictionary, returns a `dict`, and raises a `ValueError` if the payload is malformed. - `save_to_db()`: Connects to the database and inserts the record, returns `None`, and raises a `ConnectionError` on network failure. - `send_email()`: Triggers a welcome message, returns a `bool` indicating success, and raises a `TimeoutError` if the server is unreachable. One common sharp edge is the trap of `Anemic Domain Models`, where responsibilities are over-separated. class User: def __init__(self, email): self.email = email def update_email(user, new_email): user.email = new_email When data and its intrinsically linked behavior are torn apart, simple state changes require coordinating multiple classes. A class should still own the simple behaviors that mutate its internal state. Another sharp edge is `Constructor Over-Injection`, where a class takes too many dependencies. class OrderProcessor: def __init__(self, db, logger, emailer, config): self.db = db self.logger = logger If a class needs four different services just to instantiate, it is likely doing too much. Splitting the class into smaller, coordinated services fixes this `code smell`.

It isolates changes. When a requirement shifts (e.g., switching from a local database to a cloud API), you only update one focused class without risking bugs in unrelated logic. This prevents `brittle` code where fixing one bug accidentally breaks another part of the system.

Where used: FastAPI routers, Django views, Pydantic validators

Why learn this

Code walkthrough

class Order:
    def __init__(self, amount):
        self.amount = amount

class EmailService:
    def send_receipt(self, order):
        print(f'Sending receipt for ${order.amount}')

order = Order(150)
mailer = EmailService()
mailer.send_receipt(order)

Focus: mailer.send_receipt(order)

Aha moment

class ReportParser:
    def __init__(self, raw_data):
        self.raw_data = raw_data
        self.parsed = None

    def parse(self):
        self.parsed = {'status': self.raw_data.strip()}
        print(f'Parsed: {self.parsed}')

    def save_to_disk(self):
        print('Writing to local disk...')

report = ReportParser('  SUCCESS  ')
report.parse()
report.save_to_disk()

Prediction: If the business decides to save reports to an S3 bucket instead of local disk, which class must change?

Common guess: The `ReportParser` class, we just change `save_to_disk` to `save_to_s3`.

Modifying `ReportParser` for storage changes violates `SRP`. `ReportParser` should only parse data. A separate `StorageService` should handle the saving mechanism.

Common mistakes

Glossary

God objects
Massive, overly complex classes in a program that try to do too many different things at once. For example, a single manager class that handles saving, emailing, and reporting. Example: `class GodManager:`.
brittle
Code that easily breaks or causes unexpected errors when you try to make small changes or updates. For example, brittle code means fixing one bug accidentally breaks another part of the system. Example: `process_and_validate()`.

Recall questions

Understanding checks

Why does a `God object` violate the Single Responsibility Principle?

A `God object` takes on too many responsibilities (like network, database, and logic) so it has multiple reasons to change.

`SRP` requires a class to have only one reason to change, isolating the changes and preventing unrelated bugs.

Does this code violate `SRP`?

Yes, because it mixes core authentication logic with database querying.

Mixing database access with core business logic means the class has multiple reasons to change.

Practice tasks

Refactor the User Manager

The `UserManager` class currently handles both storing user data and saving it to a database. Refactor it into two classes: `User` to hold the data and `UserRepository` to handle the database simulation.

Challenge

Separate Logging from Execution

You have a `PaymentProcessor` that processes a payment and logs the result directly to the console. Refactor this to separate the logging responsibility into a `ConsoleLogger` class, and pass the logger to the processor's `__init__` method. The processor should use the logger to output the message.

Continue learning

Previous: Class Creation and Instantiation

Previous: Defining functions and return values

Return to Python Roadmap