Skip to main content

🦎 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

🔊 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)

✅ 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!")

🏃 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!