# Inheritance and Composition in Python: OOP in Python, Part 2

> Inheritance and composition in Python with tested code: when to use each, how super() really works, duck typing, and why deep class hierarchies backfire.

- **Author:** Althaf Ashraf — AI Systems Engineer, Tata Consultancy Services (https://www.1stepgrow.com/authors/althaf-ashraf/)
- **Published:** Jul 28, 2026 · **Updated:** Sep 17, 2026
- **Topic:** Python & Programming · **Format:** Guide · **Read time:** 9 min
- **Canonical URL:** https://www.1stepgrow.com/articles/python-inheritance-composition/

## Key takeaways

- Inheritance models an 'is a' relationship and composition models 'has a'; most real relationships are 'has a'.
- super() follows the method resolution order, so always use it rather than naming the parent class directly.
- Duck typing means you often do not need inheritance at all, because a shared method name is enough.
- Abstract base classes make a missing method fail at instantiation rather than deep inside a call stack.
- Two levels of inheritance is usually plenty, because deep hierarchies become impossible to reason about.

Inheritance and composition in Python are the two ways classes relate to each other. Inheritance makes one class a specialised version of another, while composition builds a class from other objects it holds. Prefer composition by default and use inheritance for genuine "is a" relationships kept shallow.

The syntax is the easy part. The design mistakes cost more: naming the parent class instead of calling `super()` can silently skip a class under multiple inheritance, and a five-level hierarchy makes it hard to find which class a method comes from. This part is for readers who know [Python classes and objects from Part 1](https://www.1stepgrow.com/articles/python-classes-objects). It covers `super()` and the MRO, composition, duck typing, class and static methods and abstract base classes, with a table for choosing between them.

## How does inheritance work in Python?

A subclass gets the parent's attributes and methods, and can add or override them:

```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return f"{self.name} earns {self.salary:,}"

    def annual_cost(self):
        return self.salary * 12


class Manager(Employee):
    def __init__(self, name, salary, reports):
        super().__init__(name, salary)     # run the parent's setup
        self.reports = reports

    def describe(self):                     # override
        base = super().describe()
        return f"{base} and manages {len(self.reports)} people"
```

```python
m = Manager("Rekha", 180_000, ["Amit", "Sara"])
print(m.describe())      # Rekha earns 180,000 and manages 2 people
print(m.annual_cost())   # 2160000  - inherited unchanged
```

Two things to note. First, `super().__init__(...)` runs the parent initialiser rather than duplicating it. Second, `super().describe()` inside the override lets you extend behaviour instead of replacing it.

Forget the `super().__init__` call and the parent's attributes never get set. Accessing `m.name` would then raise `AttributeError: 'Manager' object has no attribute 'name'`.

## Why use super() instead of naming the parent?

Always use `super()` rather than `Employee.__init__(self, ...)`. The [super() documentation](https://docs.python.org/3/builtins/functions.html#super) describes it as returning a proxy that delegates to "a parent or sibling class", following the method resolution order (MRO). Under multiple inheritance, the next class is not necessarily the one you would have named:

```python
class Base:
    def greet(self):
        return "Base"

class Logged(Base):
    def greet(self):
        return "Logged > " + super().greet()

class Cached(Base):
    def greet(self):
        return "Cached > " + super().greet()

class Service(Logged, Cached):
    def greet(self):
        return "Service > " + super().greet()

print(Service().greet())
# Service > Logged > Cached > Base
print([k.__name__ for k in Service.__mro__])
# ['Service', 'Logged', 'Cached', 'Base', 'object']
```

Inside `Logged`, `super()` points to `Cached`, a sibling, not to `Base`. Consequently every class runs exactly once. Had `Logged` called `Base.greet(self)` directly, `Cached` would have been skipped. The [tutorial section on multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) explains how the order is computed.

## Why does composition usually win over inheritance?

Inheritance couples classes tightly. The subclass depends on the parent's internals, and changing the parent can break children in non-obvious ways. This is known as the "fragile base class" problem.

Composition holds a reference instead:

```python
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return "Engine started"


class ElectricMotor:
    def __init__(self, kilowatts):
        self.kilowatts = kilowatts

    def start(self):
        return "Motor humming"


class Car:
    def __init__(self, model, power_unit):
        self.model = model
        self.power_unit = power_unit      # has-a, not is-a

    def start(self):
        return f"{self.model}: {self.power_unit.start()}"
```

```python
print(Car("Nexon", Engine(120)).start())          # Nexon: Engine started
print(Car("Nexon EV", ElectricMotor(95)).start()) # Nexon EV: Motor humming
```

The power unit is swappable at runtime, and `Car` does not depend on either class's internals. Testing is also easy, because you can pass a fake object with a `start` method.

The heuristic: **if you would say "is a", consider inheritance. If you would say "has a", use composition.** Most real relationships are "has a".

## Inheritance and composition in Python compared

| Question | Inheritance | Composition | Duck typing |
|---|---|---|---|
| Relationship | "is a" | "has a" | "behaves like" |
| Coupling | Tight: subclass sees parent internals | Loose: talks through methods | None: only the method name matters |
| Swap behaviour at runtime | No | Yes | Yes |
| Code reuse | Automatic | Explicit delegation | None |
| Typical Python use | Custom exceptions, framework base classes | Services, pipelines, models with parts | Exporters, file-like objects, plugins |

## How does duck typing remove the need for inheritance?

Python does not require a shared base class for polymorphism. If two objects have the same method, they are interchangeable:

```python
import json

class CSVExporter:
    def export(self, rows):
        return "\n".join(",".join(map(str, r)) for r in rows)

class JSONExporter:
    def export(self, rows):
        return json.dumps(rows)

def save(exporter, rows):
    return exporter.export(rows)      # neither class inherits from anything

rows = [[1, "a"], [2, "b"]]
print(save(CSVExporter(), rows))      # 1,a  then  2,b  on the next line
print(save(JSONExporter(), rows))     # [[1, "a"], [2, "b"]]
```

In a statically typed language this would need an interface. In Python, having the method is enough, which removes a large share of the inheritance you might otherwise write. If you want a type checker to verify the shape, [typing.Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol) describes it without forcing the classes to inherit anything.

## What is the difference between a class method and a static method?

A `classmethod` receives the class as `cls` and suits alternative constructors; a `staticmethod` receives nothing and is a plain function kept in the class:

```python
class Date:
    def __init__(self, day, month, year):
        self.day, self.month, self.year = day, month, year

    @classmethod
    def from_string(cls, text):
        d, m, y = map(int, text.split("-"))
        return cls(d, m, y)          # cls, so subclasses get their own type

    @staticmethod
    def is_leap(year):
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
```

```python
d = Date.from_string("15-08-2026")
print(d.day, d.month, d.year)        # 15 8 2026
print(Date.is_leap(2028))            # True
print(Date.is_leap(2100))            # False

class IndianDate(Date):
    pass

print(type(IndianDate.from_string("15-08-2026")).__name__)   # IndianDate
```

A `classmethod` receives the class and is the idiomatic way to write alternative constructors. Using `cls(...)` rather than `Date(...)` means a subclass calling `from_string` gets an instance of itself, as the last line shows.

By contrast, a `staticmethod` receives nothing automatically. It is a plain function that lives in the class for organisational reasons.

## When are abstract base classes worth it?

Use one when you want to define an interface and fail loudly if it is not implemented:

```python
from abc import ABC, abstractmethod

class Model(ABC):
    @abstractmethod
    def fit(self, X, y): ...

    @abstractmethod
    def predict(self, X): ...

    def score(self, X, y):              # concrete, shared by all subclasses
        preds = self.predict(X)
        return sum(p == t for p, t in zip(preds, y)) / len(y)


class Baseline(Model):
    def fit(self, X, y):
        self.majority = max(set(y), key=list(y).count)
        return self

    def predict(self, X):
        return [self.majority] * len(X)
```

```python
X = [[0], [1], [2], [3]]
y = ["spam", "ham", "ham", "ham"]
print(Baseline().fit(X, y).score(X, y))   # 0.75

Model()
# TypeError: Can't instantiate abstract class Model without an
# implementation for abstract methods 'fit', 'predict'
```

As the [abc module documentation](https://docs.python.org/3/library/abc.html#abc.abstractmethod) states, a class with abstract methods cannot be instantiated until all of them are overridden. A subclass missing `predict` therefore fails at instantiation rather than deep inside a call stack later, which is the whole point. The exact wording of the error message varies slightly between Python versions.

## Common mistakes with inheritance

- **Forgetting `super().__init__()`.** The parent's attributes are never set.
- **Hardcoding the parent class.** `Employee.__init__(self, ...)` breaks cooperative multiple inheritance.
- **Inheriting to reuse one helper method.** Move the helper into a function or a component instead.
- **Returning `Date(...)` from a classmethod.** Subclasses then get the wrong type; return `cls(...)`.
- **Deep hierarchies.** Five levels of subclasses make it hard to find which class implements a method.

## How deep should an inheritance hierarchy go?

Two levels is usually plenty. The most common OOP failure in real codebases is not wrong syntax. Instead, it is a five-level inheritance chain where finding which class actually implements a method requires reading all five.

If you need more, that is a strong signal that some of those relationships are "has a" and want composition instead.

Practical guidance: prefer functions to classes, prefer composition to inheritance, prefer shallow to deep, and only add abstraction when the duplication actually hurts.



## Related reading

[Python classes and objects (OOP Part 1)](https://www.1stepgrow.com/articles/python-classes-objects) covers classes, instances and dunder methods. Custom error hierarchies are a good place to practise inheritance, as shown in [exception handling in Python](https://www.1stepgrow.com/articles/python-exception-handling). For functional patterns, see [lambda functions](https://www.1stepgrow.com/articles/python-lambda-functions) and [iterators and generators](https://www.1stepgrow.com/articles/python-iterators-generators).

## Frequently asked questions

### What is the difference between inheritance and composition?

Inheritance says a Manager is an Employee and gets its behaviour automatically. Composition says a Car has an Engine and delegates to it. Composition is usually more flexible, because you can swap the component at runtime and the two classes do not depend on each other's internals, which also makes testing with fakes easy.

### What does super() actually do?

super() returns a proxy object that delegates method calls to the next class in the method resolution order. Under multiple inheritance that next class can be a sibling rather than the direct parent. That is precisely why you should use super() instead of hardcoding ParentClass.method(self), which skips classes and breaks cooperative designs.

### What is the difference between a class method and a static method?

A classmethod receives the class itself as its first argument, usually named cls, and is typically used for alternative constructors such as from_string. A staticmethod receives nothing automatically. It is really just a plain function grouped inside the class for organisation, and it cannot see the class or instance unless you pass them.

### Do I need abstract base classes?

Rarely in small codebases. They earn their place when you are defining an interface that several people or plugins will implement, and you want a clear TypeError at instantiation rather than an AttributeError deep in a call stack. For lighter-weight checking with type checkers, typing.Protocol describes the same interface without inheritance.

---
_Source: 1stepGrow (https://www.1stepgrow.com/articles/python-inheritance-composition/). Cite with the title, "1stepGrow" and a link._
