Dataslope logoDataslope

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 classesunittest.TestCase, django.db.models.Model, flask.views.MethodView, http.server.BaseHTTPRequestHandler
  • Abstract base classescollections.abc.Iterable, numbers.Number, abc.ABC
  • Exception hierarchiesBaseException → 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

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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.

Code Block
Python 3.13.2

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., ElectricEngine vs GasEngine) without changing the Car class 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:

  1. The subclass truly is a specialized version of the parent (Liskov Substitution Principle).
  2. You need to plug into a framework that requires subclassing (e.g., Django models, Flask views).
  3. 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

Challenge
Python 3.13.2
Abstract shapes with area

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) using math.pi * radius ** 2.

main.py uses your classes. Do not edit main.py.

Challenge
Python 3.13.2
Polymorphic animals

Open animals.py and define:

  • Animal base class with __init__(self, name) and a speak() method that returns f"{self.name} makes a sound".
  • Dog(Animal) that overrides speak() to return f"{self.name} says woof!".
  • Cat(Animal) that overrides speak() to return f"{self.name} says meow!".

main.py uses your classes polymorphically. Do not edit main.py.

Challenge
Python 3.13.2
Employee and Manager with super()

Open employees.py and define:

  • Employee with __init__(self, name, base_salary) and a salary() method that returns self.base_salary.
  • Manager(Employee) with __init__(self, name, base_salary, bonus) that calls super().__init__ and stores the bonus. Override salary() to return self.base_salary + self.bonus.

main.py uses your classes. Do not edit main.py.

Challenge
Python 3.13.2
Override a method

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

QuestionSelect one

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.

QuestionSelect one

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.

QuestionSelect one

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.

QuestionSelect one

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.

QuestionSelect one

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.

QuestionSelect one

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.

On this page