Guide
Project 05 · Method Overriding

🎧 Media Player

Songs, podcasts, audiobooks and video clips — four different kinds of media, each with a completely different idea of what play(), pause() and duration() mean. Yet the transport bar below calls exactly the same three methods on every one of them.

This is method overriding in action: a single MediaItem base class defines the interface, each subclass overrides the behaviour, and the player loop never asks "what are you?". In this lesson you will play with it, build it from scratch, break it on purpose, then prove you understand it.

~45 min Beginner → Intermediate 14 runnable examples 8-question quiz
Full Guide Open the Player Build It Step by Step Jump to Quiz

By the end you can…

  • Explain polymorphism in one sentence
  • Override methods and call super() correctly
  • Use ABC to enforce a contract
  • Replace isinstance ladders with one call
  • Choose between inheritance and duck typing

You should already know

  • Defining classes and __init__
  • Basic inheritance (class B(A))
  • f-strings, loops and lists

New to classes? Skim the Polymorphism Guide first.

One Transport Bar, Four Media Kinds

Click any track to load it. Press ▶ to play, ⏸ to pause, or click the progress bar to seek. Watch the trace panel at the bottom — every action calls a method that the object's own class implements differently.

Try this experiment (2 minutes): ① Play each of the four tracks and compare the trace lines. ② Press ➕ Add Lecture and play it — the player code did not change. ③ Press 🦆 Duck-type test to add an object that does not inherit from MediaItem. ④ Press ⚠ Call base play() to see what the abstract base class does.

🎚️ Poetic Bytes Media Player Ready
🎵
SONG
Select a track below
No media loaded
item.duration() → —
0:000:00
70%

📚 Library

each row = a different subclass
Method trace — click a track or press play:
Load a track to begin.

What to notice: the controls section only calls item.play(), item.pause() and item.duration(). The base class MediaItem declares these methods; each subclass (Song, Podcast, Audiobook, VideoClip) overrides them with its own logic. The player never checks the type.

What Is Polymorphism, Really?

The word comes from Greek: poly (many) + morph (forms). In programming it means one interface, many implementations. The caller says what it wants done; each object decides how.

🔌 Real-world analogy: the wall socket

A wall socket promises one thing: "plug in and receive power." A lamp, a phone charger and a fan all plug into the same socket, and each does something completely different with the electricity. The socket never needs to know which device is attached. Your media player is the socket; Song, Podcast and friends are the devices.

The contract: every MediaItem promises play(), pause() and duration(). As long as the promise holds, the player works with any item — including ones that do not exist yet.

The four faces of polymorphism in Python

🔁

Method overriding

A subclass replaces a parent method. This page.

class Song(MediaItem)
🦆

Duck typing

"If it quacks like a duck…" No shared parent needed.

obj.play()
➕

Operator overloading

Your class defines what +, ==, < mean.

__add__, __lt__
🎯

Single dispatch

One function name, behaviour chosen by argument type.

@singledispatch

Why bother? Three payoffs

  • Open for extension, closed for modification. Add a Lecture class without touching the player (the Open/Closed Principle).
  • Less branching. No growing if/elif ladder that must be edited in every function that handles media.
  • Easier testing. Swap in a fake item that records calls — the player cannot tell the difference.

Liskov Substitution Principle in one line: anywhere your code expects a MediaItem, any subclass must work without surprises. Overrides may change behaviour, but not the promise (names, arguments, return type).

From a Messy Ladder to Clean Polymorphism

Press Run on every step. Each block is self-contained, so you can also edit it in the playground later. (The first run loads Python in your browser and takes a few seconds.)

Start with the problem

Without polymorphism, the player must know every media type. Notice how play_item already needs editing each time a new kind appears.

def play_item(item):
    if item["kind"] == "song":
        return f"🎵 Now playing {item['title']}"
    elif item["kind"] == "podcast":
        return f"🎙 Episode {item['episode']}: {item['title']}"
    elif item["kind"] == "audiobook":
        return f"📖 Chapter {item['chapter']} of {item['title']}"
    else:
        return "❓ Unknown media type"

library = [
    {"kind": "song", "title": "Sunrise"},
    {"kind": "podcast", "title": "Why OOP Matters", "episode": 12},
    {"kind": "video", "title": "Polymorphism Explained"},
]
for item in library:
    print(play_item(item))

