Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

🦎 What is Polymorphism?

The word polymorphism comes from Greek:
  • Poly = Many
  • Morph = Forms
So polymorphism means “many forms”! Imagine a universal remote control 📺. You press the “ON” button and:
  • For a TV: It turns on the TV
  • For a AC: It turns on the air conditioner
  • For Lights: It turns on the lights
Same button, different results based on what you’re controlling!
Simple Definition: Polymorphism = Same method name, different behavior depending on the object
A deeper analogy: Think of a power outlet in your wall. The outlet provides one standard interface (two or three prongs). You can plug in a lamp, a phone charger, a blender, or a laptop — each device does something completely different with the same electricity. The outlet does not need to know what is plugged in, and you do not need a different outlet for each device. That is polymorphism: one interface, unlimited implementations.

🔊 The Classic Shape Example

class Shape:
    def area(self):
        pass  # Each shape calculates differently
    
    def draw(self):
        pass  # Each shape draws differently

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def draw(self):
        print("⭕ Drawing a circle")

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def draw(self):
        print("⬜ Drawing a rectangle")

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def area(self):
        return 0.5 * self.base * self.height
    
    def draw(self):
        print("🔺 Drawing a triangle")

# THE MAGIC OF POLYMORPHISM ✨
# Same code works for ANY shape!

shapes = [
    Circle(5),
    Rectangle(4, 6),
    Triangle(3, 8)
]

print("Drawing all shapes and calculating areas:\n")
for shape in shapes:
    shape.draw()  # Same method call
    print(f"   Area: {shape.area():.2f}\n")  # Different results!
Output:
Drawing all shapes and calculating areas:

⭕ Drawing a circle
   Area: 78.54

⬜ Drawing a rectangle
   Area: 24.00

🔺 Drawing a triangle
   Area: 12.00

🎵 Real-World Example: Music Player

Imagine a music player that can play different types of audio:
class AudioFile:
    def __init__(self, filename):
        self.filename = filename
    
    def play(self):
        raise NotImplementedError("Subclass must implement this!")

class MP3File(AudioFile):
    def play(self):
        print(f"🎵 Playing MP3: {self.filename}")
        print("   Using MP3 decoder...")

class WAVFile(AudioFile):
    def play(self):
        print(f"🎵 Playing WAV: {self.filename}")
        print("   Raw audio, high quality!")

class FLACFile(AudioFile):
    def play(self):
        print(f"🎵 Playing FLAC: {self.filename}")
        print("   Lossless compression!")

class OGGFile(AudioFile):
    def play(self):
        print(f"🎵 Playing OGG: {self.filename}")
        print("   Open source format!")

# Music Player doesn't care about file type!
class MusicPlayer:
    def __init__(self):
        self.playlist = []
    
    def add_to_playlist(self, audio_file):
        self.playlist.append(audio_file)
    
    def play_all(self):
        print("=" * 40)
        print("🎧 Now Playing Your Playlist")
        print("=" * 40)
        for track in self.playlist:
            track.play()  # Polymorphism! Same method, different behavior
            print()

# Create playlist with different file types
player = MusicPlayer()
player.add_to_playlist(MP3File("song1.mp3"))
player.add_to_playlist(WAVFile("sound_effect.wav"))
player.add_to_playlist(FLACFile("classical.flac"))
player.add_to_playlist(OGGFile("podcast.ogg"))

player.play_all()

🎮 Fun Example: Game Attacks

Every character attacks differently, but the game just calls attack()!
class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health
    
    def attack(self, target):
        raise NotImplementedError("Each character attacks differently!")
    
    def take_damage(self, amount):
        self.health -= amount
        status = "💀 Defeated!" if self.health <= 0 else f"❤️ {self.health} HP left"
        print(f"   {self.name}: {status}")

class Warrior(Character):
    def attack(self, target):
        print(f"⚔️ {self.name} slashes with sword!")
        target.take_damage(25)

class Mage(Character):
    def attack(self, target):
        print(f"🔥 {self.name} casts Fireball!")
        target.take_damage(35)

class Archer(Character):
    def attack(self, target):
        print(f"🏹 {self.name} shoots an arrow!")
        target.take_damage(20)

class Healer(Character):
    def attack(self, target):
        print(f"✨ {self.name} throws holy light!")
        target.take_damage(10)
    
    def heal(self, target):
        print(f"💚 {self.name} heals {target.name}!")
        target.health += 30

class Ninja(Character):
    def attack(self, target):
        print(f"🌀 {self.name} appears behind the enemy!")
        print(f"   Triple strike combo!")
        target.take_damage(15)
        target.take_damage(15)
        target.take_damage(15)

