Multiple Inheritance
Basic Usage
Python support for multiple inheritance allows a class to inherit from more than one parent class. To define a subclass that inherits from multiple base classes, list the parent classes in order inside the parentheses:
class A(B, C):
pass
In this code, class A inherits all attributes and methods from both B and C. Let's look at a more concrete example:
Suppose you run a furniture store that sells tables and chairs. We want to write a program to model these items. We could design the system like this:
First, we define a base Furniture class containing properties common to all inventory—such as ID, cost, tax rate, and material, alongside an assemble() method. Next, we create Table and Chair subclasses that inherit from Furniture. These subclasses automatically gain all properties of the base class. We then add specialized behaviors: the Table class gets a lay_tablecloth() method, and the Chair class gets an add_pillow() method. Later, we can derive more specific subclasses like DiningTable or OfficeDesk from Table. Every physical item in the store is instantiated from one of these subclasses.
Now, suppose the store introduces a hybrid piece of furniture that functions as both a table and a chair, as shown in the illustration:

We can represent this table-chair combo using multiple inheritance. The combo class should inherit from both the Table and Chair classes, acquiring the traits of both parent classes:
# Base class
class Furniture:
def __init__(self, material, furniture_id, cost):
self.material = material
self.id = furniture_id
self.cost = cost
def description(self):
return f"Furniture ID: {self.id}, Main material: {self.material}, Cost: ${self.cost}."
def assemble(self):
print(f"Furniture {self.id} has been assembled.")
# Subclass
class Chair(Furniture):
def __init__(self, material, furniture_id, cost):
super().__init__(material, furniture_id, cost)
def set_number_of_legs(self, number_of_legs):
self.number_of_legs = number_of_legs
def description(self):
return super().description() + f"It has {self.number_of_legs} legs."
def add_pillow(self):
print(f"A pillow has been installed on chair {self.id}.")
# Another subclass
class Table(Furniture):
def __init__(self, material, furniture_id, cost):
super().__init__(material, furniture_id, cost)
def set_shape(self, shape):
self.shape = shape
def description(self):
return super().description() + f"Table shape: {self.shape}."
def lay_tablecloth(self):
print(f"A tablecloth has been laid on table {self.id}.")
# Multiple inheritance, inheriting from both Chair and Table
class ChairWithTableAttached(Chair, Table):
def __init__(self, material, furniture_id, cost):
super().__init__(material, furniture_id, cost)
# Override the description method
def description(self):
# Below, we directly call the parent class methods using the class name.
# We cannot use the super() function here because we need to use multiple
# parent classes, and super() can only return one of them.
chair_desc = Chair.description(self)
table_desc = Table.description(self)
return f"Chair part: {chair_desc} Table part: {table_desc}"
# Example
item = ChairWithTableAttached("solid wood", 101, 150.00)
item.set_number_of_legs(4)
item.set_shape("round")
print(item.description())
item.assemble()
item.add_pillow()
item.lay_tablecloth()
Consider these questions:
- In the
__init__constructor ofChairWithTableAttached, which class doessuper()resolve to?ChairorTable? - In the constructor of
Chair, what doessuper()resolve to? The intuitive answer might be: "SinceChairinherits only fromFurniture, itssuper()must resolve toFurniture." Interestingly, as we will see, this is not always true. We will return to these questions at the end of this section.
Problems with Multiple Inheritance
While multiple inheritance appears straightforward, it introduces several classical architectural dilemmas when classes share identical attribute or method names (known as name clashes). For our table-chair combo, resolving these clashes depends on the semantics of the shared traits:
- Preserving separate versions: For example, if both
TableandChairhave amaterialattribute, our combo might be composed of a plastic chair attached to a steel table. We would want to preserve both distinct values. - Merging into a single version: For example, if both
TableandChairdeclare acostattribute, the combo itself is sold as a single unit and should have a single, unified price. - The Diamond Problem: Since both
TableandChairinherit fromFurniture, they form a diamond-shaped inheritance tree. If an inventory program treats the combo as a genericFurnitureobject and queries itsmaterial, which execution path should the program take? Should it fall back to the baseFurnituredefaults, or resolve to one of the subclasses?
Different languages adopt different strategies to resolve these ambiguities. C++, which supported multiple inheritance early on, suffered from highly complex resolution rules that often led to hard-to-debug behaviors. Because of these challenges, C++ developers are generally advised to avoid multiple inheritance unless absolutely necessary. Many modern object-oriented languages (such as Java and C#) learned from this and explicitly banned multiple inheritance for classes.
In languages that prohibit multiple inheritance, interfaces are used instead to allow a class to take on multiple roles. While class inheritance is about borrowing implementation (code reuse), interfaces represent behavioral contracts (guaranteeing that a class provides specific methods). For example, if a TableChairCombo class inherits from Table, it is physically subclassing Table and reusing its code. If it implements a ChairInterface, it simply promises to provide the methods expected of a chair, writing the actual implementation itself.
Interfaces allow a class to support multiple functional behaviors without the risk of tightly coupled, diamond-shaped inheritance graphs. However, interfaces do not support automatic code reuse out of the box: they only specify which methods must exist, leaving the actual code to be written repeatedly inside each implementing class.
Abstract Classes
Python does not provide a native interface keyword, but it implements equivalent functionality using abstract classes.
An abstract method is a method declared in a base class that has no implementation; it serves as a contract that subclasses must implement. A class containing one or more abstract methods cannot be instantiated directly, as it is incomplete. Such a class is called an abstract class (or abstract base class).
In our furniture system, we should not be able to instantiate a generic Furniture object; any piece of inventory must be a specific type of furniture (like a chair or a table). If a developer tries to instantiate Furniture directly, it is likely a design error. Therefore, Furniture is a perfect candidate for an abstract class.
We can separate abstract contracts from concrete implementations. We use AbstractTable to define what behaviors a table must support, while concrete classes like Table provide the actual code.
In Python, we declare abstract methods using the @abstractmethod decorator, and define abstract base classes by inheriting from the built-in abc.ABC class. Here is our furniture store program refactored using abstract classes:
from abc import ABC, abstractmethod
# Abstract class Furniture, defines the attributes and methods that all furniture must have
class Furniture(ABC):
@abstractmethod
def set_material(self, material):
pass
@abstractmethod
def assemble(self):
pass
# Abstract class AbstractTable, defines the attributes and methods a table must have
class AbstractTable(Furniture):
@abstractmethod
def place_tablecloth(self):
pass
# Abstract class AbstractChair, defines the attributes and methods a chair must have
class AbstractChair(Furniture):
@abstractmethod
def place_pillow(self):
pass
# Concrete class Table, implements the attributes and methods defined by AbstractTable
class Table(AbstractTable):
def set_material(self, material):
self.material = material
print(f"Setting table material: {self.material}.")
def assemble(self):
print("The table has been assembled!")
def place_tablecloth(self):
print("A tablecloth has been laid on the table.")
# Concrete class Chair, implements the attributes and methods defined by AbstractChair
class Chair(AbstractChair):
def set_material(self, material):
self.material = material
print(f"Setting chair material: {self.material}.")
def assemble(self):
print("The chair has been assembled!")
def place_pillow(self):
print("A pillow has been placed on the chair.")
# Concrete class ChairWithTableAttached, implements all the attributes and methods of a table or chair
class ChairWithTableAttached(AbstractTable, AbstractChair):
def set_material(self, material):
self.material = material
print(f"Setting combo furniture material: {self.material}.")
def assemble(self):
print("The table-chair combo has been assembled!")
def place_tablecloth(self):
print("A tablecloth has been laid on the table-chair combo.")
def place_pillow(self):
print("A pillow has been placed on the table-chair combo.")
# Usage examples
table = Table()
table.set_material("glass")
table.assemble()
table.place_tablecloth()
chair = Chair()
chair.set_material("wood")
chair.assemble()
chair.place_pillow()
combo = ChairWithTableAttached()
combo.set_material("plastic")
combo.assemble()
combo.place_tablecloth()
combo.place_pillow()
MixIn
While abstract base classes establish contracts, they do not resolve duplicate implementation details. In the refactored code above, the set_material() method has identical implementations across all three concrete classes, yet we had to write the code repeatedly.
Fortunately, Python's dynamic nature and reliance on duck typing make multiple inheritance far cleaner than in static languages. Because Python prioritizes an object's runtime behavior over its explicit type declaration, whether a table-chair combo inherits directly from a Table class or a TableInterface is architecturally irrelevant. What matters is that the combo object implements the expected methods.
Instead of designing rigid hierarchical trees (e.g., trying to decide if a combo is a subclass of Table or Chair), Python developers use multiple inheritance for composing capabilities. This approach is implemented via Mixins.
A Mixin is a lightweight, specialized class designed to bundle reusable methods and inject them into other classes. A Mixin is not intended for standalone instantiation and typically does not declare its own constructor. By subclassing one or more Mixins, a concrete class can inherit specific, pre-written behaviors.
We can use Mixins to refactor our furniture program, breaking capabilities into independent components:
# Define MixIns
class MaterialMixin:
material = "unknown material"
def set_material(self, material):
self.material = material
print(f"Material set to: {self.material}")
class AssemblyMixin:
def assemble(self):
print("Furniture has been assembled!")
class PillowPlacementMixin:
def place_pillow(self):
print("A pillow has been placed on the chair.")
class TableclothMixin:
def place_tablecloth(self):
print("A tablecloth has been laid on the table.")
# Use MixIns to refactor the original classes
class Furniture(MaterialMixin, AssemblyMixin):
def __init__(self, id, cost, **kwargs):
# Pass **kwargs to the next class, ensuring other classes in the MRO chain (including object) work correctly
super().__init__(**kwargs)
self.id = id
self.cost = cost
class Chair(PillowPlacementMixin, Furniture):
def __init__(self, id, cost, number_of_legs=4):
# Note: This requires cooperative inheritance (mentioned below) to work perfectly
super().__init__(id, cost)
self.number_of_legs = number_of_legs
class Table(TableclothMixin, Furniture):
def __init__(self, id, cost, shape="round"):
super().__init__(id, cost)
self.shape = shape
class ChairWithTableAttached(Furniture, PillowPlacementMixin, TableclothMixin):
def __init__(self, id, cost, number_of_legs=4, shape="round"):
super().__init__(id, cost)
self.number_of_legs = number_of_legs
self.shape = shape
# Usage examples
combo = ChairWithTableAttached("101", 150.00, 4, "round")
combo.set_material("solid wood")
combo.assemble()
combo.place_pillow()
combo.place_tablecloth()
In Python best practices, Mixin classes are listed first in the class inheritance declaration. Because Mixins are designed to extend or override default behaviors, they must precede base classes. If a Mixin were listed after a base class like Furniture that already implements a method with the same name (e.g., save()), the base class method would shadow the Mixin's method, rendering the Mixin useless.
Method Resolution Order (MRO)
When an attribute or method is queried on an object, Python searches the object's class first, then traverses its parent classes in a deterministic sequence. This lookup path is called the Method Resolution Order (MRO).
For our ChairWithTableAttached class, the MRO chain resolves as follows: ChairWithTableAttached -> Furniture -> MaterialMixin -> AssemblyMixin -> PillowPlacementMixin -> TableclothMixin -> object.
Python 3 computes this chain using the C3 Linearization algorithm. The algorithm guarantees three core properties:
- Subclasses before superclasses: A class is always searched before its parents.
- Left-to-right order: Parent classes are searched in the order they are declared in the class definition.
- Monotonicity: If class precedes class in one MRO chain, it must precede in all MRO chains across the codebase.
Conceptually, C3 linearization performs a left-to-right, depth-first search, but delays common ancestors (diamond inheritance bases) until all of their subclasses have been searched first.
We can view the exact order by using print(ClassName.mro()):
print(ChairWithTableAttached.mro())
Earlier, we asked a question: which class does the super() function in the constructors of ChairWithTableAttached, Chair, and other classes return? Let's simplify a few classes and run the program below to see which class each constructor's super() returns:
class Base:
def method(self, child):
print(f"{child}'s super is Base")
class ChildA(Base):
def method(self, child):
super().method("ChildA")
print(f"{child}'s super is ChildA")
class ChildB(Base):
def method(self, child):
super().method("ChildB")
print(f"{child}'s super is ChildB")
class GrandChild(ChildA, ChildB):
def method(self, child):
super().method("GrandChild")
print(f"GrandChild is called")
gc = GrandChild()
gc.method(None) # Calls methods according to MRO
Running the program above produces the following result:
ChildB's super is Base
ChildA's super is ChildB
GrandChild's super is ChildA
GrandChild is called
We can see that "ChildA's super is ChildB". The super() function does not directly return the parent class; it returns a proxy object that delegates method calls to the next class in the MRO order after the current class.