The smell: the video fell into else and printed "Unknown". Every new kind means editing this function — and every other function with a similar ladder.

Define the contract with a base class

We declare what every media item must do, and leave how to the subclasses. @abstractmethod turns the promise into a rule Python enforces.

from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title = title
        self.creator = creator

    @abstractmethod
    def play(self): ...

    @abstractmethod
    def pause(self): ...

    @abstractmethod
    def duration(self): ...

try:
    MediaItem("x", "y")
except TypeError as e:
    print("TypeError:", e)

Write the first subclass

A subclass overrides each abstract method. Use super().__init__() to reuse the parent's setup instead of copying it.

from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title, self.creator = title, creator
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist)
        self.seconds = seconds

    def play(self):     return f"🎵 Now playing '{self.title}' by {self.creator}"
    def pause(self):    return f"⏸ Song paused at chorus of '{self.title}'"
    def duration(self): return self.seconds

s = Song("Sunrise", "The Harbors", 213)
print(s.play())
print(s.pause())
print(isinstance(s, MediaItem))

Add more kinds and use one loop

Now the payoff. Two more subclasses, and the loop is identical for all of them.

from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title, self.creator = title, creator
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist); self.seconds = seconds
    def play(self):     return f"🎵 Now playing '{self.title}'"
    def pause(self):    return "⏸ Song paused"
    def duration(self): return self.seconds

class Podcast(MediaItem):
    def __init__(self, title, host, seconds, episode):
        super().__init__(title, host); self.seconds = seconds; self.episode = episode
    def play(self):     return f"🎙 Episode {self.episode}: '{self.title}'"
    def pause(self):    return f"⏸ Bookmarked at episode {self.episode}"
    def duration(self): return self.seconds

library = [Song("Sunrise", "The Harbors", 213),
           Podcast("Why OOP Matters", "Byte Talk", 1540, 12)]

for item in library:
    print(item.play())
    print(item.pause())

Extend without touching the loop

This is the Open/Closed Principle proving itself. We add a brand-new Lecture class and the loop from step 4 stays exactly the same.

from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title, self.creator = title, creator
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist); self.seconds = seconds
    def play(self):     return f"🎵 Now playing '{self.title}'"
    def pause(self):    return "⏸ Song paused"
    def duration(self): return self.seconds

class Lecture(MediaItem):
    def __init__(self, title, professor, seconds, week):
        super().__init__(title, professor); self.seconds = seconds; self.week = week
    def play(self):     return f"🎓 Week {self.week}: '{self.title}' with Prof. {self.creator}"
    def pause(self):    return f"⏸ Lecture paused — notes saved for week {self.week}"
    def duration(self): return self.seconds

library = [Song("Sunrise", "The Harbors", 213),
           Lecture("Intro to Polymorphism", "Reyes", 3000, 4)]

for item in library:
    print(item.play())

Milestone reached. You just used inheritance, overriding, super(), an abstract contract and the Open/Closed Principle. The ➕ Add Lecture button in the live player does the same thing in JavaScript.

The Same App, in Python OOP

This is the exact model that runs the player above — a MediaItem base class and four subclasses, each overriding play(), pause(), duration() and info(). Press Run to execute it in your browser with Pyodide.

media_player.py

from abc import ABC, abstractmethod


class MediaItem(ABC):
    """Base contract: every media item can play, pause, and report its duration."""

    def __init__(self, title, creator):
        self.title = title
        self.creator = creator

    @abstractmethod
    def play(self):
        ...

    @abstractmethod
    def pause(self):
        ...

    @abstractmethod
    def duration(self):
        """Return duration in seconds."""

    def info(self):
        m, s = divmod(int(self.duration()), 60)
        return f"{type(self).__name__:<10} {self.title!r} by {self.creator} ({m}:{s:02d})"


class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist)
        self.seconds = seconds

    def play(self):
        return f"🎵 Now playing '{self.title}' by {self.creator}"

    def pause(self):
        return f"⏸ Song paused at chorus of '{self.title}'"

    def duration(self):
        return self.seconds


class Podcast(MediaItem):
    def __init__(self, title, host, seconds, episode):
        super().__init__(title, host)
        self.seconds = seconds
        self.episode = episode

    def play(self):
        return f"🎙 Episode {self.episode}: '{self.title}' with {self.creator}"

    def pause(self):
        return f"⏸ Podcast paused — bookmarked at episode {self.episode}"

    def duration(self):
        return self.seconds


