Skip to content

Inheritance and Multiple Inheritance#

Inheritance lets a class reuse and extend another class's behavior — an is-a relationship (Dog is an Animal). Multiple inheritance lets a class inherit from more than one parent — powerful for mixins, dangerous in deep hierarchies. Python supports both fully, with C3 linearization determining method lookup order.

This page is the advanced companion to Introdicion to OOPs and Classes and OOP Basics.

How to use this page

Master single inheritance and super() first, then mixins and diamonds. Essential follow-up: Method Resolution Order, Polymorphism.

At a glance
Track Python Advanced → Object Oriented Programming
Sections 15 major topics
Outline Use the right-hand TOC to jump

Topics: Single inheritance fundamentals · super() — not "call the parent" · Method overriding vs overloading · isinstance and issubclass · Composition vs inheritance · Multiple inheritance · Mixins — intended multiple inheritance · The diamond problem · … (+7 more)

  1. Single inheritance fundamentals
  2. super() — not "call the parent"
  3. Method overriding vs overloading
  4. isinstance and issubclass
  5. Composition vs inheritance
  6. Multiple inheritance
  7. Mixins — intended multiple inheritance
  8. The diamond problem
  9. Cooperative multiple inheritance
  10. Template method pattern
  11. Inheriting built-in types
  12. __init_subclass__ — hook on subclass creation … and 3 more sections

Single inheritance fundamentals#

class Animal:
    def __init__(self, name: str):
        self.name = name

    def speak(self) -> str:
        return "..."

class Dog(Animal):
    def speak(self) -> str:
        return "woof"

class GuideDog(Dog):
    def __init__(self, name: str, owner: str):
        super().__init__(name)
        self.owner = owner
Term Meaning
Base / parent / superclass Class being inherited from
Derived / child / subclass Class that inherits
Override Redefine a method in subclass
Extend Add new methods or call super() then add behavior

super() — not "call the parent"#

super() returns a proxy that delegates to the next class in the MRO — not necessarily the direct parent:

class A:
    def greet(self):
        return "A"

class B(A):
    def greet(self):
        return super().greet() + " B"

class C(A):
    def greet(self):
        return super().greet() + " C"

class D(B, C):
    def greet(self):
        return super().greet() + " D"

D().greet()   # "A C B D"

The order follows D.__mro__: (D, B, C, A, object).

Interview trap — super() in single inheritance

class Parent:
    def __init__(self):
        self.initialized = True

class Child(Parent):
    def __init__(self, x: int):
        super().__init__()   # required — Parent.__init__ not automatic
        self.x = x
Python does not auto-call parent __init__. Forgetting super().__init__() leaves parent state uninitialized.


Method overriding vs overloading#

Python has override (same name, subclass replaces) but no method overloading by signature:

class Calculator:
    def add(self, a: int, b: int = 0) -> int:
        return a + b

# Can't define add(self, a, b, c) as separate overload — second replaces first

Use default arguments, *args, or @overload from typing for type checkers only:

from typing import overload

class Converter:
    @overload
    def convert(self, x: int) -> str: ...
    @overload
    def convert(self, x: str) -> int: ...

    def convert(self, x: int | str) -> int | str:
        if isinstance(x, int):
            return str(x)
        return int(x)

isinstance and issubclass#

dog = Dog("Rex")
isinstance(dog, Dog)       # True
isinstance(dog, Animal)    # True — transitive
issubclass(GuideDog, Dog)  # True
issubclass(Dog, GuideDog)  # False

Prefer isinstance(obj, BaseClass) over type(obj) == BaseClass — respects subclasses.


Composition vs inheritance#

Relationship Pattern When
is-a Inheritance Subtype substitutability is real
has-a Composition Combining behaviors, avoiding fragile hierarchies
# Inheritance — genuine subtype
class SortedList(list):
    def append(self, item):
        super().append(item)
        self.sort()

# Composition — preferred for flexibility
class Engine:
    def start(self) -> None: ...

class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self) -> None:
        self.engine.start()

Rule of thumb: if you're inheriting only to reuse code (not for polymorphic substitution), consider composition.


Multiple inheritance#

class Flyer:
    def fly(self) -> str:
        return "flying"

class Swimmer:
    def swim(self) -> str:
        return "swimming"

class Duck(Animal, Flyer, Swimmer):
    def speak(self) -> str:
        return "quack"

d = Duck("Donald")
d.speak()   # quack
d.fly()     # flying
d.swim()    # swimming

Python resolves d.fly() by walking the MRO left-to-right through base classes.


Mixins — intended multiple inheritance#

Mixins are small, focused classes that add one capability — not meant to stand alone:

class JSONMixin:
    def to_json(self) -> str:
        import json
        return json.dumps(self.__dict__)

class TimestampMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        import time
        self.created_at = time.time()

class Record(JSONMixin, TimestampMixin):
    def __init__(self, name: str):
        super().__init__()
        self.name = name

Record("Alice").to_json()

Mixin rules#

Do Don't
Keep mixins small and focused Create deep mixin chains
Call super().__init__() cooperatively Assume a specific parent __init__
Put mixins before base class in MRO Use mixins for unrelated state blobs
Name with Mixin suffix Instantiate mixins directly
class Record(JSONMixin, TimestampMixin):
    ...