# THE BATTLE!
def battle_round(attackers, defender):
    print("\n" + "=" * 50)
    print(f"🎯 Target: {defender.name}")
    print("=" * 50)
    
    for attacker in attackers:
        attacker.attack(defender)  # Same call, different attacks!
        if defender.health <= 0:
            print(f"\n🏆 {defender.name} has been defeated!")
            return

# Create team
team = [
    Warrior("Conan"),
    Mage("Gandalf"),
    Archer("Legolas"),
    Ninja("Naruto")
]

# Create enemy
dragon = Character("Dragon", 200)
dragon.health = 200

# Battle!
battle_round(team, dragon)

📚 Types of Polymorphism

Method Overriding

Same method name in parent and childChild provides its own version
class Animal:
    def speak(self): pass

class Dog(Animal):
    def speak(self):
        print("Woof!")

Duck Typing

If it walks like a duck…Python doesn’t check type, just method
def make_it_speak(thing):
    thing.speak()  # Works if has speak()

🦆 Duck Typing Explained

“If it walks like a duck and quacks like a duck, it must be a duck!”
Python doesn’t care about the TYPE, only about whether the object has the method:
class Duck:
    def quack(self):
        print("Quack! 🦆")
    
    def walk(self):
        print("Waddle waddle 🚶")

class Person:
    def quack(self):
        print("I'm pretending to be a duck! 🗣️")
    
    def walk(self):
        print("Walking normally 🚶")

class Robot:
    def quack(self):
        print("QUACK.exe executed 🤖")
    
    def walk(self):
        print("*mechanical walking sounds* 🦿")

# This function works with ANYTHING that can quack and walk
def do_duck_things(creature):
    creature.quack()
    creature.walk()

# All of these work!
print("=== Real Duck ===")
do_duck_things(Duck())

print("\n=== Person ===")
do_duck_things(Person())

print("\n=== Robot ===")
do_duck_things(Robot())

🎨 Practical Example: Drawing Application

from abc import ABC, abstractmethod

class Drawable(ABC):
    """Anything that can be drawn on screen"""
    
    @abstractmethod
    def draw(self):
        pass
    
    @abstractmethod
    def get_area(self):
        pass

class Circle(Drawable):
    def __init__(self, x, y, radius, color):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
    
    def draw(self):
        print(f"⭕ Circle at ({self.x}, {self.y})")
        print(f"   Radius: {self.radius}, Color: {self.color}")
    
    def get_area(self):
        return 3.14159 * self.radius ** 2