class Audiobook(MediaItem):
    def __init__(self, title, narrator, seconds, chapter):
        super().__init__(title, narrator)
        self.seconds = seconds
        self.chapter = chapter

    def play(self):
        return f"📖 Chapter {self.chapter} of '{self.title}', read by {self.creator}"

    def pause(self):
        return f"⏸ Audiobook closed at chapter {self.chapter}"

    def duration(self):
        return self.seconds


class VideoClip(MediaItem):
    def __init__(self, title, channel, seconds, resolution="1080p"):
        super().__init__(title, channel)
        self.seconds = seconds
        self.resolution = resolution

    def play(self):
        return f"🎬 Streaming '{self.title}' from {self.creator} at {self.resolution}"

    def pause(self):
        return f"⏸ Video paused at {self.resolution}"

    def duration(self):
        return self.seconds


library = [
    Song("Sunrise", "The Harbors", 213),
    Podcast("Why OOP Matters", "Byte Talk", 1540, 12),
    Audiobook("Pythonic Patterns", "R. Domingo", 7200, 3),
    VideoClip("Polymorphism Explained", "Poetic Bytes", 480, "4K"),
]

print("─" * 50)
for item in library:
    print(item.info())
print("─" * 50)

for item in library:
    print(item.play())
    print(item.pause())

print("─" * 50)
print("Total library time:", sum(i.duration() for i in library), "seconds")

Why this works: item.play() appears once in the loop, but Python dispatches it to Song.play, Podcast.play, Audiobook.play or VideoClip.play depending on the object's real class. Adding a Lecture class later means writing the class — the loop stays untouched.

🔍 Code-reading guide

  • Lines 1–2: ABC and abstractmethod come from the standard library's abc module.
  • MediaItem.__init__: stores the data every item shares, so subclasses do not repeat it.
  • info(): a concrete method in the base class. It calls self.duration(), which resolves to whichever subclass the object really is.
  • super().__init__(title, artist): hands shared setup to the parent, then each subclass adds its own fields.
  • The final loops: no isinstance, no if. That absence is the whole lesson.

How the Player Chooses a Method

The Dispatch Table

When the player calls item.play(), Python searches the object's Method Resolution Order for a play attribute. Here is what it finds for each media kind:

ObjectMRO searchResolves toBehaviour
SongSong → MediaItem → ABC → objectSong.playTitle + artist message
PodcastPodcast → MediaItem → ABC → objectPodcast.playEpisode-numbered message
AudiobookAudiobook → MediaItem → ABC → objectAudiobook.playChapter-numbered message
VideoClipVideoClip → MediaItem → ABC → objectVideoClip.playResolution-aware message

You can inspect this yourself — every class stores its search order in __mro__:

from abc import ABC, abstractmethod

class MediaItem(ABC):
    @abstractmethod
    def play(self): ...

class Song(MediaItem):
    def play(self): return "song"

for cls in Song.__mro__:
    print(cls.__name__)

print()
print("play defined on Song itself?", "play" in vars(Song))
print("info defined on Song itself?", "info" in vars(Song))

Why the Base Class Declares @abstractmethod

MediaItem cannot be instantiated directly — pressing ⚠ Call base play() in the player demonstrates the equivalent: the base play() has no implementation, so it refuses to run. That guarantees every item in the library actually has a working play().

Without the abstract method, a subclass could "forget" to override play() and inherit a stub that silently does nothing — a classic late-night bug. With ABC + @abstractmethod, the error is loud and immediate at instantiation.

Template Method: overriding part of the behaviour

Notice info() is written once in the base class, yet it prints different durations per subclass. It calls self.duration(), and Python picks the override. This is called the Template Method pattern: the parent defines the skeleton, children fill in the steps. Overrides can also extend a parent method by calling super():

class MediaItem:
    def __init__(self, title):
        self.title = title
    def info(self):
        return f"{self.title}"

class VideoClip(MediaItem):
    def __init__(self, title, resolution):
        super().__init__(title)
        self.resolution = resolution
    def info(self):
        base = super().info()
        return f"{base} [{self.resolution}]"

print(MediaItem("Intro").info())
print(VideoClip("Intro", "4K").info())

