Movatterモバイル変換


[0]ホーム

URL:


— FREE Email Series —

🐍 Python Tricks 💌

Python Tricks Dictionary Merge

🔒 No spam. Unsubscribe any time.

Browse TopicsGuided Learning Paths
Basics Intermediate Advanced
aialgorithmsapibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnewsnumpyprojectspythonstdlibtestingtoolsweb-devweb-scraping

Table of Contents

Recommended Course

SOLID Principles: Improve Object-Oriented Design in Python

Design and Guidance: Object-Oriented Programming in Python

37m · 8 lessons

SOLID Principles: Improve Object-Oriented Design in Python

SOLID Design Principles: Improve Object-Oriented Code in Python

byLeodanis Pozo RamosPublication date Dec 22, 2025Reading time estimate 21mintermediatebest-practicespython

Table of Contents

Remove ads

Recommended Course

Design and Guidance: Object-Oriented Programming in Python(37m)

A great approach to writing high-quality object-oriented Python code is to consistently apply the SOLID design principles. SOLID is a set of five object-oriented design principles that can help you write maintainable, flexible, and scalable code based on well-designed, cleanly structured classes. These principles are foundational best practices in object-oriented design.

In this tutorial, you’ll explore each of these principles with concrete examples and refactor your code so that it adheres to the principle at hand.

By the end of this tutorial, you’ll understand that:

  • You apply theSOLID design principles to write classes that you can confidently maintain, extend, test, and reason about.
  • You can apply SOLID principles to splitresponsibilities, extend viaabstractions, honorsubtype contracts, keepinterfaces small, andinvert dependencies.
  • You enforce theSingle-Responsibility Principle by separating tasks into specialized classes, giving each class onlyone reason to change.
  • You satisfy theOpen-Closed Principle by defining an abstract class with the required interface and adding newsubclasses without modifying existing code.
  • You honor theLiskov Substitution Principle by making the subtypes preserve theirexpected behaviors.
  • You implementDependency Inversion by making your classes depend onabstractions rather than on details.

Follow the examples to refactor each design, verify behaviors, and internalize how each SOLID design principle can improve your code.

Free Bonus:Click here to download sample code so you can build clean, maintainable classes with the SOLID Principles in Python.

Take the Quiz: Test your knowledge with our interactive “SOLID Design Principles: Improve Object-Oriented Code in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:


SOLID Principles: Improve Object-Oriented Design in Python

Interactive Quiz

SOLID Design Principles: Improve Object-Oriented Code in Python

Test your Python understanding of Liskov substitution, Square–Rectangle pitfalls, and safer API design with polymorphism.

The SOLID Design Principles in Python

When it comes to writingclasses and designing their interactions in Python, you can follow a series of principles that will help you build better object-oriented code. One of the most popular and widely accepted sets of standards forobject-oriented design (OOD) is known as theSOLID design principles.

If you’re coming fromC++ orJava, you may already be familiar with these principles. Maybe you’re wondering if the SOLID principles also apply to Python code. The answer to that question is a resoundingyes. If you’re writing object-oriented code, then you should consider applying these principles to your OOD.

But what are these SOLID design principles? SOLID is an acronym that encompasses five core principles applicable to object-oriented design. These principles are the following:

  1. Single-responsibility principle (SRP)
  2. Open–closed principle (OCP)
  3. Liskov substitution principle (LSP)
  4. Interface segregation principle (ISP)
  5. Dependency inversion principle (DIP)

You’ll explore each of these principles in detail and code real-world examples of how to apply them in Python. In the process, you’ll gain a strong understanding of how to write more straightforward, organized, scalable, and reusable object-oriented code by applying the SOLID design principles. To kick things off, you’ll start with the first principle on the list.

Single-Responsibility Principle (SRP)

Thesingle-responsibility principle (SRP) comes fromRobert C. Martin, more commonly known by his nickname Uncle Bob. Martin is a well-respected figure in software engineering and one of the original signatories of theAgile Manifesto. He coined the termSOLID.

