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("[email protected]"), 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("[email protected]"),
    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!