Duck Typing Means You Don't Even Need the Base Class

The 🦆 Duck-type test button injects a completely unrelated class — no inheritance from MediaItem — and the player still works, because it only calls .play(), .pause() and .duration(). That's the duck-typing version of the same polymorphism.

class Platypus:
    def play(self):     return "🦆 Platypus Stream"
    def pause(self):    return "⏸ Platypus paused"
    def duration(self): return 60

def run(item):
    print(item.play(), "|", item.duration(), "s")

run(Platypus())

Rule of thumb: use inheritance when subclasses share code (here: the info() formatting and the title / creator fields). Use duck typing when you want unrelated objects to plug in — like a test double or a plugin.

A Modern Middle Path: typing.Protocol

Since Python 3.8 you can describe the interface without inheritance, and let type checkers (mypy, Pyright) verify duck-typed objects. Nothing is enforced at runtime, but your editor warns you before you ship the bug.

from typing import Protocol

class Playable(Protocol):
    def play(self) -> str: ...
    def pause(self) -> str: ...
    def duration(self) -> int: ...

class Jingle:
    def play(self):     return "🔔 ding"
    def pause(self):    return "⏸"
    def duration(self): return 3

def start(item: Playable) -> None:
    print(item.play())

start(Jingle())
ApproachEnforced when?Shares code?Best for
ABC + inheritanceAt instantiation (runtime)YesFamilies of related classes
Plain duck typingNever — fails when calledNoSmall scripts, plugins, test doubles
ProtocolBy the type checker (before running)NoLarge codebases, library APIs

Side-by-Side: Ladder vs Polymorphism

# ✗ Ladder version — every new media kind edits the player
def play_item(item):
    if isinstance(item, Song):
        return "🎵 Now playing"
    elif isinstance(item, Podcast):
        return "🎙 Episode"
    elif isinstance(item, Audiobook):
        return "📖 Chapter"
    # add VideoClip → edit here, and every other ladder too

# ✓ Polymorphic version — one call, zero edits for new types
for item in library:
    item.play()

Four Classic Polymorphism Mistakes

The fastest way to understand a rule is to watch it fail. Run each broken example, read the error, then run the fix.

Mistake 1 — Forgetting to override an abstract method

from abc import ABC, abstractmethod

class MediaItem(ABC):
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def play(self):
        return "playing"

Song()

Python refuses to create the object and names the missing method. That is the contract doing its job.

✓ Fix: implement every abstract method
from abc import ABC, abstractmethod

class MediaItem(ABC):
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def play(self):     return "playing"
    def duration(self): return 213

print(Song().play(), Song().duration())

Mistake 2 — Changing the method signature in the override

Python will not stop you, but you have broken the Liskov Substitution Principle: callers written for the base class now crash for one subclass.

class Song:
    def play(self):
        return "song playing"

class Podcast(Song):
    def play(self, episode):
        return f"episode {episode}"

for item in [Song(), Podcast()]:
    print(item.play())
✓ Fix: keep the promise; make extras optional or store them on the object
class Song:
    def play(self):
        return "song playing"

class Podcast(Song):
    def __init__(self, episode=1):
        self.episode = episode
    def play(self):
        return f"episode {self.episode}"

for item in [Song(), Podcast(12)]:
    print(item.play())

Mistake 3 — Rebuilding the ladder with isinstance

If you catch yourself writing isinstance to decide which behaviour to run, the behaviour probably belongs inside the classes.

class Song: pass
class Podcast: pass
class Lecture: pass

def label(item):
    if isinstance(item, Song):    return "🎵"
    elif isinstance(item, Podcast): return "🎙"
    return "❓"

print([label(x) for x in (Song(), Podcast(), Lecture())])
✓ Fix: make each class carry its own icon
class Song:    icon = "🎵"
class Podcast: icon = "🎙"
class Lecture: icon = "🎓"

print([x.icon for x in (Song(), Podcast(), Lecture())])

Mistake 4 — Forgetting super().__init__()

When a subclass defines its own __init__, the parent's is not called automatically. Shared fields never get set.

class MediaItem:
    def __init__(self, title, creator):
        self.title = title
        self.creator = creator

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        self.seconds = seconds

