Inheritance
Inheritance lets a class reuse and extend another class's behavior, forming hierarchies from a base class down through single, multilevel, hierarchical, and even multiple parent lines.
Inheritance lets you define a new class (a subclass, or derived class) in terms of an existing one (a base class, or superclass), reusing its attributes and methods while adding or overriding what's different. Python supports both single inheritance (one parent) and multiple inheritance (several parents at once), and resolves which method actually runs through an attribute-lookup process guided by the Method Resolution Order.
Base Class and Subclass
A base class defines shared behavior; a subclass is declared with the base class in parentheses and automatically has access to everything the base class defines, without rewriting it.
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return f"{self.name} makes a sound"
class Dog(Animal): # Dog inherits from Animal
pass
d = Dog("Rex")
print(d.speak()) # 'Rex makes a sound' -- inherited unchangedOverriding Methods
A subclass can redefine a method with the same name to change its behavior; the subclass's version takes priority over the base class's during lookup.
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} barks"
d = Dog("Rex")
print(d.speak()) # 'Rex barks' -- Dog's override winssuper(): Calling the Parent's Implementation
super() gives access to the base class's version of a method from within an override, so you can extend rather than fully replace the inherited behavior -- most commonly in __init__.
super().__init__(name) delegates to Animal.__init__ rather than duplicating self.name = name, keeping the two classes' logic from drifting apart.
super() in Python 3 needs no arguments inside an instance method; it looks up the next class in the MRO after the current class, relative to the instance's actual type -- see the Method Resolution Order topic for the full mechanics, especially under multiple inheritance.
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) # let Animal set self.name
self.breed = breed
d = Dog("Rex", "Labrador")
print(d.name, d.breed) # Rex LabradorSingle Inheritance
Single inheritance means a class has exactly one direct base class. This is the simplest and most common shape and maps closely to inheritance in languages like Java (which allows only single class inheritance).
class Vehicle:
def __init__(self, wheels: int) -> None:
self.wheels = wheels
class Car(Vehicle): # single inheritance: one parent
def __init__(self) -> None:
super().__init__(wheels=4)Multilevel Inheritance
Multilevel inheritance is a chain: C inherits from B, which inherits from A. Each level can add or override behavior; a subclass at the bottom of the chain has access to everything defined at every level above it.
class Animal:
def eat(self) -> str:
return "eating"
class Mammal(Animal):
def breathe(self) -> str:
return "breathing air"
class Dog(Mammal): # Dog -> Mammal -> Animal
def bark(self) -> str:
return "barking"
d = Dog()
print(d.eat(), d.breathe(), d.bark()) # all three levels are reachableHierarchical Inheritance
Hierarchical inheritance is the reverse shape: multiple subclasses share one common base class. Each subclass independently extends the shared base.
class Shape:
def describe(self) -> str:
return "a shape"
class Circle(Shape):
def area(self, r: float) -> float:
return 3.14159 * r ** 2
class Square(Shape):
def area(self, s: float) -> float:
return s ** 2
# Circle and Square both inherit describe() from Shape independentlyMultiple Inheritance
Python allows a class to inherit from more than one base class directly -- multiple inheritance. This is more flexible than single-parent-only languages, but also more complex: it introduces the question of which base's method wins when several bases define the same name, which Python answers via its Method Resolution Order (covered in depth in that topic).
Base-class order in the class statement matters: it's the primary input to the linearization algorithm that decides lookup order.
class Flyer:
def move(self) -> str:
return "flies"
class Swimmer:
def move(self) -> str:
return "swims"
class Duck(Flyer, Swimmer): # multiple inheritance
pass
print(Duck().move()) # 'flies' -- Flyer is listed first, so it wins per MROMethod Lookup and Attribute Resolution
When you access obj.attr, Python searches the instance's __dict__ first, then walks the class's Method Resolution Order (the class itself, then its bases, in a well-defined linearized sequence) until it finds attr, raising AttributeError if no class in the chain defines it. This is the same underlying mechanism whether there's one base class or several -- multiple inheritance just makes the search order more than a simple straight line.
class A:
def greet(self):
return "A"
class B(A):
pass
b = B()
print(b.greet()) # 'A' -- found by walking up from B to A
print(B.__mro__) # (B, A, object) -- the search orderisinstance() and issubclass()
isinstance(obj, Cls) checks whether obj's type is Cls or any subclass of it; issubclass(Sub, Base) checks the class relationship directly. Both walk the inheritance hierarchy, so a Dog instance is also considered an instance of Animal.
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True -- Dog is a subclass of Animal
print(issubclass(Dog, Animal)) # TruePractical Usage: Extending Behavior Without Duplication
The practical value of inheritance is avoiding copy-pasted code across related classes: shared state and behavior live once, in the base class, and each subclass expresses only what makes it different.
class Employee:
def __init__(self, name: str, salary: float) -> None:
self.name = name
self.salary = salary
def annual_pay(self) -> float:
return self.salary * 12
class Manager(Employee):
def __init__(self, name: str, salary: float, bonus: float) -> None:
super().__init__(name, salary)
self.bonus = bonus
def annual_pay(self) -> float:
return super().annual_pay() + self.bonusEdge Cases
Every class implicitly inherits from object, even with no explicit base listed (class Foo: is the same as class Foo(object):). A subclass that defines __init__ without calling super().__init__() will not run the base class's initialization at all -- the base class's setup code simply doesn't execute unless explicitly invoked.
class Base:
def __init__(self):
self.ready = True
class Sub(Base):
def __init__(self):
pass # forgot super().__init__()
s = Sub()
print(hasattr(s, "ready")) # False -- Base.__init__ never ranKey Takeaways
A subclass inherits attributes and methods from its base class and can override any of them.
super() calls the next implementation in the MRO, letting an override extend rather than replace inherited behavior.
Single inheritance is one parent; multilevel is a chain of parents; hierarchical is many children sharing one parent; multiple inheritance is one class with several direct parents.
Attribute lookup walks the instance, then the class's Method Resolution Order, raising AttributeError only if nothing in the chain defines the name.
Forgetting super().__init__() in an overriding constructor silently skips the base class's setup.
class Dog(Animal): def __init__(self, name, breed): self.name = name # duplicates Animal's logic self.breed = breed class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed = breed
Duplicating the base class's initialization logic instead of calling super().__init__() means the two implementations can drift apart as the base class changes.
class Sub(Base): def __init__(self): pass # Base.__init__ never runs class Sub(Base): def __init__(self): super().__init__()
An overriding __init__ that never calls super().__init__() silently skips whatever setup the base class performs, often leaving expected attributes missing.
class Duck(Swimmer, Flyer): pass # assumes Flyer.move() wins regardless of listed order class Duck(Flyer, Swimmer): pass # base order in the class statement determines MRO precedence -- list the priority parent first
Which overridden method 'wins' under multiple inheritance depends on the order bases are listed in the class statement, not on some fixed rule about which base is 'more important'.
What does calling super().__init__(...) inside a subclass's __init__ accomplish?
It delegates to the base class's __init__ (or, more precisely, the next class in the MRO), running that class's initialization logic instead of duplicating it in the subclass.
What is the difference between multilevel and hierarchical inheritance?
Multilevel inheritance is a chain (A -> B -> C, each inheriting from the previous); hierarchical inheritance is multiple subclasses independently inheriting from the same single base class.
Why does Python need a Method Resolution Order at all, rather than a simple 'search each base class' rule?
Because multiple inheritance can create diamond-shaped hierarchies where more than one base class defines the same attribute; a well-defined, consistent linearized order (not an ad hoc search) is needed to determine which one is found first and to avoid processing a shared ancestor more than once.
What happens if a subclass overrides __init__ but never calls super().__init__()?
The base class's __init__ never runs, so any attributes it would have set are simply absent on the instance -- this typically surfaces later as an AttributeError when that missing attribute is accessed.
Does every Python class inherit from something, even if no base class is written explicitly?
Yes -- a class with no explicit base (class Foo:) implicitly inherits from object, the root of every class hierarchy.
In class Duck(Flyer, Swimmer), if both Flyer and Swimmer define move(), which one does Duck().move() call, and why?
Flyer's move(), because Flyer is listed first among Duck's bases -- base-class order in the class statement is the primary input to the MRO's linearization.
How do isinstance() and issubclass() relate to inheritance?
isinstance(obj, Cls) returns True if obj's type is Cls or any subclass of it (walking up the inheritance chain); issubclass(Sub, Base) checks the same class relationship directly, without needing an instance.
Write a three-level inheritance chain Vehicle -> Car -> SportsCar, where SportsCar adds a top_speed attribute and an overridden describe() that calls the parent's describe() via super() and appends the top speed.
class Vehicle: def __init__(self, wheels: int) -> None: self.wheels = wheels def describe(self) -> str: return f"a vehicle with {self.wheels} wheels" class Car(Vehicle): def __init__(self) -> None: super().__init__(wheels=4) class SportsCar(Car): def __init__(self, top_speed: int) -> None: super().__init__() self.top_speed = top_speed def describe(self) -> str: return f"{super().describe()}, top speed {self.top_speed} mph" sc = SportsCar(180) print(sc.describe())
'a vehicle with 4 wheels, top speed 180 mph' -- each super() call delegates one level up the multilevel chain, and SportsCar.describe() extends rather than replaces Vehicle.describe().
What does this print?
'B-A' -- C has no hello() of its own, so lookup finds B.hello(), which calls super().hello() to reach A.hello() and prepends 'B-' to the result.
Write a hierarchical inheritance example: a base class Shape with a method name() returning 'shape', and two subclasses Circle and Square that each override name() to return their own name, without redefining anything else Shape provides.
class Shape: def name(self) -> str: return "shape" def info(self) -> str: return f"This is a {self.name()}" class Circle(Shape): def name(self) -> str: return "circle" class Square(Shape): def name(self) -> str: return "square" print(Circle().info()) # This is a circle print(Square().info()) # This is a square
Both Circle and Square inherit info() unchanged from Shape (hierarchical inheritance: two siblings sharing one base), while each overrides only name(), and info() calls self.name() so it picks up whichever subclass's override actually runs.
class A:
def hello(self):
return "A"
class B(A):
def hello(self):
return "B-" + super().hello()
class C(B):
pass
print(C().hello())