Inheritance, super() and composition

Python · Practical · 10 min

Frameworks and exception hierarchies are built on inheritance; good design knows its limits.

class Dog(Animal): makes Dog a subclass of Animal. It inherits every attribute and method, and can override any of them by defining a method with the same name. Attribute lookup checks the instance, then the class, then its parents.

To extend rather than replace parent behaviour, call super(): super().__init__(name) runs the parent's initialiser on this object. Use super() instead of naming the parent class directly; it follows the method resolution order (MRO), which matters when classes use multiple inheritance. You can inspect it with Dog.__mro__ or Dog.mro().

isinstance(obj, Animal) is true for instances of Animal and of any subclass, and issubclass(Dog, Animal) checks the classes. Every class ultimately inherits from object.

Python supports multiple inheritance (class C(A, B)), typically used for small mixins that add one capability, like JsonMixin providing to_json(). Keep mixins narrow and always call super() so that cooperative chains work.

To declare an interface that subclasses must implement, inherit from abc.ABC and mark methods with @abstractmethod; such a class can't be instantiated until every abstract method is overridden.

Inheritance means "is-a" and couples the subclass tightly to the parent's internals. When you just want to reuse functionality, composition — holding another object as an attribute and delegating to it — is usually more flexible ("has-a"). A Car has an Engine; it isn't one. Deep inheritance hierarchies are a code smell in Python.

Example

from abc import ABC, abstractmethod
class Shape(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def area(self): ...
    def describe(self):
        return f"{self.name} with area {self.area():.1f}"
class Rectangle(Shape):
    def __init__(self, w, h):
        super().__init__("rectangle")
        self.w, self.h = w, h
    def area(self):
        return self.w * self.h
class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)
        self.name = "square"
sq = Square(3)
print(sq.describe())                 # square with area 9.0
print(isinstance(sq, Shape))         # True
print([c.__name__ for c in Square.__mro__])
# ['Square', 'Rectangle', 'Shape', 'ABC', 'object']
# Shape("x") -> TypeError: can't instantiate abstract class

Remember

  • Subclasses inherit and can override; super() calls the next class in the MRO
  • abc.ABC + @abstractmethod define interfaces that must be implemented
  • Prefer composition (has-a) when you only need to reuse behaviour

Watch out for

  • Forgetting to call super().__init__() so parent attributes are never set.

Check yourself

  1. Why prefer `super().__init__()` over `Parent.__init__(self)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/tutorial/classes.html#inheritance