Python Polymorphism

Learn about polymorphism in Python - the ability of different objects to respond to the same interface.

What is Polymorphism?

Polymorphism is a core concept in object-oriented programming that allows objects of different types to be treated as objects of a common base type.

The word "polymorphism" comes from Greek, meaning "many forms". In programming, it refers to the ability of different objects to respond to the same method call in their own specific way.

Python supports polymorphism through method overriding, duck typing, and operator overloading.

Method Polymorphism

Different classes can have methods with the same name, but different implementations:

Example - Basic polymorphism with animals:

class Dog:
    def make_sound(self):
        return "Woof!"
    
    def move(self):
        return "Running on four legs"

class Cat:
    def make_sound(self):
        return "Meow!"
    
    def move(self):
        return "Sneaking quietly"

class Bird:
    def make_sound(self):
        return "Tweet!"
    
    def move(self):
        return "Flying in the sky"

# Polymorphic function
def animal_sounds(animals):
    for animal in animals:
        print(f"Animal says: {animal.make_sound()}")
        print(f"Animal moves: {animal.move()}")
        print()

# Usage
dog = Dog()
cat = Cat()
bird = Bird()

animals = [dog, cat, bird]
animal_sounds(animals)

Polymorphism with Inheritance

Polymorphism is often used with inheritance, where child classes override parent methods:

Example - Shape hierarchy with polymorphism:

import math

class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        raise NotImplementedError("Subclass must implement area method")
    
    def perimeter(self):
        raise NotImplementedError("Subclass must implement perimeter method")
    
    def describe(self):
        return f"This is a {self.name}"

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius
    
    def area(self):
        return math.pi * self.radius ** 2
    
    def perimeter(self):
        return 2 * math.pi * self.radius

class Triangle(Shape):
    def __init__(self, side1, side2, side3):
        super().__init__("Triangle")
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3
    
    def area(self):
        # Using Heron's formula
        s = self.perimeter() / 2
        return math.sqrt(s * (s - self.side1) * (s - self.side2) * (s - self.side3))
    
    def perimeter(self):
        return self.side1 + self.side2 + self.side3

# Polymorphic function
def calculate_shape_properties(shapes):
    total_area = 0
    total_perimeter = 0
    
    for shape in shapes:
        print(shape.describe())
        area = shape.area()
        perimeter = shape.perimeter()
        print(f"Area: {area:.2f}")
        print(f"Perimeter: {perimeter:.2f}")
        print()
        
        total_area += area
        total_perimeter += perimeter
    
    print(f"Total area: {total_area:.2f}")
    print(f"Total perimeter: {total_perimeter:.2f}")

# Usage
rectangle = Rectangle(5, 3)
circle = Circle(4)
triangle = Triangle(3, 4, 5)

shapes = [rectangle, circle, triangle]
calculate_shape_properties(shapes)

Duck Typing

Python uses "duck typing" - if it walks like a duck and quacks like a duck, it's a duck. Objects don't need to inherit from the same class to be used polymorphically:

Example - Duck typing with different classes:

class Duck:
    def fly(self):
        return "Duck flying"
    
    def swim(self):
        return "Duck swimming"

class Airplane:
    def fly(self):
        return "Airplane flying"

class Fish:
    def swim(self):
        return "Fish swimming"

class Boat:
    def swim(self):
        return "Boat floating on water"

# Polymorphic functions using duck typing
def make_it_fly(flying_object):
    try:
        return flying_object.fly()
    except AttributeError:
        return f"{type(flying_object).__name__} can't fly"

def make_it_swim(swimming_object):
    try:
        return swimming_object.swim()
    except AttributeError:
        return f"{type(swimming_object).__name__} can't swim"

# Usage
duck = Duck()
airplane = Airplane()
fish = Fish()
boat = Boat()

flying_things = [duck, airplane, fish]
swimming_things = [duck, fish, boat, airplane]

print("Flying:")
for thing in flying_things:
    print(make_it_fly(thing))

print("\nSwimming:")
for thing in swimming_things:
    print(make_it_swim(thing))

Operator Overloading

Python allows you to define how operators work with your custom objects through special methods:

