
Introduction Link to heading
In the second part of this series we covered Python classes and OOP, and specifically noted that multiple inheritance, something C# deliberately avoids at the class level, would be getting its own article. Here we are! We will work through inheritance from the ground up, starting with single inheritance before moving on to multiple inheritance, which Python handles in a way that is both powerful and occasionally surprising.
We will also be addressing something raised in the first article: the absence of interfaces in Python. We’ll see how abstract base classes step in to fill a similar role, and Python’s preference for duck typing means interfaces are not as essential.
Single Inheritance Link to heading
The basic syntax for inheritance in Python will feel familiar enough. Where C# places the base class name after a colon, Python places it in parentheses following the class name:
// C#
class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void Speak()
{
Console.WriteLine($"{Name} makes a sound.");
}
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} barks.");
}
}# Python
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound.")
class Dog(Animal):
def speak(self):
print(f"{self.name} barks.")
dog = Dog("Ollie")
dog.speak() # Ollie barks.You may have noticed a few things. First, there is no equivalent of virtual in Python: any method can be overridden in a subclass without any special declaration on the parent. This reflects Python’s more permissive, dynamic nature: the assumption is that the developer knows what they are doing, and preventing overrides by default would get in the way more often than it would help.
Second, there is no override keyword either. You simply define a method with the same name in the subclass and Python will use it in preference to the parent’s version. There is no compiler warning if you accidentally shadow a method with a typo.
Calling the Parent Class with super() Link to heading
Python’s equivalent of C#’s base keyword is super(). A common use is calling the parent constructor from the child’s init, especially when the subclass adds its own attributes on top of the parent’s:
// C#
class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, string breed) : base(name)
{
Breed = breed;
}
public override void Speak()
{
Console.WriteLine($"{Name} ({Breed}) barks.");
}
}# Python
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def speak(self):
print(f"{self.name} ({self.breed}) barks.")
dog = Dog("Ollie", "Labrador")
dog.speak() # Ollie (Labrador) barks.super().init(name) calls the parent’s’ init and forwards the name argument, exactly as base(name) does in C#. You can use super() to call any parent method, not just the constructor:
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def speak(self):
super().speak() # Call the parent's speak() first
print(f"...and {self.name} barks specifically.")Multiple Inheritance Link to heading
This is where Python diverges quite significantly from C#. C# supports multiple inheritance of interfaces (a class can implement as many as it likes), but not multiple inheritance of classes; a class can only extend one. Python, by contrast, allows a class to inherit from multiple parent classes directly:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound.")
class Flyable:
def fly(self):
print("Flying!")
class Swimmable:
def swim(self):
print("Swimming!")
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
super().__init__(name)
def speak(self):
print(f"{self.name} quacks.")
donald = Duck("Donald")
donald.speak() # Donald quacks.
donald.fly() # Flying!
donald.swim() # Swimming!This is a useful pattern in Python, and is seen commonly as an implementataion called mixins, in which small, focused classes add a specific capability without being intended for standalone instantiation. Flyable and Swimmable above are essentially mixins: they are not that meaningful on their own, but they compose cleanly into a concrete class. The convention is to name mixin classes with a descriptive adjective or an explicit Mixin suffix.
That said, multiple inheritance can get complicated when two parent classes define a method with the same name. When a conflict arises, which one wins? This is the classic diamond problem, and Python resolves it through an algorithm called the Method Resolution Order (MRO).
The Method Resolution Order (MRO) Link to heading
The MRO defines the order in which Python searches the class hierarchy for a method. It uses the C3 linearisation algorithm, which makes lookup order predictable. You can inspect the MRO of any class via its mro attribute or the mro() method:
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):
pass
d = D()
d.hello() # Hello from B
print(D.__mro__)Hello from B
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)Python searches left-to-right and depth-first, but with the guarantee that no class appears before its own subclasses. It is important to use super() consistently, especially in a multiple inheritance scenario. That means avoiding calling parent methods by name directly! Note also that all Python classes ultimately inherit from object, the root of the entire class hierarchy, which is why it always appears at the end of the MRO.
Abstract Base Classes Link to heading
In the first article we noted that Python has no interfaces out of the box. The closest equivalent is an Abstract Base Class (ABC), provided by the abc module in the standard library. An ABC lets you define a contract: a set of methods that any concrete subclass must implement, raising a TypeError if it does not.
// C#
public interface IShape
{
double Area();
double Perimeter();
}
public class Circle : IShape
{
private double _radius;
public Circle(double radius) { _radius = radius; }
public double Area() => Math.PI * _radius * _radius;
public double Perimeter() => 2 * Math.PI * _radius;
}# Python
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
c = Circle(5)
print(f"Area: {c.area():.2f}") # Area: 78.54
print(f"Perimeter: {c.perimeter():.2f}") # Perimeter: 31.42If you attempt to instantiate Shape directly (in a class or any subclass that does not implement all of the abstract methods), you’ll end up raising a TypeError at runtime, much like a compile-time error in C# when a class claims to implement an interface but leaves methods unimplemented:
shape = Shape()
# TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeterDuck Typing and Polymorphism Link to heading
There’s a fundamental and philosophical difference between C# Python’s ABCs. Python does not rely on ABCs as the primary driver of polymorphism. Instead, Python encourages duck typing: if an object has the methods and attributes you need, you can use it, regardless of what class it inherits from.
In simple terms, “If it walks like a duck and quacks like a duck, then it’s a duck.”
class Cat:
def speak(self):
print("Meow!")
class Robot:
def speak(self):
print("Beep boop.")
def make_it_speak(thing):
thing.speak()
make_it_speak(Dog("Ollie")) # Ollie barks.
make_it_speak(Cat()) # Meow!
make_it_speak(Robot()) # Beep boop.None of those three classes share a base class or implement a common interface, yet make_it_speak works with all of them because all that matters is that they have a speak() method. This is a direct consequence of Python’s dynamic typing, and it is an extremely natural way to write polymorphic code once you get used to it. That said, ABCs are still valuable in larger codebases where the contract helps communicate intent clearly and catches implementation mistakes early.
In the next article, we will be coming back to those peculiar double-underscore names we encountered in part two, magic methods, and also taking a deeper look at introspection: Python’s tools for examining objects at runtime. Thanks for reading!