Object-Oriented Programming (OOP) is a powerful paradigm that helps us model real-world problems using classes and objects. While the basics of OOP — such as creating classes, instantiating objects, and using methods — are essential skills, the true power of OOP in Python shines when you master its advanced concepts. This lesson will guide you through some of the most important advanced OOP techniques in Python, enabling you to design cleaner, more flexible, and maintainable codebases.
We'll explore key topics including:
- Class and static methods
- Property decorators for controlled attribute access
- Magic (dunder) methods to customize behavior
- Multiple inheritance and the method resolution order (MRO)
- Abstract base classes for interface enforcement
- Composition vs inheritance
By the end of this lesson, you'll be able to harness these tools to create sophisticated Python classes and architectures.
Class Methods and Static Methods: Managing Behavior at the Class Level
Sometimes, you want methods that are not tied to a specific instance, but rather to the class itself. Python provides two special decorators for this: @classmethod and @staticmethod.
- Class Methods take the class itself as the first argument (conventionally named
cls). They can modify class state that applies across all instances. - Static Methods don’t take either
selforclsas the first argument. They behave like regular functions but belong to the class’s namespace.
📌 Deep Dive: Using Class and Static Methods
class Employee:
raise_amount = 1.05 # 5% raise for all employees
def __init__(self, name, salary):
self.name = name
self.salary = salary
def apply_raise(self):
self.salary = int(self.salary * self.raise_amount)
@classmethod
def set_raise_amount(cls, amount):
cls.raise_amount = amount
@staticmethod
def is_workday(day):
# Monday is 0 and Sunday is 6
return day.weekday() < 5
# Usage
import datetime
emp1 = Employee('John Doe', 50000)
print(emp1.salary) # 50000
Employee.set_raise_amount(1.10) # Change raise amount for all employees
emp1.apply_raise()
print(emp1.salary) # 55000
my_date = datetime.date(2024, 6, 15) # This is a Saturday
print(Employee.is_workday(my_date)) # False
55000
False
Notice how set_raise_amount modifies the class variable raise_amount for all instances, while is_workday is a utility function logically grouped inside the Employee class.
Property Decorators: Encapsulating Attribute Access Elegantly
Directly exposing attributes sometimes leads to issues when validation or computed values are needed. Python’s @property decorator allows you to expose methods like attributes, providing a clean interface to get, set, or delete attributes without changing the class’s external API.
📌 Deep Dive: Controlling Attributes Using @property
class Celsius:
def __init__(self, temperature=0):
self._temperature = temperature
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
print("Setting value...")
self._temperature = value
# Usage
c = Celsius()
c.temperature = 37 # Calls setter
print(c.temperature) # Calls getter
try:
c.temperature = -300 # Invalid, raises exception
except ValueError as e:
print(e)
Getting value...
37
Temperature below -273.15 is not possible
This approach lets you change internal implementation without affecting the users of your class. It’s a crucial tool for maintaining backward compatibility in large projects.
Magic Methods: Customizing Object Behavior
Magic methods (or “dunder” methods) are special methods surrounded by double underscores, like __init__, __str__, or __add__. They allow you to define how objects behave with built-in operations such as printing, addition, equality comparison, and more.
Implementing these methods lets your classes integrate smoothly with Python’s syntax and built-in functions, making your objects behave like built-in types.
📌 Deep Dive: Implementing Magic Methods
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
def __add__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return Vector2D(self.x + other.x, self.y + other.y)
def __eq__(self, other):
if not isinstance(other, Vector2D):
return False
return self.x == other.x and self.y == other.y
# Usage
v1 = Vector2D(2, 4)
v2 = Vector2D(5, -2)
print(v1 + v2) # Vector2D(7, 2)
print(v1 == v2) # False
print(v1 == Vector2D(2, 4)) # True
False
True
By overriding __add__, the + operator works intuitively with vectors. The __repr__ method returns a clear, unambiguous string representation, useful for debugging.
Multiple Inheritance and Method Resolution Order (MRO)
Python supports multiple inheritance, allowing a class to inherit from more than one parent class. This enables powerful abstractions but can introduce complexity, especially with method conflicts. Python uses the C3 linearization algorithm to determine the Method Resolution Order (MRO), which is the order in which base classes are searched when a method is called.
💡 Why MRO Matters
When multiple parents define the same method, Python uses the MRO to decide which method to call, ensuring consistent and predictable behavior.
📌 Deep Dive: Multiple Inheritance and MRO
class A:
def greet(self):
print("Hello from A")
class B(A):
def greet(self):
print("Hello from B")
class C(A):
def greet(self):
print("Hello from C")
class D(B, C):
pass
d = D()
d.greet() # Which greet is called?
print(D.__mro__)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
The D class inherits from B and C. The greet method from B is called because B appears before C in the MRO. You can check the MRO by inspecting the __mro__ attribute.
Abstract Base Classes: Defining Interfaces and Contracts
In large projects or libraries, it's essential to enforce that certain classes implement specific methods. Python’s abc module lets you create Abstract Base Classes (ABCs) that define abstract methods. Subclasses must implement these methods, or they cannot be instantiated.
📌 Deep Dive: Using Abstract Base Classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# Usage
rect = Rectangle(4, 5)
print(rect.area()) # 20
print(rect.perimeter()) # 18
# The following will raise an error:
# shape = Shape() # TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
18
ABCs are powerful tools for API design, forcing subclasses to implement critical methods and preventing incomplete implementations.
Composition Over Inheritance: Building Flexible Systems
While inheritance is a core feature of OOP, relying on it excessively can lead to fragile and tightly coupled code. Composition — where objects contain other objects — is often preferred for building flexible systems. It allows you to assemble behaviors dynamically and avoid the pitfalls of complex inheritance hierarchies.
💡 Composition vs Inheritance
Composition means "has-a" relationship (e.g., a Car has an Engine), while inheritance means "is-a" relationship (e.g., a Car is-a Vehicle).
| Inheritance | Composition |
|---|---|
| Creates a tight coupling between parent and child | More flexible; components can be swapped or changed |
| Good for “is-a” relationships | Good for “has-a” relationships |
| Can lead to deep, complex hierarchies | Flatter, easier to maintain structures |
| Behavior is inherited automatically | Behavior is delegated to contained objects |
📌 Deep Dive: Using Composition to Model a Car
class Engine:
def start(self):
print("Engine starting...")
def stop(self):
print("Engine stopping...")
class Car:
def __init__(self):
self.engine = Engine() # Car has an Engine
def start(self):
self.engine.start()
print("Car is now moving")
def stop(self):
self.engine.stop()
print("Car has stopped")
# Usage
my_car = Car()
my_car.start()
my_car.stop()
Car is now moving
Engine stopping...
Car has stopped
By composing a Car with an Engine, we keep responsibilities separated and code modular. This approach scales well for complex systems.