The single-responsibility principle states that:

A class should have only one reason to change.

This means that a class should have only oneresponsibility, as expressed through itsmethods. If a class takes care of more than one task, then you should separate those tasks into dedicated classes with descriptive names. Note that SRP isn’t only aboutresponsibility but also about thereasons for changing the class implementation.

Note: You’ll find the SOLID design principles worded in various ways out there. In this tutorial, you’ll refer to them following the wording that Uncle Bob uses in his bookAgile Software Development: Principles, Patterns, and Practices. So, all the direct quotes come from this book.

If you want to read alternate wordings in a quick roundup of these and related principles, then check out Uncle Bob’sThe Principles of OOD.

This principle is closely related to the concept ofseparation of concerns, which suggests that you should divide your programs into components, each addressing a separate concern.

To illustrate the single-responsibility principle and how it can help you improve your object-oriented design, say that you have the followingFileManager class:

Pythonfile_manager_srp.py
frompathlibimportPathfromzipfileimportZipFileclassFileManager:def__init__(self,filename):self.path=Path(filename)defread(self,encoding="utf-8"):returnself.path.read_text(encoding)defwrite(self,data,encoding="utf-8"):self.path.write_text(data,encoding)defcompress(self):withZipFile(self.path.with_suffix(".zip"),mode="w")asarchive:archive.write(self.path)defdecompress(self):withZipFile(self.path.with_suffix(".zip"),mode="r")asarchive:archive.extractall()

In this example, yourFileManager class has two different responsibilities. It manages files using the.read() and.write() methods. It also deals withZIP archives by providing the.compress() and.decompress() methods.

This class violates the single-responsibility principle because there is more than one reason for changing its implementation (fileI/O and ZIP handling). This implementation also makes code testing and code reuse harder.

To fix this issue and make your design more robust, you can split the class into two smaller, more focused classes, each with its own specific concern:

Pythonfile_manager_srp.py
frompathlibimportPathfromzipfileimportZipFileclassFileManager:def__init__(self,filename):self.path=Path(filename)defread(self,encoding="utf-8"):returnself.path.read_text(encoding)defwrite(self,data,encoding="utf-8"):self.path.write_text(data,encoding)classZipFileManager:def__init__(self,filename):self.path=Path(filename)defcompress(self):withZipFile(self.path.with_suffix(".zip"),mode="w")asarchive:archive.write(self.path)defdecompress(self):withZipFile(self.path.with_suffix(".zip"),mode="r")asarchive:archive.extractall()

Now, you have two smaller classes, each having only a single responsibility:

  1. FileManager takes care of managing a file.
  2. ZipFileManager handles thecompression anddecompression of a file using the ZIP format.

These two classes are smaller, so they’re more manageable. They’re also easier to reason about, test, and debug.

The concept ofresponsibility in this context may be pretty subjective. Having a single responsibility doesn’t necessarily mean having a single method. Responsibility isn’t directly tied to the number of methods but to the core task that your class is responsible for, depending on your idea of what the class represents in your code. However, that subjectivity shouldn’t stop you from striving to use the SRP.

Open-Closed Principle (OCP)

Theopen-closed principle (OCP) for object-oriented design was originally introduced byBertrand Meyer in 1988 and means that:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

To understand what the open-closed principle is all about, consider the followingShape class:

Pythonshapes_ocp.py
frommathimportpiclassShape:def__init__(self,shape_type,**kwargs):self.shape_type=shape_typeifself.shape_type=="rectangle":self.width=kwargs["width"]self.height=kwargs["height"]elifself.shape_type=="circle":self.radius=kwargs["radius"]else:raiseTypeError("Unsupported shape type")defcalculate_area(self):ifself.shape_type=="rectangle":returnself.width*self.heightelifself.shape_type=="circle":returnpi*self.radius**2else:raiseTypeError("Unsupported shape type")