# MRO: Record → JSONMixin → TimestampMixin → object

The diamond problem#

class A:
    def method(self):
        print("A")

class B(A):
    def method(self):
        print("B")
        super().method()

class C(A):
    def method(self):
        print("C")
        super().method()

class D(B, C):
    def method(self):
        print("D")
        super().method()

D().method()
# Output:
# D
# B
# C
# A

C3 linearization produces D → B → C → A → object — each class appears once, parents respect order. Details: Method Resolution Order.


Cooperative multiple inheritance#

Every class in the chain must call super() — not hardcode a parent class name:

# Bad — breaks if MRO changes
class B(A):
    def __init__(self):
        A.__init__(self)

# Good — cooperative
class B(A):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

Full cooperative example#

class Root:
    def __init__(self, value: int, **kwargs):
        self.value = value

class Left(Root):
    def __init__(self, value: int, left_tag: str = "", **kwargs):
        super().__init__(value, **kwargs)
        self.left_tag = left_tag

class Right(Root):
    def __init__(self, value: int, right_tag: str = "", **kwargs):
        super().__init__(value, **kwargs)
        self.right_tag = right_tag

class Both(Left, Right):
    def __init__(self, value: int, left_tag: str, right_tag: str):
        super().__init__(value, left_tag=left_tag, right_tag=right_tag)

b = Both(1, "L", "R")
b.value       # 1
b.left_tag    # "L"
b.right_tag   # "R"

Passing kwargs through the chain requires each class to accept and forward unknown kwargs — or use a shared initialization protocol.


Template method pattern#

Base class defines algorithm skeleton; subclasses fill in steps:

class DataProcessor:
    def process(self, raw: str) -> dict:
        cleaned = self.clean(raw)
        parsed = self.parse(cleaned)
        return self.validate(parsed)

    def clean(self, raw: str) -> str:
        raise NotImplementedError

    def parse(self, cleaned: str) -> dict:
        raise NotImplementedError

    def validate(self, parsed: dict) -> dict:
        return parsed   # optional override

class CSVProcessor(DataProcessor):
    def clean(self, raw: str) -> str:
        return raw.strip()

    def parse(self, cleaned: str) -> dict:
        keys, *rows = cleaned.split("\n")
        headers = keys.split(",")
        return dict(zip(headers, rows[0].split(",")))

ABC version enforces steps at instantiation: Abstract Base Classes.


Inheriting built-in types#

class CounterDict(dict):
    def __init__(self):
        super().__init__()
        self.access_count = 0

    def __getitem__(self, key):
        self.access_count += 1
        return super().__getitem__(key)

Caution: built-in types (dict, list, str) are implemented in C — overriding may not intercept all code paths. For full control, wrap (composition) or use collections.UserDict.


__init_subclass__ — hook on subclass creation#

class PluginBase:
    plugins: dict[str, type] = {}

    def __init_subclass__(cls, name: str = "", **kwargs):
        super().__init_subclass__(**kwargs)
        if name:
            PluginBase.plugins[name] = cls

class EmailPlugin(PluginBase, name="email"):
    pass

class SMSPlugin(PluginBase, name="sms"):
    pass

PluginBase.plugins   # {"email": EmailPlugin, "sms": SMSPlugin}

Useful for registration patterns without metaclasses.


When not to inherit#

Situation Better approach
Reusing one utility method Composition or module function
"Extending" unrelated class for one feature Wrapper
Deep tree (>3 levels) Flatten, use composition
Changing built-in semantics heavily Don't subclass list/dict casually
Cross-cutting concerns (logging, timing) Decorators

Inheritance with ABCs#

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class Rectangle(Shape):
    def __init__(self, w: float, h: float):
        self.w, self.h = w, h

    def area(self) -> float:
        return self.w * self.h

Abstract methods must be implemented in concrete subclasses — see Abstract Base Classes.


Interview patterns#

LeetCode-style tree/list nodes — usually no inheritance#

class TreeNode:
    def __init__(self, val: int = 0, left: "TreeNode | None" = None, right: "TreeNode | None" = None):
        self.val = val
        self.left = left
        self.right = right

Design question — exception hierarchy#

class AppError(Exception):
    """Base for all app errors."""

class ValidationError(AppError):
    pass

class NotFoundError(AppError):
    pass

Exception hierarchies are one of the best uses of single inheritance in Python.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
Forgetting super().__init__() Parent state uninitialized Always call super in __init__
Hardcoding parent class in MRO Breaks with multiple inheritance Use super()
Deep inheritance Fragile, hard to debug Composition + shallow trees
Mixin without super() Skips other mixin init Cooperative super().__init__
Subclassing list/dict casually Some operations bypass overrides UserDict, composition
Multiple inheritance for code reuse only Diamond problems, confusion Composition
Wrong base order in mixins Unexpected MRO Mixins first, base last

Mental model checklist#

  1. What does super() actually refer to in multiple inheritance?
  2. When is composition preferred over inheritance?
  3. What is a mixin, and how should it call super()?
  4. Why doesn't Python auto-call parent __init__?
  5. How does Python resolve the diamond problem?

What's next#

Topic Page
MRO deep dive Method Resolution Order
Polymorphism Polymorphism
ABCs Abstract Base Classes
Intermediate OOP Classes and OOP Basics