Example - Vector class with operator overloading:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    def __sub__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x - other.x, self.y - other.y)
        return NotImplemented
    
    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented
    
    def __rmul__(self, scalar):
        return self.__mul__(scalar)
    
    def __eq__(self, other):
        if isinstance(other, Vector):
            return self.x == other.x and self.y == other.y
        return False
    
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    
    def __bool__(self):
        return self.x != 0 or self.y != 0

# Usage
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v1 + v2: {v1 + v2}")
print(f"v1 - v2: {v1 - v2}")
print(f"v1 * 3: {v1 * 3}")
print(f"2 * v1: {2 * v1}")
print(f"v1 == v2: {v1 == v2}")
print(f"abs(v1): {abs(v1)}")
print(f"bool(v1): {bool(v1)}")
print(f"bool(Vector(0, 0)): {bool(Vector(0, 0))}")

Abstract Base Classes

Use abstract base classes to define interfaces that ensure polymorphic behavior:

Example - Payment processing system:

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass
    
    @abstractmethod
    def validate_payment_info(self):
        pass
    
    def log_transaction(self, amount, status):
        print(f"Transaction: ${amount:.2f} - Status: {status}")

class CreditCardProcessor(PaymentProcessor):
    def __init__(self, card_number, cvv, expiry_date):
        self.card_number = card_number
        self.cvv = cvv
        self.expiry_date = expiry_date
    
    def validate_payment_info(self):
        # Simplified validation
        return len(self.card_number) == 16 and len(self.cvv) == 3
    
    def process_payment(self, amount):
        if self.validate_payment_info():
            # Simulate payment processing
            self.log_transaction(amount, "SUCCESS")
            return True
        else:
            self.log_transaction(amount, "FAILED - Invalid card info")
            return False

class PayPalProcessor(PaymentProcessor):
    def __init__(self, email, password):
        self.email = email
        self.password = password
    
    def validate_payment_info(self):
        return "@" in self.email and len(self.password) >= 8
    
    def process_payment(self, amount):
        if self.validate_payment_info():
            self.log_transaction(amount, "SUCCESS")
            return True
        else:
            self.log_transaction(amount, "FAILED - Invalid PayPal credentials")
            return False

class BankTransferProcessor(PaymentProcessor):
    def __init__(self, account_number, routing_number):
        self.account_number = account_number
        self.routing_number = routing_number
    
    def validate_payment_info(self):
        return len(self.account_number) >= 8 and len(self.routing_number) == 9
    
    def process_payment(self, amount):
        if self.validate_payment_info():
            self.log_transaction(amount, "SUCCESS")
            return True
        else:
            self.log_transaction(amount, "FAILED - Invalid bank info")
            return False

# Polymorphic payment processing
def process_payments(processors, amount):
    successful_payments = 0
    
    for processor in processors:
        print(f"\nProcessing payment with {type(processor).__name__}")
        if processor.process_payment(amount):
            successful_payments += 1
    
    print(f"\nSuccessful payments: {successful_payments}/{len(processors)}")

# Usage
credit_card = CreditCardProcessor("1234567890123456", "123", "12/25")
paypal = PayPalProcessor("user@example.com", "password123")
bank_transfer = BankTransferProcessor("12345678", "123456789")

processors = [credit_card, paypal, bank_transfer]
process_payments(processors, 100.00)

Polymorphism with Built-in Functions

Many built-in Python functions work polymorphically with different types:

Example - len() function polymorphism:

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __len__(self):
        return len(self.songs)
    
    def __str__(self):
        return f"Playlist '{self.name}' with {len(self)} songs"

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    
    def __len__(self):
        return self.pages
    
    def __str__(self):
        return f"Book '{self.title}' with {len(self)} pages"

# Polymorphic use of len()
items = [
    [1, 2, 3, 4, 5],  # List
    "Hello World",    # String
    {"a": 1, "b": 2}, # Dictionary
    Playlist("My Favorites"),  # Custom class
    Book("Python Guide", 300)  # Custom class
]

# Add some songs to playlist
items[3].add_song("Song 1")
items[3].add_song("Song 2")
items[3].add_song("Song 3")

for item in items:
    print(f"Length of {type(item).__name__}: {len(item)}")
    if hasattr(item, '__str__'):
        print(f"  Details: {item}")
    print()