The initializer ofShape takes ashape_typeargument that can be either"rectangle" or"circle". It also takes a specific set of keyword arguments using the**kwargs syntax. If you set the shape type to"rectangle", then you should also pass thewidth andheight keyword arguments so that you can construct a proper rectangle.

In contrast, if you set the shape type to"circle", then you must also pass aradius argument to construct a circle. Finally, if you pass a different shape, like a"triangle", for example, then you’ll get an error because the class doesn’t support this shape.

Additionally, the class has a flaw: it doesn’t constrain the shape-type checking to a single place, the initialization. Instead, it requires checking all across the logic.

Note: This example may seem a bit extreme. Its intention is to clearly expose the core idea behind the open-closed principle.

Shape also has a.calculate_area() method that computes the area of the current shape according to its.shape_typeattribute:

Python
>>>fromshapes_ocpimportShape>>>rectangle=Shape("rectangle",width=10,height=5)>>>rectangle.calculate_area()50>>>circle=Shape("circle",radius=5)>>>circle.calculate_area()78.53981633974483

The class works. You can create circles and rectangles, compute their area, and so on. However, imagine that you need to add a new shape, maybe a square. How would you do that? Well, the option here is to add anotherelif clause to.__init__() and to.calculate_area() so that you can address the requirements of a square shape.

Having to make these changes to create new shapes means that your class is open to modification, which violates the Open-Closed Principle.

Note: OCP doesn’t mean your classes should never be modified. It means that you should minimize the need to modify existing classes when adding new functionality to your code.

How can you fix your code to make it open to extension but closed to modification? Here’s a possible solution:

Pythonshapes_ocp.py
fromabcimportABC,abstractmethodfrommathimportpiclassShape(ABC):def__init__(self,shape_type):self.shape_type=shape_type@abstractmethoddefcalculate_area(self):passclassCircle(Shape):def__init__(self,radius):super().__init__("circle")self.radius=radiusdefcalculate_area(self):returnpi*self.radius**2classRectangle(Shape):def__init__(self,width,height):super().__init__("rectangle")self.width=widthself.height=heightdefcalculate_area(self):returnself.width*self.heightclassSquare(Shape):def__init__(self,side):super().__init__("square")self.side=sidedefcalculate_area(self):returnself.side**2

In this code, you completelyrefactored theShape class, turning it into anabstract base class (ABC). This class provides the requiredinterface (API) for any shape that you’d like to define. Thatinterface consists of a.shape_type attribute and a.calculate_area() method that you must override in all thesubclasses.

Note: The example above and some examples in the next sections use Python’sABCs to provide what’s calledinterface inheritance. In this type ofinheritance, subclasses inherit interfaces rather than functionality. In contrast, when classes inherit functionality, then you’re presented withimplementation inheritance.

This update makes the class closed for modification. Now, you can add new shapes to your class design without the need to modifyShape. In every case, you’ll have to implement the required interface, which also makes your classespolymorphic.

Liskov Substitution Principle (LSP)

TheLiskov substitution principle (LSP) was introduced byBarbara Liskov at anOOPSLA conference in 1987. Since then, this principle has been a fundamental part ofobject-oriented programming. The principle states that:

Subtypes must be substitutable for their base types.

For example, if you have a piece of code that works with aShape class, then you should be able to substitute that class with any of its subclasses, such asCircle orRectangle, without breaking the code.

Note: You can read theconference proceedings from the keynote where Barbara Liskov first shared this principle, or you can watch a short fragment of aninterview with her for more context.

In practice, this principle is about making your subclasses behave like theirbase classes without breaking anyone’s expectations when they call the same methods.

To continue with shape-related examples, say you have aRectangle class like the following:

Pythonshapes_lsp.py
classRectangle:def__init__(self,width,height):self.width=widthself.height=heightdefcalculate_area(self):returnself.width*self.height

InRectangle, you’ve provided the.calculate_area() method, which operates with the.width and.heightinstance attributes.

