Inheritance lets one class build on another. The new class — the subclass — gets everything from its parent for free, and can add or override behaviour.
A first inheritance
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "Some sound"
class Dog(Animal):
def speak(self) -> str:
return "Woof"
class Cat(Animal):
def speak(self) -> str:
return "Meow"
d = Dog("Rex")
c = Cat("Whiskers")
print(d.name, d.speak()) # Rex Woof
print(c.name, c.speak()) # Whiskers Meow
Dog(Animal) says “Dog inherits from Animal”. The Dog class gets __init__ from Animal for free, but overrides speak.
This is the core idea: shared behaviour goes in the parent; specific differences go in each subclass.
Calling the parent — super()
Sometimes a subclass wants to extend behaviour, not replace it. Use super() to call the parent’s version:
class Animal:
def __init__(self, name: str) -> None:
self.name = name
class Dog(Animal):
def __init__(self, name: str, breed: str) -> None:
super().__init__(name) # call Animal.__init__
self.breed = breed
d = Dog("Rex", "Labrador")
print(d.name, d.breed) # Rex Labrador
super() returns a reference to the parent class. Calling super().__init__(name) reuses the parent’s setup, then Dog.__init__ adds its own.
This pattern is everywhere in real code — __init__, file handlers, GUI widgets, framework base classes.
Inheritance vs composition
Inheritance is one way to share behaviour. The other is composition — holding an instance of another class as an attribute:
# composition
class Engine:
def start(self) -> None:
print("Engine starting")
class Car:
def __init__(self) -> None:
self.engine = Engine() # has-an Engine, not is-an Engine
def drive(self) -> None:
self.engine.start()
c = Car()
c.drive()
The rule of thumb:
- Inheritance — “a Dog is an Animal”
- Composition — “a Car has an Engine”
Composition is more flexible and easier to refactor. Modern Python style leans toward composition unless inheritance genuinely fits.
isinstance and type checks
To check at runtime whether an object is an instance of a class (including subclasses):
d = Dog("Rex", "Labrador")
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True — Dog inherits from Animal
print(isinstance(d, Cat)) # False
isinstance respects inheritance. Most of the time it’s what you want.
For strict equality, use type():
print(type(d) is Dog) # True
print(type(d) is Animal) # False
But type() is is rarely what you want. Prefer isinstance.
Method resolution
When you call d.speak(), Python looks for speak on Dog first; if not there, then Animal; then its parent, and so on. The first match wins.
This is called the MRO (method resolution order). For single inheritance, it’s a straight chain — easy to reason about. Multiple inheritance complicates it (next lesson — but kept brief).
You can see it:
print(Dog.__mro__)
# (<class 'Dog'>, <class 'Animal'>, <class 'object'>)
Every Python class ultimately inherits from object.
A common base — Exception
We saw this in Section 9. Custom exception classes inherit from Exception:
class HttpError(Exception):
pass
class NotFoundError(HttpError):
pass
try:
raise NotFoundError("missing")
except HttpError as e:
print("caught:", e)
The hierarchy lets callers catch broadly (HttpError) or narrowly (NotFoundError).
A note on multiple inheritance and ABCs
Python does allow a class to inherit from multiple parents:
class A: ...
class B: ...
class C(A, B): ...
This is usually a sign the design needs rethinking. The exception is mixin classes — small helper classes that add one specific capability. In modern Python, the cleaner alternative is usually Protocols (we’ll see in Section 13) or composition.
Abstract base classes (ABCs, in the abc module) let you declare a class that can’t be instantiated directly — only inherited. They’re used in libraries to define interfaces. We’ll keep this brief: just know they exist, and reach for Protocols first in your own code.
What’s next
Last lesson of the section — data classes, the way to write tiny classes with much less boilerplate.