Quick Answer

Single responsibility: one reason to change. Open/closed: extend without editing. Liskov: subclasses must be usable as the parent. Interface segregation: no forced unused methods. Dependency inversion: depend on abstractions, not concrete classes.

S — Single Responsibility

A class should have one reason to change.

class Report:
    def fetch_data(self): ...      # database changes
    def format_html(self): ...     # design changes
    def send_email(self): ...      # mail provider changes

Three unrelated reasons to edit this class. A change of mail provider risks breaking report formatting, because they live together and share state. In a team, three people end up editing the same file for unrelated reasons.

Split into ReportRepository, ReportFormatter and EmailSender. Each has one reason to change, each is testable alone.

The common misreading is "a class should do one thing", which leads to hundreds of one-method classes. The principle is about reasons to change — things that change together belong together.

O — Open/Closed

Open for extension, closed for modification.

def area(shape):
    if shape.type == "circle":     return 3.14 * shape.r ** 2
    elif shape.type == "square":   return shape.side ** 2
    # every new shape edits this function

Each new shape means editing tested, working code — and risking the shapes that already worked.

class Circle:
    def area(self): return 3.14 * self.r ** 2

class Square:
    def area(self): return self.side ** 2

total = sum(s.area() for s in shapes)   # never changes

Adding a triangle now means adding a class, not editing existing ones.

Applied too eagerly this produces abstraction for variation that never arrives. A reasonable rule is to write the conditional first, and extract the abstraction the second or third time you edit it for the same reason.

L — Liskov Substitution

A subclass must be usable anywhere its parent is.

The classic violation:

class Bird:
    def fly(self): ...

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("penguins cannot fly")

Any code accepting a Bird and calling fly() now breaks when handed a penguin. The inheritance says "a penguin is a bird", which is true biologically and false for this interface.

Practical smells that indicate a violation: a subclass overriding a method to throw, or to do nothing; and calling code checking isinstance before deciding what to call. If callers must know the concrete type, the abstraction is not working.

The fix is usually to reshape the hierarchy — separate Bird from FlyingBird — or to prefer composition over inheritance entirely.

I and D — Interface Segregation and Dependency Inversion

Interface Segregation: no client should be forced to depend on methods it does not use.

class Worker:                     # too broad
    def work(self): ...
    def eat(self): ...

class Robot(Worker):
    def eat(self): pass           # meaningless, but required

Empty implementations existing only to satisfy an interface mean the interface is doing too much. Split it into Workable and Feedable.

Dependency Inversion: depend on abstractions, not concrete implementations.

class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()      # locked to MySQL, untestable

class OrderService:
    def __init__(self, db):            # anything with the right methods
        self.db = db

The second version can be given a real database in production and a fake in tests, without changing the class. This is the principle behind dependency injection in Spring and most modern frameworks — see Spring Boot basics.

Of the five, this one has the highest practical payoff, because it is what makes code testable.

An honest note on applying them

SOLID emerged from large object-oriented systems, and the principles are genuinely valuable there. Applied mechanically to small programs, they produce codebases where following a single request means opening nine files.

Three sensible guidelines:

  • Duplication is cheaper than the wrong abstraction. Two similar functions are easy to change. One abstraction covering both badly is not.
  • Wait for the second or third repetition before abstracting. The first occurrence does not tell you what varies.
  • Optimise for reading. If a change requires opening many files to understand, the design is not helping regardless of which principles it satisfies.

For interviews, be able to give each principle with a one-line example. For real code, dependency inversion and single responsibility deliver most of the value; the rest matter as systems grow.

Frequently Asked Questions

What does SOLID stand for? Single responsibility, Open/closed, Liskov substitution, Interface segregation, and Dependency inversion. Five design principles for object-oriented code, popularised by Robert Martin.
Does SOLID apply to non-object-oriented code? The underlying ideas do. Single responsibility and dependency inversion apply to functions and modules just as well. The class-specific framing of Liskov is where the fit is weakest.
Is SOLID always worth following? The ideas are sound; mechanical application is not. Small programs made SOLID-compliant can become harder to read. Apply the principles when the problem they prevent has actually appeared.
What is the most important principle in practice? Dependency inversion, because it is what makes code testable, and single responsibility, because it limits how far a change spreads. Those two deliver most of the practical benefit.
How is SOLID asked in interviews? Often as 'explain SOLID', where naming each with a short example is enough. Better interviews show you code and ask which principle it violates, so practise spotting the smells rather than reciting definitions.