Real-World Example: Media Player

Complete polymorphism example:

from abc import ABC, abstractmethod
import time

class MediaFile(ABC):
    def __init__(self, filename, duration):
        self.filename = filename
        self.duration = duration
        self.position = 0
        self.is_playing = False
    
    @abstractmethod
    def play(self):
        pass
    
    @abstractmethod
    def get_info(self):
        pass
    
    def pause(self):
        self.is_playing = False
        return f"Paused {self.filename}"
    
    def stop(self):
        self.is_playing = False
        self.position = 0
        return f"Stopped {self.filename}"
    
    def seek(self, position):
        if 0 <= position <= self.duration:
            self.position = position
            return f"Seeked to {position}s in {self.filename}"
        return "Invalid position"

class AudioFile(MediaFile):
    def __init__(self, filename, duration, artist, album):
        super().__init__(filename, duration)
        self.artist = artist
        self.album = album
    
    def play(self):
        self.is_playing = True
        return f"Playing audio: {self.filename} by {self.artist}"
    
    def get_info(self):
        return {
            "type": "Audio",
            "filename": self.filename,
            "artist": self.artist,
            "album": self.album,
            "duration": self.duration
        }

class VideoFile(MediaFile):
    def __init__(self, filename, duration, resolution, fps):
        super().__init__(filename, duration)
        self.resolution = resolution
        self.fps = fps
    
    def play(self):
        self.is_playing = True
        return f"Playing video: {self.filename} ({self.resolution})"
    
    def get_info(self):
        return {
            "type": "Video",
            "filename": self.filename,
            "resolution": self.resolution,
            "fps": self.fps,
            "duration": self.duration
        }

class PodcastFile(MediaFile):
    def __init__(self, filename, duration, host, episode_number):
        super().__init__(filename, duration)
        self.host = host
        self.episode_number = episode_number
    
    def play(self):
        self.is_playing = True
        return f"Playing podcast: Episode {self.episode_number} hosted by {self.host}"
    
    def get_info(self):
        return {
            "type": "Podcast",
            "filename": self.filename,
            "host": self.host,
            "episode": self.episode_number,
            "duration": self.duration
        }

class MediaPlayer:
    def __init__(self):
        self.playlist = []
        self.current_index = 0
    
    def add_media(self, media_file):
        self.playlist.append(media_file)
    
    def play_current(self):
        if self.playlist and 0 <= self.current_index < len(self.playlist):
            current_media = self.playlist[self.current_index]
            return current_media.play()
        return "No media to play"
    
    def next_track(self):
        if self.current_index < len(self.playlist) - 1:
            self.current_index += 1
            return self.play_current()
        return "End of playlist"
    
    def previous_track(self):
        if self.current_index > 0:
            self.current_index -= 1
            return self.play_current()
        return "Beginning of playlist"
    
    def show_playlist(self):
        print("Playlist:")
        for i, media in enumerate(self.playlist):
            marker = "▶ " if i == self.current_index else "  "
            info = media.get_info()
            print(f"{marker}{i+1}. {info['filename']} ({info['type']})")
    
    def get_current_info(self):
        if self.playlist and 0 <= self.current_index < len(self.playlist):
            return self.playlist[self.current_index].get_info()
        return None

# Usage
player = MediaPlayer()

# Add different types of media files
audio = AudioFile("song.mp3", 180, "The Beatles", "Abbey Road")
video = VideoFile("movie.mp4", 7200, "1920x1080", 24)
podcast = PodcastFile("tech_talk.mp3", 3600, "John Doe", 42)

player.add_media(audio)
player.add_media(video)
player.add_media(podcast)

# Polymorphic operations
print(player.play_current())
print()

player.show_playlist()
print()

print("Current media info:")
current_info = player.get_current_info()
for key, value in current_info.items():
    print(f"  {key}: {value}")
print()

print(player.next_track())
print(player.next_track())
print(player.previous_track())

Benefits of Polymorphism

  • Code Reusability: Write functions that work with multiple types
  • Flexibility: Easy to add new types without changing existing code
  • Maintainability: Changes to specific implementations don't affect client code
  • Extensibility: New classes can be added that work with existing polymorphic functions
  • Abstraction: Client code works with interfaces rather than concrete implementations