Skip to Content

OOP: Classes & Inheritance

When to use

Bundle state + behaviour into a custom type that participates in Python’s polymorphic dispatch.

Analogy

A class is a blueprint; an instance is a house; inheritance is a derived blueprint with modifications.

Data-flow diagram

class Animal:          class Dog(Animal):
  def speak(self):       overrides:
    return 'generic'        def speak(self):
                                  return 'woof'
Animal()=='generic'; Dog()=='woof'
MRO: Dog -> Animal -> object

Deep explanation

Instance attrs live in self.__dict__; class attrs are SHARED across instances. Inheritance uses the C3 linearisation to compute MRO, so diamond inheritance is predictable. Composition (‘has-a’) is generally preferred over inheritance (‘is-a’); use inheritance only for true Liskov substitution. super() chains parent calls; abc.ABC declares interface contracts.

Examples

Example 1

class Counter:
    def __init__(self, start=0):
        self._n = start
    def inc(self, by=1):
        self._n += by
c = Counter(); c.inc(); c.inc(5)

canonical class; _n signals ‘intended private by convention’.

Example 2

class Dog(Animal):
    def speak(self): return 'woof'
print(Dog.__mro__)   # (Dog, Animal, object)

subclass overrides; mro shows resolution order.

Example 3

from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...
class Square(Shape):
    def __init__(self, s): self.s = s
    def area(self): return self.s * self.s

ABC forbids Shape() instantiating; Square must implement area.

Common mistake

Overriding __init__ and forgetting to call super().__init__(...) - parent state never init’d; AttributeError later. Avoid deep inheritance chains; prefer composition.

Key takeaway

Favour composition; inheritance only for true IS-A; always super().__init__(); ABC for interface contracts; remember class attrs are shared.

Production Failure Playbook

Failure scenario 1: class-attr-shadowing-instance

Failure scenario 2: diamond-mro-surprise