Putting It All Together: Designing a Realistic Class Hierarchy
Let’s design a simplified employee management system that combines many of the concepts we covered.
📌 Deep Dive: Employee Management with Advanced OOP
from abc import ABC, abstractmethod
from datetime import date
class Employee(ABC):
raise_factor = 1.05
def __init__(self, name, salary):
self.name = name
self._salary = salary
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, amount):
if amount < 0:
raise ValueError("Salary cannot be negative.")
self._salary = amount
def apply_raise(self):
self.salary = int(self.salary * self.raise_factor)
@classmethod
def set_raise_factor(cls, factor):
cls.raise_factor = factor
@staticmethod
def is_workday(check_date):
return check_date.weekday() < 5
@abstractmethod
def work(self):
pass
class Developer(Employee):
def __init__(self, name, salary, prog_lang):
super().__init__(name, salary)
self.prog_lang = prog_lang
def work(self):
print(f"{self.name} writes {self.prog_lang} code.")
class Manager(Employee):
def __init__(self, name, salary, employees=None):
super().__init__(name, salary)
self.employees = employees if employees else []
def add_employee(self, emp):
self.employees.append(emp)
def work(self):
print(f"{self.name} manages {len(self.employees)} employees.")
# Usage example
dev = Developer("Alice", 70000, "Python")
mgr = Manager("Bob", 90000, [dev])
dev.work() # Alice writes Python code.
mgr.work() # Bob manages 1 employees.
print(f"Is 2024-06-22 a workday? {Employee.is_workday(date(2024,6,22))}")
mgr.apply_raise()
print(f"{mgr.name}'s new salary after raise: {mgr.salary}")
Bob manages 1 employees.
Is 2024-06-22 a workday? True
Bob's new salary after raise: 94500
This example demonstrates:
- Abstract base class
Employeeforces derived classes to implementwork(). - Class method to adjust raise factor globally.
- Static method to check if a date is a workday.
- Encapsulated salary attribute with property decorators.
- Inheritance for different employee roles.
Such design improves code clarity, enforces rules, and keeps your system extensible.
⚠️ Common Pitfall: Overusing Inheritance
Be cautious not to create unnecessarily deep or broad inheritance trees. Over-inheritance can make your code rigid and hard to maintain. Prefer composition and interface abstractions where appropriate.
Summary
Advanced OOP in Python equips you with tools to write more structured, reusable, and readable code. Remember to:
- Use
@classmethodand@staticmethodto organize methods logically at the class level. - Leverage
@propertydecorators to control attribute access with ease. - Implement magic methods to make your objects behave like built-in types.
- Understand multiple inheritance and how Python’s MRO works to avoid surprises.
- Use abstract base classes to define essential interfaces.
- Favor composition over inheritance to build flexible systems.
Practice these concepts by refactoring your existing code and experimenting with new designs. Mastery of advanced OOP will elevate your Python skills to a professional level.
Quick Knowledge Check
Test what you just learned
Question 1 of 2
What is the main difference between a @classmethod and a @staticmethod in Python?
Question 2 of 2
Why might you prefer composition over inheritance when designing your classes?
Loading results...