Inheritance in Python | Python Course | Dataplexa

Inheritance in Python

One of the most powerful ideas in object-oriented programming is that a new class can be built on top of an existing one — inheriting all of its attributes and methods, and then adding or changing only what needs to be different. This is inheritance, and it is how Python lets you build specialised types without rewriting shared behaviour.

This lesson covers single inheritance, method overriding, super(), multiple inheritance, mixins, and the Method Resolution Order — the complete picture for designing clean, reusable class hierarchies.

Basic Inheritance

To inherit from a class, put the parent class name in parentheses after the child class name. The child automatically receives every attribute and method from the parent.

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

    def describe(self):
        print(f"{self.name} is a {self.species}.")

    def breathe(self):
        print(f"{self.name} breathes air.")

# Dog inherits everything from Animal
class Dog(Animal):
    def bark(self):            # new method — only exists on Dog
        print(f"{self.name} says: Woof!")

rex = Dog("Rex", "Canis lupus familiaris")

rex.describe()   # inherited from Animal
rex.breathe()    # inherited from Animal
rex.bark()       # defined on Dog only

print(isinstance(rex, Dog))     # True
print(isinstance(rex, Animal))  # True — rex IS an Animal too
print(issubclass(Dog, Animal))  # True — Dog IS a subclass of Animal
Rex is a Canis lupus familiaris. Rex breathes air. Rex says: Woof! True True True
  • The parent class is also called the base class or superclass. The child is the derived class or subclass.
  • A child instance passes isinstance() checks for both its own class and every parent class in its hierarchy.
  • issubclass(Child, Parent) checks the class relationship without needing an instance.
  • A child class that adds no new code can use pass — it still fully inherits the parent.

Method Overriding

When a child class defines a method with the same name as a parent method, the child's version overrides the parent's. Python always calls the most specific version — the one closest to the actual object's type.

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

    def speak(self):
        return f"{self.name} makes a sound."

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

class Dog(Animal):
    def speak(self):                    # overrides Animal.speak
        return f"{self.name} says: Woof!"

class Cat(Animal):
    def speak(self):                    # overrides Animal.speak
        return f"{self.name} says: Meow!"

class Fish(Animal):
    pass                                # no override — uses Animal.speak

animals = [Dog("Rex"), Cat("Luna"), Fish("Nemo"), Dog("Buddy")]

for a in animals:
    print(a.speak())

# Polymorphism in a real scenario — total sound count
print(f"\n{len(animals)} animals, all speak via the same interface")
Rex says: Woof! Luna says: Meow! Nemo makes a sound. Buddy says: Woof! 4 animals, all speak via the same interface

The super() Function

super() gives access to the parent class from inside a child method. Most commonly used in __init__ to call the parent's initialiser before adding the child's own setup, but works in any method.

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

    def describe(self):
        print(f"{self.name} ({self.species})")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Canis lupus familiaris")  # run parent __init__ first
        self.breed = breed                                # then add own attributes

    def describe(self):
        super().describe()             # call parent's describe() first
        print(f"  Breed: {self.breed}")

class GuideDog(Dog):
    def __init__(self, name, breed, owner):
        super().__init__(name, breed)  # runs Dog.__init__ (which runs Animal.__init__)
        self.owner = owner

    def describe(self):
        super().describe()             # Dog.describe → Animal.describe → adds breed
        print(f"  Guide dog for: {self.owner}")

g = GuideDog("Max", "Labrador", "Alice")
g.describe()
Max (Canis lupus familiaris) Breed: Labrador Guide dog for: Alice
  • super().__init__(...) — always call this when the child adds new attributes so the parent's attributes are set up first.
  • super() with no arguments works in Python 3 — no need to pass the class name.
  • In a chain like GuideDog → Dog → Animal, each super() call follows the MRO automatically.
  • Forgetting super().__init__() means the parent's attributes are never created — a common bug.

Real-World Example — Payment System

class Payment:
    def __init__(self, amount, currency="USD"):
        self.amount   = amount
        self.currency = currency

    def process(self):
        raise NotImplementedError("Subclasses must implement process()")

    def receipt(self):
        name = self.__class__.__name__
        print(f"  Receipt: {self.currency} {self.amount:.2f} via {name}")

class CreditCard(Payment):
    def __init__(self, amount, last_four, currency="USD"):
        super().__init__(amount, currency)
        self.last_four = last_four

    def process(self):
        print(f"Charging {self.currency} {self.amount:.2f} to card ****{self.last_four}")
        self.receipt()

class PayPal(Payment):
    def __init__(self, amount, email, currency="USD"):
        super().__init__(amount, currency)
        self.email = email

    def process(self):
        print(f"PayPal transfer {self.currency} {self.amount:.2f} → {self.email}")
        self.receipt()

