Python Inheritance

Master Python inheritance: single and multiple inheritance, super(), MRO, abstract base classes, and the mixin pattern.

Intermediate 13 min read 10 examples

Single Inheritance

A child class inherits all attributes and methods from its parent and can override or extend them.

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

    def speak(self):
        return f"{self.name} says {self.sound}!"

    def __repr__(self):
        return f"{type(self).__name__}({self.name!r})"

# Dog inherits from Animal
class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")  # call parent __init__

    def fetch(self, item):
        return f"{self.name} fetches the {item}!"

class Cat(Animal):
    def __init__(self, name):
        super().__init__(name, "Meow")

    # Override parent method
    def speak(self):
        return f"{self.name} says {self.sound}... and ignores you."

d = Dog("Buddy")
c = Cat("Whiskers")

print(d.speak())    # Buddy says Woof!
print(c.speak())    # Whiskers says Meow... and ignores you.
print(d.fetch("ball"))  # Buddy fetches the ball!

# Inheritance checks
print(isinstance(d, Dog))       # True
print(isinstance(d, Animal))    # True  - d is also an Animal
print(isinstance(d, Cat))       # False
print(issubclass(Dog, Animal))  # True
print(issubclass(Dog, Cat))     # False

# All classes implicitly inherit from object
print(issubclass(Animal, object))   # True
print(d.__class__.__mro__)
# (, , )

super()

super() gives you access to the parent class in the MRO chain. It is essential for cooperative multiple inheritance.

Python
class Vehicle:
    def __init__(self, make, model, year):
        self.make  = make
        self.model = model
        self.year  = year

    def info(self):
        return f"{self.year} {self.make} {self.model}"

class Car(Vehicle):
    def __init__(self, make, model, year, doors=4):
        super().__init__(make, model, year)     # initialize parent
        self.doors = doors

    def info(self):
        base = super().info()                   # call parent method
        return f"{base} ({self.doors}-door)"

class ElectricCar(Car):
    def __init__(self, make, model, year, range_km, doors=4):
        super().__init__(make, model, year, doors)  # initialize Car
        self.range_km = range_km

    def info(self):
        base = super().info()
        return f"{base} - EV ({self.range_km}km range)"

ec = ElectricCar("Tesla", "Model 3", 2024, 490)
print(ec.info())
# 2024 Tesla Model 3 (4-door) - EV (490km range)

# super() in class methods
class Base:
    @classmethod
    def create(cls, **kwargs):
        obj = cls.__new__(cls)
        return obj

class Child(Base):
    def __init__(self, x):
        self.x = x

    @classmethod
    def create_with_default(cls):
        obj = super().create()  # calls Base.create
        obj.x = 0
        return obj

Multiple Inheritance

Python
class Flyable:
    def fly(self):
        return f"{self.__class__.__name__} is flying"

class Swimmable:
    def swim(self):
        return f"{self.__class__.__name__} is swimming"

class Duck(Flyable, Swimmable):
    def quack(self):
        return "Quack!"

d = Duck()
print(d.fly())      # Duck is flying
print(d.swim())     # Duck is swimming
print(d.quack())    # Quack!

# All inherited methods are available
print(isinstance(d, Flyable))    # True
print(isinstance(d, Swimmable))  # True

# Diamond inheritance - the classic problem
class A:
    def greet(self):
        return "Hello from A"

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

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

class D(B, C):
    pass

d = D()
print(d.greet())    # B + C + Hello from A
# MRO: D -> B -> C -> A
# super() in B delegates to C (not A!), ensuring A is called once

print(D.__mro__)
# (, , , , )

Method Resolution Order (MRO)

Python uses the C3 linearization algorithm to determine the order in which classes are searched for methods.

Python
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

# View MRO
print(D.__mro__)
# (, , , , )

print(D.mro())   # same, returns list

# Method lookup follows MRO left to right
class A:
    def method(self): return "A"

class B(A):
    def method(self): return "B"

class C(A):
    def method(self): return "C"

class D(B, C): pass
class E(C, B): pass     # different order changes MRO

print(D().method())     # B  (D -> B comes first)
print(E().method())     # C  (E -> C comes first)

# Cooperative multiple inheritance - all super() calls must pass args
class Base:
    def __init__(self):
        super().__init__()     # important: always call super().__init__()

class LogMixin:
    def __init__(self):
        super().__init__()
        print(f"LogMixin init for {type(self).__name__}")

class TimestampMixin:
    def __init__(self):
        super().__init__()
        import datetime
        self.created_at = datetime.datetime.now()

class Service(LogMixin, TimestampMixin, Base):
    def __init__(self, name):
        self.name = name
        super().__init__()  # triggers cooperative chain

