Python Polymorphism and Inner Classes

Write code that works with any object offering the right methods, prefer composition where it fits, and use nested classes properly.

Lesson 38 of 107Advanced12 min read

Polymorphism means that code can work with objects of different types as long as they support the same operation. In Python, that usually doesn't require a shared parent class: if an object has a describe() method, code that calls describe() will work with it. This approach is called duck typing.

Python
class Walk:
    def describe(self): return "Follow the canal path"

class Ferry:
    def describe(self): return "Board at the east pier"

for route in [Walk(), Ferry()]:
    print(route.describe())

This lesson covers shared interfaces, duck typing, composition as an alternative to inheritance, and nested classes, using a city guide that plans routes. It builds on inheritance, where polymorphism through subclasses was introduced.

Shared interface

Polymorphism pays off when several classes implement the same meaningful operation under the same name, so calling code doesn't need to know which class it has:

Python
def print_itinerary(routes):
    for number, route in enumerate(routes, start=1):
        print(f"{number}. {route.describe()} ({route.minutes()} min)")

print_itinerary works with walks, ferries, buses and any route type added next year, without an if isinstance(...) chain. Adding a new route type means writing one new class; the function stays untouched.

Sharing a name isn't enough on its own: the methods must also share a meaning and a compatible signature. If Walk.minutes() returns minutes but Ferry.minutes() returns hours, the code runs and produces wrong itineraries. Agree what each method returns, document it, and test each implementation against that contract.

Duck typing

"If it walks like a duck and quacks like a duck, it's a duck." Python checks whether an object supports an operation when the operation is used, not whether the object's class inherits from a particular parent:

Python
class Taxi:                       # no common base class with Walk or Ferry
    def describe(self): return "Taxi from the station"
    def minutes(self): return 8

A Taxi works in print_itinerary immediately. Built-in functions rely on the same idea: len() works on anything with __len__, and for works on anything iterable.

The flip side is that a missing method fails only at run time, with AttributeError: 'Bicycle' object has no attribute 'minutes'. Tests catch this, and so do static type checkers if you describe the expected interface with typing.Protocol:

Python
from typing import Protocol

class Route(Protocol):
    def describe(self) -> str: ...
    def minutes(self) -> int: ...

mypy then reports any object passed as a Route that lacks those methods, while classes still don't have to inherit from Route.

Composition

Composition means an object holds other objects and delegates work to them, instead of inheriting behaviour. Inheritance models "is a"; composition models "has a":

Python
class FareCalculator:
    def __init__(self, per_km):
        self.per_km = per_km
    def fare(self, km):
        return round(self.per_km * km, 2)

class FerryRoute:
    def __init__(self, km, fares):
        self.km = km
        self.fares = fares            # has a fare calculator
    def price(self):
        return self.fares.fare(self.km)

A ferry route is not a kind of fare calculator, so it shouldn't inherit from one. Composition keeps each class focused, lets you swap collaborators (a peak-time calculator, a test fake) without subclassing, and avoids deep inheritance chains. Use inheritance when the child truly is a specialised version of the parent and can be used anywhere the parent is expected.

Nested classes

A class defined inside another class is a nested (or inner) class. It's simply an attribute of the outer class: it's reached as Route.Stop, and it groups a helper type with the class that uses it.

Python
class Route:
    class Stop:
        def __init__(self, name, minute):
            self.name, self.minute = name, minute

    def __init__(self, name):
        self.name = name
        self.stops = []

    def add_stop(self, name, minute):
        self.stops.append(Route.Stop(name, minute))

Unlike Java's inner classes, a Python nested class has no automatic link to an outer instance. A Stop doesn't know which route it belongs to unless you pass the route in explicitly, for example Route.Stop(name, minute, route=self). Nested classes are used for small helper types, such as Django's class Meta inside models, but a separate module-level class is often clearer and easier to import and test.

Independent practice and a worked check

Task: create compatible route classes and pass a route explicitly to a nested stop record.

Python
class Bus:
    class Stop:
        def __init__(self, bus, name):
            self.bus, self.name = bus, name
        def __str__(self):
            return f"{self.name} on route {self.bus.number}"

    def __init__(self, number):
        self.number = number
    def describe(self):
        return f"Bus {self.number}"

class Walk:
    def describe(self):
        return "Walk along the canal"

for route in [Bus(42), Walk()]:
    print(route.describe())
print(Bus.Stop(Bus(42), "Market Square"))

This prints Bus 42, Walk along the canal and Market Square on route 42. For the worked check: does a nested class automatically receive an outer instance? No. The Stop only knows its bus because the bus is passed to its constructor.

Reference and next practice

The documentation describes typing.Protocol for static duck typing and the glossary entry for duck typing. Continue with context managers and type checking. Practise with the Python exercises and Python quiz.

Polymorphism and Inner Classes Summary

  • Shared interface: Different objects can implement the same meaningful operation.
  • Duck typing: Python often relies on supported operations rather than declared ancestry.
  • Composition: An object can contain a collaborator instead of inheriting its behavior.
  • Nested classes: A nested class belongs to the outer class namespace.