class Rectangle(Drawable):
    def __init__(self, x, y, width, height, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
    
    def draw(self):
        print(f"⬜ Rectangle at ({self.x}, {self.y})")
        print(f"   Size: {self.width}x{self.height}, Color: {self.color}")
    
    def get_area(self):
        return self.width * self.height

class Text(Drawable):
    def __init__(self, x, y, content, font_size):
        self.x = x
        self.y = y
        self.content = content
        self.font_size = font_size
    
    def draw(self):
        print(f"📝 Text at ({self.x}, {self.y})")
        print(f"   \"{self.content}\" (size: {self.font_size})")
    
    def get_area(self):
        # Text area is approximate
        return len(self.content) * self.font_size * 0.5

class Canvas:
    def __init__(self, name):
        self.name = name
        self.elements = []
    
    def add(self, drawable):
        self.elements.append(drawable)
    
    def render(self):
        print(f"\n🖼️ Rendering Canvas: {self.name}")
        print("=" * 40)
        for element in self.elements:
            element.draw()  # Polymorphism!
            print()
    
    def total_area(self):
        return sum(e.get_area() for e in self.elements)

# Create a drawing
canvas = Canvas("My Artwork")
canvas.add(Circle(100, 100, 50, "red"))
canvas.add(Rectangle(200, 50, 80, 60, "blue"))
canvas.add(Text(50, 200, "Hello World!", 24))
canvas.add(Circle(300, 300, 30, "green"))

canvas.render()
print(f"📐 Total covered area: {canvas.total_area():.2f}")

💳 Example: Payment System

Different payment methods, same interface:
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError
    
    def get_name(self):
        raise NotImplementedError

class CreditCard(PaymentMethod):
    def __init__(self, card_number, cvv):
        self.card_number = card_number[-4:]  # Only store last 4
    
    def pay(self, amount):
        print(f"💳 Credit Card ending in {self.card_number}")
        print(f"   Processing ${amount:.2f}...")
        print(f"   ✅ Payment successful!")
        return True
    
    def get_name(self):
        return f"Card ***{self.card_number}"

class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def pay(self, amount):
        print(f"🅿️ PayPal ({self.email})")
        print(f"   Redirecting to PayPal...")
        print(f"   ✅ ${amount:.2f} paid via PayPal!")
        return True
    
    def get_name(self):
        return f"PayPal: {self.email}"

class Crypto(PaymentMethod):
    def __init__(self, wallet_address):
        self.wallet = wallet_address[:8] + "..."
    
    def pay(self, amount):
        btc_amount = amount / 50000  # Fake conversion
        print(f"₿ Crypto Wallet ({self.wallet})")
        print(f"   Converting ${amount:.2f} to {btc_amount:.6f} BTC...")
        print(f"   ⏳ Waiting for blockchain confirmation...")
        print(f"   ✅ Transaction confirmed!")
        return True
    
    def get_name(self):
        return f"Crypto: {self.wallet}"

class ApplePay(PaymentMethod):
    def __init__(self, device_name):
        self.device = device_name
    
    def pay(self, amount):
        print(f"🍎 Apple Pay ({self.device})")
        print(f"   Authenticating with Face ID...")
        print(f"   ✅ ${amount:.2f} paid!")
        return True
    
    def get_name(self):
        return f"Apple Pay: {self.device}"

# Checkout system - doesn't care WHICH payment method!
class Checkout:
    def __init__(self):
        self.cart_total = 0
    
    def process_payment(self, payment_method, amount):
        print("\n" + "=" * 50)
        print(f"🛒 Checkout - Total: ${amount:.2f}")
        print(f"📱 Using: {payment_method.get_name()}")
        print("=" * 50)
        
        # POLYMORPHISM! Same call, different behavior
        success = payment_method.pay(amount)
        
        if success:
            print("\n🎉 Thank you for your purchase!")
        return success

# Test different payment methods
checkout = Checkout()

# Same checkout process, different payment methods
checkout.process_payment(CreditCard("1234567890123456", "123"), 99.99)
checkout.process_payment(PayPal("user@email.com"), 49.99)
checkout.process_payment(Crypto("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), 199.99)
checkout.process_payment(ApplePay("iPhone 15"), 29.99)

Why Polymorphism Is the Heart of Good Design

Polymorphism is not just a convenience — it is the mechanism that makes the Open/Closed Principle (which you will learn in the SOLID section) possible. When your code depends on an abstract interface like PaymentMethod.pay() rather than a concrete class like CreditCard.charge_visa(), you can add new payment methods (Apple Pay, crypto, buy-now-pay-later) without modifying the checkout code at all. A senior engineer would say: “If encapsulation protects your data and inheritance organizes your types, polymorphism is what makes your system genuinely extensible. Every well-designed plugin system, every driver architecture, every strategy pattern — they all depend on polymorphism.” Production example: Consider how Python’s sorted() function works. It does not know whether it is sorting integers, strings, or custom objects. It just calls each object’s comparison methods. That is polymorphism in the standard library — the sorting algorithm is written once but works with infinite types.

Benefits of Polymorphism

BenefitDescription
FlexibilityAdd new types without changing existing code
SimplicityOne interface for many implementations
MaintainabilityChanges in one class don’t affect others
TestabilityEasy to swap implementations for testing

🧪 Practice Exercise

Create a notification system that can send messages through different channels:
  1. EmailNotification: Sends via email
  2. SMSNotification: Sends via text message
  3. PushNotification: Sends to mobile app
  4. SlackNotification: Sends to Slack channel
All should have a send(message) method!
class Notification:
    def send(self, message):
        raise NotImplementedError

class EmailNotification(Notification):
    def __init__(self, email):
        # TODO
        pass
    
    def send(self, message):
        # TODO: Print sending email
        pass

# TODO: Add SMS, Push, Slack notifications

# Test with NotificationService
class NotificationService:
    def notify_all(self, channels, message):
        for channel in channels:
            channel.send(message)

# Test
service = NotificationService()
channels = [
    EmailNotification("user@example.com"),
    SMSNotification("+1234567890"),
    PushNotification("user_device_123"),
    SlackNotification("#general")
]
service.notify_all(channels, "Your order has shipped!")

Interview Insight

Key interview pattern: When designing any system in an LLD interview, polymorphism is your secret weapon for handling variability. The interviewer says “now what if we need to support a new notification channel?” If your design uses polymorphism (a NotificationChannel interface with send()), the answer is “just add a new class that implements the interface.” If your design uses if/elif chains, the answer is “modify existing code and hope nothing breaks.” The first answer demonstrates extensible design; the second demonstrates brittle design. Always structure your LLD answers so that new requirements are handled by adding new classes, not by modifying existing ones. That is polymorphism in action.

Interview Deep-Dive

Strong Answer:
  • Polymorphism is the mechanism that makes OCP work. When you define a NotificationChannel interface with send(message), the NotificationService iterates over channels and calls send() on each one. Adding a new channel (WhatsApp, Telegram, Discord) means creating a new class that implements the interface. The NotificationService never changes — it is closed for modification but open for extension.
  • Without polymorphism, adding a new channel means adding an elif branch to the service: “if channel == ‘whatsapp’: send_whatsapp()” — modifying existing code, risking regressions, and increasing cyclomatic complexity.
  • In the payment system from this page, the Checkout class calls payment_method.pay(amount) polymorphically. It works with CreditCard, PayPal, Crypto, and ApplePay without knowing or caring which one. When the business adds GooglePay next quarter, zero lines of Checkout code change.
  • This is why I always reach for an interface/ABC when I see behavior that varies across types. It is the single most impactful design decision in an LLD interview.
Follow-up: Is there a cost to polymorphism? When is a simple if/elif chain actually better?Polymorphism adds indirection: more classes, more files, and the actual implementation is resolved at runtime. For two or three types that rarely change, an if/elif chain is simpler, more readable, and faster to write. I reach for polymorphism when I expect the list of types to grow, when different teams own different implementations, or when I need to test each implementation in isolation. The crossover point is usually around three to four types.
Strong Answer:
  • In Java, polymorphism requires an explicit contract: the class must declare that it implements the interface. If MusicPlayer expects AudioFile, only classes that extend/implement AudioFile can be passed. The compiler enforces this at compile time.
  • In Python, duck typing means any object with a play() method can be passed to a function that calls play(), regardless of whether it formally implements an AudioFile interface. This is more flexible but less safe — you find type errors at runtime, not compile time.
  • For LLD design, I use ABCs (abstract base classes) to get the best of both worlds. By defining an AudioFile ABC with @abstractmethod play(), I get: (1) documentation of the contract, (2) an error at instantiation time if a subclass forgets to implement play(), and (3) compatibility with isinstance() checks and type hints.
  • The trade-off: ABCs add ceremony (import ABC, add decorators, define abstract methods) but provide safety and documentation. Pure duck typing is faster to write but harder for a new team member to understand — they must read the code to discover what methods are expected.
Follow-up: What is Python’s Protocol (from typing module) and when would you use it instead of ABC?Protocol provides structural subtyping — like duck typing but with static type checking. A class satisfies a Protocol if it has the required methods, without explicitly inheriting from it. Use Protocol when you want type checking for third-party classes you do not control (you cannot make them inherit from your ABC). Use ABC when you control the hierarchy and want runtime enforcement via isinstance().
Strong Answer:
  • This is a subtle LSP concern. The Shape contract implies area() returns the precise geometric area. Text’s implementation returns an approximation. If consumer code depends on precision (e.g., for physics simulation or cost calculation), Text violates the contract.
  • Solution 1: Separate the interfaces. Create a MeasurableArea interface for shapes with precise area calculations, and a separate Renderable interface for things that can be drawn. Text implements Renderable but not MeasurableArea. The total_area() function accepts only MeasurableArea objects, excluding Text.
  • Solution 2: Make the approximation explicit. Add a is_area_exact() method to the interface that returns True for geometric shapes and False for Text. The caller can decide whether to include approximate areas.
  • Solution 3: Use the Visitor pattern. A CanvasAreaCalculator visitor visits each element and decides how to handle it based on type — skipping Text or flagging it as approximate. This centralizes the decision logic.
  • The key insight: polymorphism works best when all implementations truly honor the same contract. When they do not, the right fix is to split the interface (ISP), not to paper over the difference.
Follow-up: This sounds like Interface Segregation. How do all four OOP pillars connect in a real design?They reinforce each other. Encapsulation protects each class’s internal state. Inheritance or composition defines the hierarchy. Polymorphism enables uniform treatment of different types. Abstraction defines the contracts that polymorphism relies on. When one pillar is weak (e.g., polymorphism with a leaky contract), the fix often comes from another pillar (abstraction via ISP). A senior engineer thinks about all four simultaneously, not in isolation.

🏃 Next Up: Abstraction

Now let’s learn how to hide complex details and show only what’s necessary!

Continue to Abstraction →

Learn how to hide complexity and create simple interfaces - like a car dashboard!