A class should have one, and only one, reason to change.

Like atomic notes, each class in a piece of software should solve a singular problem as expressed through the methods of that class. For example, should a specific class take care of more than a single task, the class should be separated into multiple classes.

This is perhaps a more specific version of the wider ideas of Layered Architectures and Domain Driven Design, where we attempt to design specific functionality that is atomic.

Take a FileManager class for instance:

# file_manager_srp.py
 
from pathlib import Path
from zipfile import ZipFile
 
class FileManager:
    def __init__(self, filename):
        self.path = Path(filename)
 
    def read(self, encoding="utf-8"):
        return self.path.read_text(encoding)
 
    def write(self, data, encoding="utf-8"):
        self.path.write_text(data, encoding)
 
    def compress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
            archive.write(self.path)
 
    def decompress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
            archive.extractall()

This FileManager has two different responsibilities: deals with IO with .read() and .write() and also manages ZIP archives by providing .compress() and .decompress(). This means that there are two reasons why the class might be modified.

Quote

This class violates the single-responsibility principle because it has two reasons for changing its internal implementation.

A solution to this is to split the class into a FileManager that simply implements .read() and .write() and a ZipFileManager that deals with .compress() and .decompress()

# file_manager_srp.py
 
from pathlib import Path
from zipfile import ZipFile
 
class FileManager:
    def __init__(self, filename):
        self.path = Path(filename)
 
    def read(self, encoding="utf-8"):
        return self.path.read_text(encoding)
 
    def write(self, data, encoding="utf-8"):
        self.path.write_text(data, encoding)
 
class ZipFileManager:
    def __init__(self, filename):
        self.path = Path(filename)
 
    def compress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
            archive.write(self.path)
 
    def decompress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
            archive.extractall()

Note that when doing this we can also implement OOP interfaces to link the code, or class inheritance if necessary.