Because a square is a special case of a rectangle with equal sides, you think of deriving aSquare class fromRectangle in order to reuse the code. Then, you override thesetter method for the.width and.height attributes so that when one side changes, the other side also changes:

Pythonshapes_lsp.py
# ...classSquare(Rectangle):def__init__(self,side):super().__init__(side,side)def__setattr__(self,key,value):super().__setattr__(key,value)ifkeyin("width","height"):self.__dict__["width"]=valueself.__dict__["height"]=value

In this snippet of code, you’ve definedSquare as a subclass ofRectangle. As a user might expect, the classconstructor takes only the side of the square as an argument. Internally, the.__init__() method initializes the parent’s attributes,.width and.height, with theside argument.

You’ve also defined aspecial method,.__setattr__(), to hook into Python’s attribute-setting mechanism and intercept theassignment of a new value to either the.width or.height attribute. Specifically, when you set one of those attributes, the other attribute is also set to the same value:

Python
>>>fromshapes_lspimportSquare>>>square=Square(5)>>>vars(square){'width': 5, 'height': 5}>>>square.width=7>>>vars(square){'width': 7, 'height': 7}>>>square.height=9>>>vars(square){'width': 9, 'height': 9}

Now, you’ve ensured that theSquareobject always remains a valid square, making your life easier for the small price of a bit of memory. Unfortunately, this violates the Liskov substitution principle because you can’t replaceinstances ofRectangle with theirSquare counterparts.

When someone expects a rectangle object in their code, they might assume that it’ll behave like one by exposing two independent.width and.height attributes. Meanwhile, yourSquare class breaks that assumption by changing the behavior promised by the object’s interface. That could have surprising and unwanted consequences, which would likely be hard todebug.

While a square is a specific type of rectangle in mathematics, the classes that represent those shapes shouldn’t inherit from each other via a parent-child relationship if you want them to abide by the LSP. One way to solve this problem is to create a base class for bothRectangle andSquare to extend:

Pythonshapes_lsp.py
fromabcimportABC,abstractmethodclassShape(ABC):@abstractmethoddefcalculate_area(self):passclassRectangle(Shape):def__init__(self,width,height):self.width=widthself.height=heightdefcalculate_area(self):returnself.width*self.heightclassSquare(Shape):def__init__(self,side):self.side=sidedefcalculate_area(self):returnself.side**2

Shape becomes the type that you can substitute through polymorphism with eitherRectangle orSquare, which are now siblings rather than a parent and a child. Notice that both concrete shape types have distinct sets of attributes and different initializer methods, and they could potentially implement other behaviors. The only thing that they have in common is the ability to calculate their area.

With this implementation in place, you can use theShape type interchangeably with itsSquare andRectangle subtypes when you only care about their common behavior:

Python
>>>fromshapes_lspimportRectangle,Square>>>defget_total_area(shapes):...returnsum(shape.calculate_area()forshapeinshapes)...>>>get_total_area([Rectangle(10,5),Square(5)])75

Here, you pass a pair consisting of a rectangle and a square into a function that calculates their total area. Because the function only cares about the.calculate_area() method, it doesn’t matter that the shapes are different. This is the essence of the Liskov substitution principle.

Interface Segregation Principle (ISP)

Theinterface segregation principle (ISP) comes from the same mind as the single-responsibility principle. Yes, it’s another feather inUncle Bob’s cap. The principle’s main idea is that:

Clients should not be forced to depend upon methods that they do not use. Interfaces belong to clients, not to hierarchies.

In this definition,clients are classes and subclasses, andinterfaces consist of methods and attributes. In other words, if a class doesn’t use particular methods or attributes, then those methods and attributes should be segregated into more specific classes.

Consider the following example of a classhierarchy to model printing machines:

