Inheritance & polymorphism
Subclasses, overriding, super(), duck typing, abstract base classes, mixins, the MRO and composition.
Inheritance lets you define a new class based on an existing one: the new subclass gets all the attributes and methods of its parent (base) class, and can add or change behaviour. Combined with polymorphism — different objects responding to the same method call in their own way — it lets you write code that works with whole families of types.
Basic inheritance#
Dog didn't define __init__ or introduce — it inherited them. It overrode speak. Notice that introduce, written in Animal, calls self.speak() — and Python uses the subclass's version. That's polymorphism in action.
Every class ultimately inherits from object, which provides defaults like __repr__ and __eq__.
Extending the parent with super()#
When a subclass needs extra attributes, it overrides __init__ — and should call the parent's initialiser with super() so the inherited setup still happens:
Forgetting super().__init__(...) is a classic bug: the subclass object never gets name and salary, and you get an AttributeError later.
Polymorphism and duck typing#
Polymorphism means code can call the same method on different types without knowing which one it has:
In Python, polymorphism doesn't even require a shared parent. Duck typing — "if it walks like a duck and quacks like a duck…" — means any object with the right method works:
This is why Python functions usually check behaviour, not types: json.dump writes to anything with a write method, for loops over anything iterable.
Abstract base classes#
Sometimes a parent class exists only to define an interface that subclasses must implement. The abc module enforces this:
The error appears when you create the broken object, not much later when someone calls pay. (For structural, duck-typed interfaces checked by type checkers, see typing.Protocol in the type hints lesson.)
Multiple inheritance and the MRO#
A class can inherit from several parents. Python decides which method wins using the Method Resolution Order (MRO), which you can inspect:
The most common, sane use of multiple inheritance is mixins: small classes that add one capability and aren't meant to stand alone. Deep diamond-shaped hierarchies quickly become confusing — avoid them in application code. Using super() consistently (rather than calling Parent.method(self) directly) is what makes cooperative multiple inheritance work.
Composition over inheritance#
Inheritance models an is-a relationship: a Manager is an Employee. When you just want to reuse behaviour, composition — holding another object (a has-a relationship) — is usually more flexible:
With inheritance you'd need PetrolCar, ElectricCar, HybridCar… With composition you plug in a different part.
Inheriting from built-ins#
You can subclass built-in types, and exceptions are the everyday example. For containers, subclassing collections.UserDict/UserList is safer than subclassing dict/list directly, because the built-ins' C methods don't always call your overrides.
Common mistakes#
- Forgetting
super().__init__()in an overridden__init__. - Deep hierarchies (five levels of subclasses) — prefer composition and small mixins.
- Overriding a method with a different signature — callers that work with the parent will break.
- Using inheritance just to share a helper function — a module-level function or composition is simpler.
What's next#
You've overridden __repr__ and __init__ — special "dunder" methods. Next you'll learn the rest: magic methods that make your objects work with +, ==, len(), for loops and with statements.
Check your understanding
Quick quiz
1.What does
super().__init__(name)do inside a subclass's__init__?2.What happens when you try to instantiate a class that inherits from
ABCand has an unimplemented@abstractmethod?3.Which statement best describes "composition over inheritance"?
Finished reading?
Mark this lesson complete to track your progress.