s = Song("Sunrise", "The Harbors", 213)
print(s.title)
✓ Fix: delegate to the parent first
class MediaItem:
    def __init__(self, title, creator):
        self.title = title
        self.creator = creator

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist)
        self.seconds = seconds

s = Song("Sunrise", "The Harbors", 213)
print(s.title, "-", s.creator)

Debug habit: when a method does something unexpected, print type(obj).__mro__ and type(obj).play. They tell you exactly which version Python picked.

Four Challenges and a Playground

Reading builds recognition; writing builds skill. Try each challenge in the playground below before opening the hint or the solution.

Easy Challenge 1 — Add a Radio class

Create Radio(MediaItem) with a station attribute. play() should return "📻 Tuned to {station}", pause() should return "⏸ Radio muted", and duration() should return 0 (live streams never end). Do not modify the loop.

💡 Hint

Copy the structure of Song. Call super().__init__(title, creator), then set self.station. Return a constant from duration().

✅ Solution
from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title, self.creator = title, creator
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def duration(self): ...

class Radio(MediaItem):
    def __init__(self, title, host, station):
        super().__init__(title, host)
        self.station = station
    def play(self):     return f"📻 Tuned to {self.station}"
    def pause(self):    return "⏸ Radio muted"
    def duration(self): return 0

for item in [Radio("Morning Show", "DJ Ana", "96.7 FM")]:
    print(item.play(), item.duration())
Medium Challenge 2 — A Playlist that is itself playable

Build Playlist(MediaItem) that holds a list of items. Its duration() must return the sum of its children, and its play() must return the play() text of every child joined by newlines. Because a playlist is a MediaItem, you should be able to put a playlist inside another playlist.

💡 Hint

Store self.items = list(items). Use sum(i.duration() for i in self.items). This is the Composite pattern: the container and its contents share one interface.

✅ Solution
from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator):
        self.title, self.creator = title, creator
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def duration(self): ...

class Song(MediaItem):
    def __init__(self, title, artist, seconds):
        super().__init__(title, artist); self.seconds = seconds
    def play(self):     return f"🎵 {self.title}"
    def pause(self):    return "⏸"
    def duration(self): return self.seconds

class Playlist(MediaItem):
    def __init__(self, title, items):
        super().__init__(title, "You")
        self.items = list(items)
    def play(self):     return "\n".join(i.play() for i in self.items)
    def pause(self):    return "⏸ Playlist paused"
    def duration(self): return sum(i.duration() for i in self.items)

inner = Playlist("Chill", [Song("A", "x", 100), Song("B", "y", 200)])
outer = Playlist("Everything", [inner, Song("C", "z", 50)])
print(outer.play())
print("Total:", outer.duration(), "seconds")
Medium Challenge 3 — Sort the library by duration

Make sorted(library) work by giving MediaItem a __lt__ method that compares durations. This combines method overriding with operator overloading — two flavours of polymorphism in one feature.

💡 Hint

sorted() only needs __lt__. Define it once on the base class: return self.duration() < other.duration().

✅ Solution
from abc import ABC, abstractmethod

class MediaItem(ABC):
    def __init__(self, title, creator, seconds):
        self.title, self.creator, self.seconds = title, creator, seconds
    @abstractmethod
    def play(self): ...
    def duration(self): return self.seconds
    def __lt__(self, other):
        return self.duration() < other.duration()
    def __repr__(self):
        return f"{type(self).__name__}({self.title!r}, {self.duration()}s)"

class Song(MediaItem):
    def play(self): return "🎵"
class Podcast(MediaItem):
    def play(self): return "🎙"

library = [Podcast("Long", "a", 1540), Song("Short", "b", 213), Song("Mid", "c", 480)]
print(sorted(library))
Hard Challenge 4 — Refactor the ladder

The code below works but breaks every time a new shape arrives. Refactor it so each class computes its own area and total_area contains no isinstance. Then add a Triangle without editing total_area.

import math

class Circle:
    def __init__(self, r): self.r = r
class Square:
    def __init__(self, s): self.s = s

def total_area(shapes):
    total = 0
    for sh in shapes:
        if isinstance(sh, Circle):
            total += math.pi * sh.r ** 2
        elif isinstance(sh, Square):
            total += sh.s ** 2
    return total

print(round(total_area([Circle(1), Square(2)]), 2))
💡 Hint

Move each formula into an area() method on its class. Then total_area becomes sum(sh.area() for sh in shapes).