Pythonprinters_isp.py
fromabcimportABC,abstractmethodclassPrinter(ABC):@abstractmethoddefprint(self,document):pass@abstractmethoddeffax(self,document):pass@abstractmethoddefscan(self,document):passclassOldPrinter(Printer):defprint(self,document):print(f"Printing{document} in black and white...")deffax(self,document):raiseNotImplementedError("Fax functionality not supported")defscan(self,document):raiseNotImplementedError("Scan functionality not supported")classModernPrinter(Printer):defprint(self,document):print(f"Printing{document} in color...")deffax(self,document):print(f"Faxing{document}...")defscan(self,document):print(f"Scanning{document}...")

In this example, the base class,Printer, provides the interface that its subclasses must implement.OldPrinter inherits fromPrinter and must implement the same interface. However,OldPrinter doesn’t use the.fax() and.scan() methods because this type of printer doesn’t support these functionalities.

This implementation violates the ISP because it forcesOldPrinter to expose an interface that the class doesn’t implement or need. To fix this issue, you should separate the interfaces into smaller and more specific classes. Then, you can create concrete classes by inheriting from multiple interface classes as needed:

Pythonprinters_isp.py
fromabcimportABC,abstractmethodclassPrinter(ABC):@abstractmethoddefprint(self,document):passclassFax(ABC):@abstractmethoddeffax(self,document):passclassScanner(ABC):@abstractmethoddefscan(self,document):passclassOldPrinter(Printer):defprint(self,document):print(f"Printing{document} in black and white...")classNewPrinter(Printer,Fax,Scanner):defprint(self,document):print(f"Printing{document} in color...")deffax(self,document):print(f"Faxing{document}...")defscan(self,document):print(f"Scanning{document}...")

NowPrinter,Fax, andScanner are base classes that provide specific interfaces with a single responsibility each. To createOldPrinter, you only inherit thePrinter interface. This way, the class won’t have unused methods. To create theModernPrinter class, you need to inherit from all the interfaces. In short, you’ve segregated thePrinter interface.

Note: In Python, you’ll rarely define many abstract base classes as you did in the example above. You may instead rely onduck typing ormixin classes to make your code morePythonic and flexible.

This class design allows you to create different machines with different sets of functionalities, making your design more flexible and extensible.

Dependency Inversion Principle (DIP)

Thedependency inversion principle (DIP) is the last principle in the SOLID set. This principle states that:

Abstractions should not depend upon details. Details should depend upon abstractions.

That sounds pretty complex. Here’s an example that will help to clarify it. Say you’re building an application and have aFrontEnd class to display data to the users in a friendly way. The app currently gets its data from a database, so you end up with the following code:

Pythonapp_dip.py
classFrontEnd:def__init__(self,back_end):self.back_end=back_enddefdisplay_data(self):data=self.back_end.get_data_from_database()print("Display data:",data)classBackEnd:defget_data_from_database(self):return"Data from the database"

In this example, theFrontEnd class depends on theBackEnd class and its concrete implementation. You can say that both classes are tightly coupled. Thiscoupling can lead to scalability issues. For example, say that your app is growing fast, and you want the app to be able to read data from aREST API. How would you do that?

You may consider adding a new method toBackEnd to retrieve the data from the REST API. However, that will also require you to modifyFrontEnd, which should be closed to modification according to theopen-closed principle.

To fix the issue, you can apply the dependency inversion principle and make your classes depend on abstractions rather than on concrete implementations likeBackEnd. In this specific example, you can introduce aDataSource class that provides the interface to use in your concrete classes:

Pythonapp_dip.py
fromabcimportABC,abstractmethodclassFrontEnd:def__init__(self,data_source):self.data_source=data_sourcedefdisplay_data(self):data=self.data_source.get_data()print("Display data:",data)classDataSource(ABC):@abstractmethoddefget_data(self):passclassDatabase(DataSource):defget_data(self):return"Data from the database"classAPI(DataSource):defget_data(self):return"Data from the API"