class BankTransfer(Payment):
    def __init__(self, amount, iban, currency="USD"):
        super().__init__(amount, currency)
        self.iban = iban

    def process(self):
        print(f"Bank transfer {self.currency} {self.amount:.2f} → {self.iban[-4:].rjust(8,'*')}")
        self.receipt()

payments = [
    CreditCard(49.99, "4242"),
    PayPal(19.99, "alice@example.com"),
    BankTransfer(299.00, "GB29NWBK60161331926819", "GBP"),
]

for p in payments:
    p.process()
    print()
Charging USD 49.99 to card ****4242 Receipt: USD 49.99 via CreditCard PayPal transfer USD 19.99 → alice@example.com Receipt: USD 19.99 via PayPal Bank transfer GBP 299.00 → ****6819 Receipt: GBP 299.00 via BankTransfer
  • Raising NotImplementedError in the parent's method enforces that every subclass must override it.
  • self.__class__.__name__ returns the actual runtime class name — "CreditCard" or "PayPal" — even when called from the parent method.
  • Adding a new payment method like ApplePay requires only writing the new class — nothing else changes.

Mixins — Composable Behaviour

A mixin is a small class that adds one specific capability to other classes without forming a full parent-child hierarchy. Mixins are used with multiple inheritance to compose behaviour like assembling building blocks.

# Mixins — add specific capabilities without full inheritance

class LogMixin:
    """Add log() to any class."""
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")

class SerializeMixin:
    """Add to_dict() to any class."""
    def to_dict(self):
        return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}

class ValidateMixin:
    """Add validate() — override in subclass for custom rules."""
    def validate(self):
        raise NotImplementedError

class Product(LogMixin, SerializeMixin, ValidateMixin):
    def __init__(self, name, price, stock):
        self.name  = name
        self.price = price
        self.stock = stock

    def validate(self):
        errors = []
        if self.price < 0:   errors.append("price must be non-negative")
        if self.stock < 0:   errors.append("stock must be non-negative")
        if not self.name:    errors.append("name required")
        return errors

p = Product("Notebook", 4.99, 50)
p.log("Product created")
print(p.to_dict())

errors = p.validate()
p.log(f"Valid: {not errors}" if not errors else f"Errors: {errors}")
[Product] Product created {'name': 'Notebook', 'price': 4.99, 'stock': 50} [Product] Valid: True
  • Mixins have no __init__ — they only add methods.
  • By convention, mixin class names end in Mixin.
  • Mixins are the clean alternative to copying the same utility methods into dozens of unrelated classes.

Multiple Inheritance and MRO

Python allows a class to inherit from more than one parent. When method lookup happens, Python follows the Method Resolution Order — a deterministic sequence computed by the C3 linearisation algorithm.

class A:
    def hello(self): print("Hello from A")

class B(A):
    def hello(self): print("Hello from B")

class C(A):
    def hello(self): print("Hello from C")

class D(B, C):   # diamond: D → B → C → A → object
    pass

d = D()
d.hello()              # B is found first in MRO

print(D.__mro__)       # full resolution order
print(D.mro())         # same, as a list

# issubclass works across the full hierarchy
print(issubclass(D, A))   # True — A is in D's MRO
Hello from B (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>] True
  • Python searches left to right through the MRO and uses the first match it finds.
  • Every Python class ultimately inherits from object — it is always last in the MRO.
  • ClassName.__mro__ (tuple) or ClassName.mro() (list) show the full resolution order.
  • The diamond problem — two parents sharing a common grandparent — is resolved automatically by Python's MRO.

Quick Reference Table

ConceptWhat It DoesKey Syntax
InheritanceChild receives all parent attributes and methodsclass Child(Parent):
Method overrideChild replaces parent's method with its own versionRedefine same method name in child
super()Calls the parent's version of a methodsuper().__init__(...)
Multiple inheritanceInherit from more than one parentclass C(A, B):
MixinSmall class adding one specific capabilityclass LogMixin: (no __init__)
MROOrder Python searches classes for methodsClassName.__mro__
issubclass()Check class relationship without an instanceissubclass(Dog, Animal)

Practice

What syntax makes a class inherit from a parent class?



What do you call inside a child's __init__ to run the parent's __init__?



What does isinstance(obj, ParentClass) return when obj is an instance of a child class?



What is a mixin in the context of multiple inheritance?



What class does every Python class ultimately inherit from?



Which built-in function checks a class relationship without needing an instance?



Quick Quiz

When a child class defines a method with the same name as a parent method, what happens?





What is the purpose of raising NotImplementedError in a parent class method?





In the MRO for class D(B, C), which class is searched first after D?





What does super() return in Python 3?





Which of the following best describes polymorphism as seen through inheritance?





What is a key characteristic of a mixin class?





NEXT UP
Polymorphism in Python
One interface, many forms — making different object types respond to the same method call in their own way, including duck typing and operator overloading.