Inheritance and Polymorphism
Subclassing, super(), method overriding, and duck typing
A class can inherit from another, getting all its attributes and methods for free. The new class can add behavior, override behavior, or both. Inheritance is one of the pillars of object-oriented programming — but it's also easy to overuse.
Why inheritance matters: the real world
Inheritance shows up everywhere in Python:
- Framework base classes —
unittest.TestCase,django.db.models.Model,flask.views.MethodView,http.server.BaseHTTPRequestHandler - Abstract base classes —
collections.abc.Iterable,numbers.Number,abc.ABC - Exception hierarchies —
BaseException → Exception → ValueError - Mixins — Small classes that add a single behavior (logging, serialization, access control)
- Liskov Substitution Principle — A subclass should be usable anywhere the parent is
Understanding inheritance is essential for working with frameworks and designing extensible systems. But remember: composition is often better than inheritance. We'll cover that too.
Real-world example: Django models
Every Django model inherits from `django.db.models.Model`. That base class provides the entire ORM machinery: `save()`, `delete()`, `objects.filter(...)`, etc. You just define fields and optionally override methods. This is inheritance done right: the base class provides infrastructure, subclasses provide domain logic.
Single inheritance
Dog(Animal) reads as "Dog inherits from Animal" or "Dog is a subclass of Animal". Calling Dog("Rex") runs Animal.__init__ because Dog does not define its own __init__.
Method overriding
When a subclass defines a method with the same name as a parent method, the subclass version overrides the parent.
When to override
Override a method when you need different behavior for the subclass. If you want to extend the parent's behavior (call the parent's version first, then add more), use `super()` (next section).
super(): calling parent methods
super() gives you access to the parent class's methods. The canonical use is to extend an inherited method rather than replace it entirely.
Without super().__init__(name), self.name would never be set. super() lets you reuse the parent's initialization logic.
super() in multiple inheritance
In single inheritance, `super()` just calls the parent. In multiple inheritance, `super()` follows the Method Resolution Order (MRO), which is computed by the C3 linearization algorithm. This ensures every class is called exactly once in a consistent order. See the MRO section below.
Polymorphism: same interface, different behavior
Polymorphism means "many shapes". In programming, it means you can call the same method on different types and get type-appropriate behavior.
None of these classes inherit from a common base, but they all work with for animal in animals: animal.speak() because they all implement speak(). This is duck typing.
Duck typing: 'If it walks like a duck...'
"If it walks like a duck and quacks like a duck, it's a duck." In Python, you don't need inheritance for polymorphism. Two unrelated classes are substitutable if they expose the same methods. This is why Python's `open()` works with files, sockets, and in-memory buffers: they all have `.read()` and `.write()`.
isinstance and issubclass
Use these instead of type(x) is SomeClass when you want to allow subclasses.
isinstance(obj, Class) returns True if obj is an instance of Class or any subclass. type(obj) is Class only returns True for exact type matches.
Prefer isinstance over type checks
Use `isinstance(x, int)` instead of `type(x) is int`. The former allows subclasses (e.g., `bool` is a subclass of `int`); the latter does not. In general, checking types at all is un-Pythonic — prefer duck typing and "ask forgiveness, not permission" (EAFP).
Multiple inheritance and the MRO
Python allows a class to inherit from multiple parents. Method lookup uses the Method Resolution Order (MRO), which is computed with the C3 linearization algorithm.
The MRO for D is [D, B, C, A, object]. super() walks this list, so D.hello() calls B.hello(), which calls C.hello(), which calls A.hello().
Multiple inheritance is powerful but tricky
Most well-designed codebases either avoid multiple inheritance or restrict it to mixins — small classes that add a single piece of behavior (e.g., `LoggingMixin`, `JSONSerializableMixin`). Deep multiple-inheritance hierarchies are hard to reason about and debug.
Abstract base classes
An abstract base class (ABC) is a class that cannot be instantiated directly. Subclasses must implement certain methods marked @abstractmethod.
Use ABCs when you want to enforce a contract: "any subclass must implement these methods". This is common in framework design.
ABCs in the standard library
Python's `collections.abc` module defines many useful ABCs: `Iterable`, `Sequence`, `Mapping`, `Set`, etc. If you want to check "does this object behave like a list?", use `isinstance(obj, collections.abc.Sequence)` instead of `isinstance(obj, list)` — it allows any list-like object, not just `list`.
Composition over inheritance
A common design principle: prefer composition (a class holds another as an attribute) over deep inheritance hierarchies.
# Composition: an Engine is PART OF a Car
class Engine:
def start(self):
return "vroom"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
# vs. inheritance: a Car IS AN Engine? That makes no sense.
class Car(Engine): # bad!
...Why prefer composition?
- Flexibility — You can swap out components (e.g.,
ElectricEnginevsGasEngine) without changing theCarclass hierarchy. - Easier to understand — "a Car has an Engine" is clearer than "a Car is an Engine".
- Avoids fragile base class problem — Changes to a base class can break subclasses in surprising ways.
When to use inheritance
Use inheritance when:
- The subclass truly is a specialized version of the parent (Liskov Substitution Principle).
- You need to plug into a framework that requires subclassing (e.g., Django models, Flask views).
- You're using a mixin to add a single, orthogonal behavior.
Otherwise, prefer composition.
The Liskov Substitution Principle
Named after Barbara Liskov, the LSP states: a subclass should be usable anywhere the parent is. In other words, substituting a Dog for an Animal should not break code that expects an Animal.
class Bird:
def fly(self):
return "flying"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly!")
def make_it_fly(bird):
return bird.fly()
# This breaks the LSP: Penguin is-a Bird, but it can't fly.
# The type hierarchy is wrong.Better: don't make Penguin inherit from Bird if the fly() contract is core to Bird. Or use composition.
Multi-file challenges
Open shapes.py and define an abstract base class Shape with an abstract area() method. Then implement two concrete subclasses:
Rectangle(width, height)with rectangle area.Circle(radius)usingmath.pi * radius ** 2.
main.py uses your classes. Do not edit main.py.
Open animals.py and define:
Animalbase class with__init__(self, name)and aspeak()method that returnsf"{self.name} makes a sound".Dog(Animal)that overridesspeak()to returnf"{self.name} says woof!".Cat(Animal)that overridesspeak()to returnf"{self.name} says meow!".
main.py uses your classes polymorphically. Do not edit main.py.
Open employees.py and define:
Employeewith__init__(self, name, base_salary)and asalary()method that returnsself.base_salary.Manager(Employee)with__init__(self, name, base_salary, bonus)that callssuper().__init__and stores the bonus. Overridesalary()to returnself.base_salary + self.bonus.
main.py uses your classes. Do not edit main.py.
Define a base class Vehicle with a method start() that returns "Vehicle starting". Then define a subclass Car(Vehicle) that overrides start() to return "Car engine starting".
Multiple choice questions
What does super().__init__(name) do inside a subclass?
Skips the parent class's constructor.
Calls the parent class's __init__ with name.
Replaces the parent class's constructor entirely.
Creates a new instance of the parent class.
What is duck typing in Python?
A type system where every variable must declare its type.
If two objects expose the same methods, they can be used interchangeably, regardless of inheritance.
A way to convert one type to another implicitly.
A design pattern for creating families of related objects.
When should you use isinstance(obj, Class) instead of type(obj) is Class?
When you want to check for exact type match, disallowing subclasses.
When you want to allow subclasses (e.g., bool is a subclass of int).
When obj might be None.
When you need better performance.
What is the Method Resolution Order (MRO)?
The order in which methods are defined in a class.
The order Python searches for methods when a class has multiple inheritance.
The order in which __init__ methods are called.
The order in which instance attributes are initialized.
What is an abstract base class (ABC)?
A class that has no methods, only attributes.
A class that cannot be instantiated directly; subclasses must implement abstract methods.
A class that uses @staticmethod for all methods.
A class that is automatically generated by Python.
Why is "composition over inheritance" often recommended?
Composition is faster at runtime.
Composition is more flexible, easier to understand, and avoids fragile base class problems.
Inheritance is deprecated in Python 3.
Composition allows multiple inheritance; inheritance does not.
Next we will look at how for loops actually iterate, and how yield turns a function into a generator.