Polymorphism and Inner Classes Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Polymorphism and Inner Classes in a small program

Python
class Walk:
    def describe(self):
        return "Follow the canal path"

class Ferry:
    def describe(self):
        return "Board at the east pier"

for route in [Walk(), Ferry()]:
    print(route.describe())
Output
Follow the canal path
Board at the east pier
Line-by-line explanation
  1. Lines 1 to 7Two unrelated classes each provide a describe() method.
  2. Lines 9 to 10The loop calls describe() without checking the type; each object answers in its own way.
Example 2

Duck typing, composition and a nested class together

  • 7 and 15: Walk and Ferry share no base class, yet both work in the loop.
Python
class FareCalculator:
    def __init__(self, per_km):
        self.per_km = per_km
    def fare(self, km):
        return round(self.per_km * km, 2)

class Walk:
    def __init__(self, km):
        self.km = km
    def describe(self):
        return f"Walk {self.km} km"
    def price(self):
        return 0.0

class Ferry:
    class Stop:
        def __init__(self, ferry, pier):
            self.ferry, self.pier = ferry, pier
        def label(self):
            return f"{self.pier} ({self.ferry.name})"

    def __init__(self, name, km, fares):
        self.name, self.km, self.fares = name, km, fares
    def describe(self):
        return f"{self.name} ferry {self.km} km"
    def price(self):
        return self.fares.fare(self.km)

harbour = Ferry("Harbour", 3, FareCalculator(1.5))
for route in [Walk(2), harbour]:
    print(f"{route.describe():<22} £{route.price():.2f}")

stop = Ferry.Stop(harbour, "East pier")
print(stop.label())
Output
Walk 2 km              £0.00
Harbour ferry 3 km     £4.50
East pier (Harbour)
Line-by-line explanation
  1. Lines 16 to 20The nested Stop class receives its ferry explicitly; it has no hidden link to one.
  2. Line 27Ferry.price() delegates to its FareCalculator: composition rather than inheritance.

Common Polymorphism and Inner Classes Mistakes

Expecting a nested class to know its outer instance

Don't
class Route:
    class Stop:
        def label(self):
            return self.route.name     # AttributeError: no route attribute
Do
class Route:
    class Stop:
        def __init__(self, route):
            self.route = route
        def label(self):
            return self.route.name

Pass the outer object in explicitly; Python doesn't link them automatically.

Checking types instead of calling the shared method

Don't
for route in routes:
    if isinstance(route, Walk):
        print(route.walk_text())
    elif isinstance(route, Ferry):
        print(route.ferry_text())
Do
for route in routes:
    print(route.describe())

Every new route type would need another branch in the first version.

Polymorphism and Inner Classes in Real Projects

Polymorphism and composition in practice

Python's standard library is built on duck typing. Any object with read() and write() methods can stand in for a file, which is how io.StringIO lets tests capture output, and json.dump() writes to anything with a write() method. Payment systems accept many gateway objects that each implement charge(); plotting libraries draw any object that provides the expected data.

Composition is the usual choice in modern application design, often summarised as "favour composition over inheritance". Services receive their collaborators, such as a database, an email sender or a clock, as constructor arguments, which also makes them easy to test by passing in fakes.

Polymorphism and Inner Classes FAQ

What is polymorphism in Python with a simple example?

Polymorphism is one piece of code working with different types through a shared operation. len("abc"), len([1, 2]) and len({"a": 1}) all work because each type implements __len__. In your own code, a loop calling shape.area() on circles and squares is polymorphic.

What is duck typing?

Duck typing means Python cares whether an object supports the operations you use, not what class it is. Any object with a describe() method can be passed to code that calls describe(), without inheriting from a common base class.

Does Python support method overloading?

Not in the Java sense. Defining two methods with the same name keeps only the last one. Python uses default arguments, *args and **kwargs for flexible signatures, and functools.singledispatch when behaviour must depend on an argument's type.

When should I use composition instead of inheritance?

Use composition when one object uses another ("has a"), when you want to swap behaviour at run time, or when inheritance would require overriding most of the parent. Use inheritance for a genuine "is a" relationship where the child can replace the parent everywhere.

Are inner classes common in Python?

Not very. They appear for small helper types tightly tied to one class, such as Django's Meta options class. Most Python code defines classes at module level, which makes them easier to import, subclass and test.

Polymorphism and Inner Classes Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Does a nested class automatically receive an outer instance?

Multiple choicemedium

Which review concern is valid for polymorphism and inner classes?

Polymorphism and Inner Classes Quiz

2 questions to check what you learned.

  1. Does a nested class automatically receive an outer instance?

    • Yes
    • No
    • Python ignores the distinction
  2. Which statement correctly describes composition?

    • An object can contain a collaborator instead of inheriting its behavior.
    • Only the example output matters, not the rule
    • Every operation has the same behavior