You may meet two more OOP features in real code: abstract base classes (ABCs) and multiple inheritance. Both are powerful, both are easy to overuse, and both have modern alternatives.
This lesson is short on purpose. The goal is to recognise them when you see them, not master them.
Abstract base classes
An abstract base class is a class that can’t be instantiated directly. It exists only to be inherited from — and to require subclasses to implement specific methods.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# can't instantiate Shape directly
Shape() # TypeError: Can't instantiate abstract class Shape
c = Circle(5.0)
print(c.area()) # 78.53975
Two pieces:
- Inherit from
ABC(from theabcmodule). - Decorate methods with
@abstractmethod.
Any subclass that doesn’t implement area will also be uninstantiable.
ABCs vs Protocols — which to use?
For most modern code, Protocols win:
- Protocols are structural — any matching class works. No inheritance required.
- ABCs are nominal — you must explicitly inherit. Tight coupling.
Use ABCs when:
- You’re building a class hierarchy where subclasses really should inherit shared behaviour (not just an interface).
- You’re working with a library or framework that already uses ABCs (the standard library has many).
Use Protocols when:
- You want to describe “anything with this shape”.
- You’re accepting types that you don’t control.
In a clean greenfield Python project, you’ll write more Protocols than ABCs.
Multiple inheritance
A class can inherit from more than one parent:
class Loggable:
def log(self, message: str) -> None:
print(f"[{self.__class__.__name__}] {message}")
class Cacheable:
def cache(self) -> None:
print("caching")
class Service(Loggable, Cacheable):
pass
s = Service()
s.log("starting") # [Service] starting
s.cache() # caching
Service gets methods from both parents. This works — but it’s a sharp tool.
The MRO
When you call s.log(), Python searches in method resolution order:
print(Service.__mro__)
# (<class 'Service'>, <class 'Loggable'>, <class 'Cacheable'>, <class 'object'>)
It checks Service first, then Loggable, then Cacheable. The first match wins.
For simple cases, the MRO is predictable. With deep, multiply-inherited hierarchies, it becomes a puzzle. This is why most experienced Python developers avoid multiple inheritance except for simple mixins.
Mixins — the one well-loved case
A mixin is a tiny class that adds one capability, intended to be combined with others. The name is convention — mixins usually have a trailing Mixin or similar:
class JsonExportMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class User(JsonExportMixin):
def __init__(self, name: str) -> None:
self.name = name
u = User("Manikandan")
print(u.to_json()) # {"name": "Manikandan"}
A mixin doesn’t define __init__ and isn’t meant to stand alone. It just contributes one feature.
Mixins are how Django, the most popular Python web framework, builds class-based views. Real-world example, real-world tradeoff: you save typing at the cost of harder-to-read hierarchies.
When NOT to use multiple inheritance
- Don’t combine two classes that both have their own
__init__with different signatures. - Don’t combine classes that fight over which version of a method runs.
- Don’t reach for it because “I have two slightly different things and I want to share code”.
For sharing code, composition (holding an instance) is almost always cleaner. For shared interfaces, Protocols are cleaner.
Summary of Section 13
You can now:
- Run pyright in strict mode and read its output
- Use generic types like
list[T]and write your own withTypeVar - Handle Optional values with
X | Noneand type narrowing - Define Protocols for structural typing
- Recognise ABCs and multiple inheritance, and know when to use them sparingly
Strict-typed Python catches a huge category of bugs before runtime. The cost is more annotations; the payoff is faster refactoring, better autocompletion, and confidence that the code you didn’t run today still works.
What’s next
Section 14: a deeper tour of the standard library — the modules you’ll use day to day.