s = Service("MyService")
# LogMixin init for Service
print(s.created_at)     # 2024-xx-xx ...

Abstract Classes

Abstract classes define an interface that subclasses must implement. You cannot instantiate an abstract class directly.

Python
from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base class for all shapes."""

    @abstractmethod
    def area(self) -> float:
        """Calculate area - must be implemented by subclasses."""
        ...

    @abstractmethod
    def perimeter(self) -> float:
        """Calculate perimeter - must be implemented."""
        ...

    def describe(self):
        """Concrete method - available to all subclasses."""
        return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"

# Cannot instantiate abstract class
try:
    s = Shape()     # TypeError: Can't instantiate abstract class
except TypeError as e:
    print(e)

# Concrete subclass must implement ALL abstract methods
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width  = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    PI = 3.14159265

    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return self.PI * self.radius ** 2

    def perimeter(self):
        return 2 * self.PI * self.radius

r = Rectangle(4, 5)
c = Circle(3)
print(r.describe())     # Rectangle: area=20.00, perimeter=18.00
print(c.describe())     # Circle: area=28.27, perimeter=18.85

# Polymorphism - treat all shapes uniformly
shapes = [Rectangle(4, 5), Circle(3), Rectangle(2, 8)]
total_area = sum(s.area() for s in shapes)
print(f"Total area: {total_area:.2f}")

# Abstract class with some concrete methods
class DataProcessor(ABC):
    @abstractmethod
    def fetch(self) -> list:
        ...

    @abstractmethod
    def transform(self, data: list) -> list:
        ...

    def process(self):          # template method pattern
        data = self.fetch()
        return self.transform(data)

Mixins

Mixins are small, focused classes that add a specific capability through multiple inheritance without being a base class.

Python
import json

# Mixin: adds JSON serialization capability
class JsonMixin:
    def to_json(self):
        return json.dumps(self.__dict__, default=str)

    @classmethod
    def from_json(cls, json_str):
        data = json.loads(json_str)
        return cls(**data)

# Mixin: adds comparison by a key
class ComparableMixin:
    def _compare_key(self):
        raise NotImplementedError

    def __lt__(self, other): return self._compare_key() < other._compare_key()
    def __le__(self, other): return self._compare_key() <= other._compare_key()
    def __eq__(self, other): return self._compare_key() == other._compare_key()
    def __gt__(self, other): return self._compare_key() > other._compare_key()

# Mixin: adds repr based on __dict__
class ReprMixin:
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{type(self).__name__}({attrs})"

# Primary class using mixins
class Product(JsonMixin, ComparableMixin, ReprMixin):
    def __init__(self, name, price, stock):
        self.name  = name
        self.price = price
        self.stock = stock

    def _compare_key(self):
        return self.price

p1 = Product("Widget", 9.99,  100)
p2 = Product("Gadget", 24.99, 50)
p3 = Product("Donut",  1.99,  200)

print(repr(p1))         # Product(name='Widget', price=9.99, stock=100)
print(p1.to_json())     # {"name": "Widget", "price": 9.99, "stock": 100}

products = [p1, p2, p3]
for p in sorted(products):     # uses ComparableMixin
    print(f"{p.name}: ${p.price}")
Mixin naming convention

By convention, mixin class names end in Mixin to signal that they are not meant to stand alone. Mixins should not define __init__ (or always call super().__init__()), should not inherit from the primary class hierarchy, and should be focused on one small capability. This keeps multiple inheritance manageable.

Frequently Asked Questions

super() returns a proxy object that delegates method calls to the parent class in the MRO chain. In __init__, calling super().__init__(...) runs the parent's initializer. In Python 3, super() with no arguments works automatically - it uses the class in which the method is defined and self. Always call super().__init__() in every class in a hierarchy to ensure cooperative initialization.

Method Resolution Order (MRO) is the order Python uses to look up methods and attributes in a class hierarchy. Python uses the C3 linearization algorithm to compute it. You can see the MRO with ClassName.__mro__ or ClassName.mro(). For diamond inheritance (class D inherits from B and C, which both inherit from A), MRO ensures each class appears only once and is visited in a consistent order.

Multiple inheritance is a powerful but complex feature. Use it sparingly. The recommended pattern is to mix in one primary base class with one or more "mixin" classes that add specific capabilities (like logging, serialization, or authentication). Mixins should be narrow in scope and not inherit from the primary hierarchy. Avoid deep diamond inheritance - it makes the MRO hard to reason about.

An abstract class (using ABC and @abstractmethod) defines an interface that subclasses must implement. You cannot instantiate an abstract class directly - it is a contract: any subclass must provide implementations for all abstract methods, or it too will be abstract. Use abstract classes when designing frameworks or plugin systems where you want to guarantee that subclasses implement specific methods.