✅ Solution
import math

class Circle:
    def __init__(self, r): self.r = r
    def area(self): return math.pi * self.r ** 2

class Square:
    def __init__(self, s): self.s = s
    def area(self): return self.s ** 2

class Triangle:
    def __init__(self, b, h): self.b, self.h = b, h
    def area(self): return 0.5 * self.b * self.h

def total_area(shapes):
    return sum(sh.area() for sh in shapes)

print(round(total_area([Circle(1), Square(2), Triangle(3, 4)]), 2))

🧪 Playground — write your own code

Edit freely, then press Run. Starter code is below; solve the challenges here. Press Ctrl/⌘ + Enter to run quickly.

Check Your Understanding

Eight questions. You get instant feedback and an explanation after each answer. Your best score is saved in this browser.

Key Takeaways

  • One interface, many forms. Callers rely on method names; objects decide behaviour.
  • Overriding replaces a parent method in a subclass. Keep the same signature and return type.
  • ABC + @abstractmethod turns a promise into an enforced rule, failing loudly at instantiation.
  • super() lets a child reuse or extend parent behaviour instead of copying it.
  • Duck typing and Protocol give polymorphism without inheritance.
  • Smell test: an isinstance ladder usually means behaviour belongs inside the classes.

Where You See This in Real Software

🗄️

Databases

One connection.execute() API; PostgreSQL, SQLite and MySQL drivers each implement it differently.

🎮

Games

Every enemy has update() and draw(); the game loop calls them on a mixed list.

💳

Payments

gateway.charge() hides whether it talks to Stripe, PayPal or GCash.

📁

File-like objects

Anything with .read() works: files, network sockets, in-memory buffers.

Glossary

Polymorphism

Using one interface to work with objects of different classes.

Override

Redefining an inherited method in a subclass.

Abstract class

A class that cannot be instantiated until its abstract methods are implemented.

MRO

Method Resolution Order: the sequence Python searches to find a method.

super()

A proxy to the parent class, used to reuse its methods.

Duck typing

Judging an object by the methods it has, not by its class.

Protocol

A type-hint-only interface matched by shape, not inheritance.

Liskov principle

Subclasses must be usable wherever the parent is expected.

Open/Closed

Open for extension, closed for modification.

Template Method

Parent defines the algorithm skeleton; children supply the steps.

Frequently Asked Questions

Is polymorphism the same as inheritance?

No. Inheritance is a mechanism for sharing code and a type relationship. Polymorphism is the benefit of treating different objects uniformly. Python gets polymorphism from inheritance, but also from duck typing, which needs no inheritance at all.

Do I always need an abstract base class?

No. Use ABC when you control a family of classes and want errors early. For small scripts or plugin-style code, duck typing is simpler. For large typed codebases, consider Protocol.

What is the difference between overriding and overloading?

Overriding replaces a parent's method in a subclass (this lesson). Overloading means several functions sharing a name with different parameters. Python has no built-in overloading by signature; use default arguments, *args, or @singledispatch instead. "Operator overloading" (__add__) is a separate, Python-supported idea.

Does polymorphism make code slower?

The lookup cost is tiny and Python caches it. In practice the clarity and maintainability gains far outweigh any difference. Measure before optimising.

Why does the base class have a duration() docstring but no body?

A function whose body is only a docstring (or ...) is valid and returns None. Because the method is abstract, subclasses must override it anyway, so the stub is never reached through a normal instance.

Where should I go next?

Continue with the other nine projects below. A good order: Shape Renderer (same overriding idea, simpler), Test-Double Lab (duck typing for testing), then Currency Converter (operator overloading).

10 Live Projects

You're looking at Project 05. Here are the other nine — each demonstrates a different flavor of polymorphism.

#ProjectFlavor
01🔷 Shape RendererMethod overriding
02🖼️ Image Filter PluginDuck typing
03💱 Currency ConverterOperator overloading
04🍳 Recipe FormatterSingle dispatch
05🎧 Media PlayerMethod overriding ← you are here
06🧪 Test-Double LabDuck typing
07📒 Double-Entry LedgerOperator overloading
08📨 Log RouterSingle dispatch
09♞ Chess PiecesMethod overriding
10✅ Protocol CheckerDuck typing
Read the Full Guide All 10 Projects