In this redesigned code, you’ve added aDataSource class as an abstraction that provides the required interface, consisting of the.get_data() method. Note howFrontEnd now depends on the interface provided byDataSource, which is an abstraction.

Then you define theDatabase class, which is a concrete implementation for those cases where you want to retrieve the data from your database. This class depends on theDataSource abstraction throughinheritance. Finally, you define theAPI class to support retrieving the data from the REST API. This class also depends on theDataSource abstraction.

Here’s how you can use theFrontEnd class in your code:

Python
>>>fromapp_dipimportAPI,Database,FrontEnd>>>db_front_end=FrontEnd(Database())>>>db_front_end.display_data()Display data: Data from the database>>>api_front_end=FrontEnd(API())>>>api_front_end.display_data()Display data: Data from the API

Here, you first initializeFrontEnd using aDatabase object and then again using anAPI object. Every time you call.display_data(), the result will depend on the concrete data source that you use. Note that you can also change the data source dynamically by reassigning the.data_source attribute in yourFrontEnd instance.

Conclusion

You’ve learned a lot about the fiveSOLID design principles, including how to identify code that violates them and how to refactor the code in adherence to best design practices. You saw good and bad examples related to each principle and learned that applying the SOLID principles can help you improve yourobject-oriented design in Python.

In this tutorial, you’ve learned how to:

  • Understand themeaning andpurpose of eachSOLID principle
  • Identify class designs thatviolate some of the SOLID principles in Python
  • Use the SOLID principles to help yourefactor Python code and improve its OOD

With this knowledge, you have a strong foundation of well-established best practices that you should apply when designing your classes and their relationships in Python. By applying these principles, you can create code that’s more maintainable, extensible, scalable, and testable.

Free Bonus:Click here to download sample code so you can build clean, maintainable classes with the SOLID Principles in Python.

Frequently Asked Questions

Now that you have some experience with the SOLID design principles in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click theShow/Hide toggle beside each question to reveal the answer.

The SOLID design principles are five ideas that guide how you split responsibilities, add features without risky modifications, respect subtype contracts, keep interfaces focused, and depend on abstractions. You use SOLID to design classes that you can maintain, extend, and test with confidence.

You provide each class with one clear reason to change and move unrelated behaviors into separate classes. For example, you keep file I/O in one class and ZIP compression in another, which allows you to simplify testing, improve component reusability, and reduce coupling.

You define an abstract interface that subclasses extend without forcing edits to the existing class.

For example, you create aShape base with.calculate_area() and addCircle orSquare by implementing that method instead of changing.__init__() or.calculate_area() in the base.

You ensure that any subtype (subclass) behaves like its base type (base class) so callers don’t face surprises.

In the shapes example, you avoid makingSquare aRectangle when changing.width shouldn’t silently change.height, and you instead share aShape base with.calculate_area().

Take the Quiz: Test your knowledge with our interactive “SOLID Design Principles: Improve Object-Oriented Code in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:


SOLID Principles: Improve Object-Oriented Design in Python

Interactive Quiz

SOLID Design Principles: Improve Object-Oriented Code in Python

Test your Python understanding of Liskov substitution, Square–Rectangle pitfalls, and safer API design with polymorphism.

Recommended Course

Design and Guidance: Object-Oriented Programming in Python(37m)

🐍 Python Tricks 💌

Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

AboutLeodanis Pozo Ramos

Leodanis is a self-taught Python developer, educator, and technical writer with over 10 years of experience.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

MasterReal-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

MasterReal-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.


Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics:intermediatebest-practicespython

Related Learning Paths:

Related Courses:

Related Tutorials:

Keep reading Real Python by creating a free account or signing in:

Already have an account?Sign-In

Almost there! Complete this form and click the button below to gain instant access:

SOLID Principles: Improve Object-Oriented Design in Python

SOLID Principles: Improve Object-Oriented Design in Python (Sample Code)

🔒 No spam. We take your privacy seriously.


[8]ページ先頭

©2009-2026 Movatter.jp