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.
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.
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:
class Taxi: # no common base class with Walk or Ferry
def describe(self): return "Taxi from the station"
def minutes(self): return 8A 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:
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":
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.
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.
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.
Polymorphism and Inner Classes in a small program
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())Follow the canal path
Board at the east pierLine-by-line explanation
- Lines 1 to 7Two unrelated classes each provide a
describe()method. - Lines 9 to 10The loop calls
describe()without checking the type; each object answers in its own way.
Duck typing, composition and a nested class together
- 7 and 15:
WalkandFerryshare no base class, yet both work in the loop.
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())Walk 2 km £0.00
Harbour ferry 3 km £4.50
East pier (Harbour)Line-by-line explanation
- Lines 16 to 20The nested
Stopclass receives its ferry explicitly; it has no hidden link to one. - Line 27
Ferry.price()delegates to itsFareCalculator: composition rather than inheritance.
Common Polymorphism and Inner Classes Mistakes
Expecting a nested class to know its outer instance
class Route:
class Stop:
def label(self):
return self.route.name # AttributeError: no route attributeclass Route:
class Stop:
def __init__(self, route):
self.route = route
def label(self):
return self.route.namePass the outer object in explicitly; Python doesn't link them automatically.
Checking types instead of calling the shared method
for route in routes:
if isinstance(route, Walk):
print(route.walk_text())
elif isinstance(route, Ferry):
print(route.ferry_text())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.
Does a nested class automatically receive an outer instance?
Which review concern is valid for polymorphism and inner classes?
Polymorphism and Inner Classes Quiz
2 questions to check what you learned.
Does a nested class automatically receive an outer instance?
- Yes
- No
- Python ignores the distinction
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