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

Duck Typing

Python cares about behaviour, not type: "if it walks like a duck and quacks like a duck, it's a duck." Any object with the right method works — no shared base class required.

class Dog:
    def speak(self): return "Woof"
class Cat:
    def speak(self): return "Meow"
class Duck:
    def speak(self): return "Quack"

for animal in (Dog(), Cat(), Duck()):
    print(animal.speak())   # Woof / Meow / Quack -> same call, different result

Operator and Function Polymorphism

print(1 + 2)          # 3       -> numeric addition
print("a" + "b")      # ab      -> string concatenation
print([1] + [2])      # [1, 2]  -> list joining

print(len("hello"))   # 5   -> works on str
print(len([1, 2, 3])) # 3   -> and on list

The same + operator and len() function behave appropriately for each type — polymorphism built into the language.

Try It Yourself

Exercise 1: Write a function describe(shape) that calls shape.area(), and pass it two different shape classes.

Show solution
class Square:
    def __init__(self, s): self.s = s
    def area(self): return self.s ** 2
class Circle:
    def __init__(self, r): self.r = r
    def area(self): return 3.14 * self.r ** 2

def describe(shape):
    print("area:", shape.area())

describe(Square(4))   # area: 16
describe(Circle(2))   # area: 12.56

Key Takeaways

  • Polymorphism = one interface, many implementations.
  • Duck typing: an object fits if it has the needed methods.
  • Operators and built-ins like + and len() are polymorphic.

📘 Real-World Deep Dive

Polymorphism is the practice of writing code that doesn't care which concrete type it's operating on. Python's duck typing makes this almost free — define the right method, and every other class implements <em>its own version</em>.

Real-Life Scenario

A pluggable exporter: any class that implements <code>render(rows)</code> can be plugged in without changing the caller.

Real-Life Example

from typing import Protocol
from dataclasses import dataclass

class Renderable(Protocol):
    def render(self, rows: list[list[str]]) -> str: ...

@dataclass
class CsvRenderer:
    newline: str = "\n"
    def render(self, rows: list[list[str]]) -> str:
        return self.newline.join(",".join(r) for r in rows)

@dataclass
class MarkdownTableRenderer:
    def render(self, rows: list[list[str]]) -> str:
        head = ["---"] * len(rows[0])
        body = "\n".join("| " + " | ".join(r) + " |" for r in rows[1:])
        return "| " + " | ".join(rows[0]) + " |\n|" + " |".join(head) + " |\n" + body

@dataclass
class JsonRenderer:
    indent: int = 2
    def render(self, rows: list[list[str]]) -> str:
        import json
        return json.dumps([dict(zip(rows[0], r)) for r in rows[1:]], indent=self.indent)

def export(renderer: Renderable, rows: list[list[str]]) -> str:
    return renderer.render(rows)

data = [["name", "age"], ["Ada", "29"], ["Bo", "41"]]
for r in [CsvRenderer(), MarkdownTableRenderer(), JsonRenderer()]:
    print("-" * 30)
    print(export(r, data))

Expected Output

------------------------------
name,age
Ada,29
Bo,41
------------------------------
| name | age |
| --- | --- |
| Ada | 29 |
| Bo | 41 |
------------------------------
[
  {
    "name": "name",
    "age": "age"
  },
  ...
]

Common mistakes

  • Over-broad interfaces are worse than narrow ones; Protocol shines when the contract is small.
  • 'Abstract base class' overload is mostly useful as documentation — Duck typing wins when there's no isinstance check.
  • Mixing composition with aggressive inheritance is a recipe for the diamond problem.

🚀 Performance & Best Practices

  • Python method dispatch is fast; polymorphism overhead is negligible compared to I/O.
  • functools.singledispatch is great for type-based dispatch on the first argument.
  • Typed protocols (runtime_checkable) add an isinstance check that's normally fine to skip.

🧪 Try It Yourself

  1. Add a YamlRenderer using pyyaml.
  2. Wrap exports to be retryable with exponential backoff on transient write errors.
  3. Side-by-side benchmark a match-based dispatcher vs. if/else for 5 renderers.

FAQ: Python Polymorphism

Common questions about this page.

What is Python Polymorphism?

Python Polymorphism is a Python Tutorial lesson that explains python polymorphism in Python. Learn about polymorphism in Python - the ability of different objects to respond to the same interface. Copy the samples and run them in the Python editor. It is written for beginners who want a clear definition and working examples.

Should I run python polymorphism examples locally for better learning?

Yes. Use the browser editor on StudyGrid for a quick check, then Download the example and run it on your computer. Local runs show real errors and the real toolchain, which is one of the fastest ways to learn python polymorphism in this Python Tutorial Python lesson (Python Polymorphism).

How do I use python polymorphism in Python?

To use python polymorphism in Python, follow the examples on this StudyGrid page. Copy a snippet, run it in the browser, then Download and run it locally for better learning. Change the values and compare the output.

What is the syntax of python polymorphism?

This Python Polymorphism tutorial shows python polymorphism syntax with short Python examples. Use the code blocks in this lesson for the exact statements, then try them in your editor.

Python Polymorphism example for beginners

Yes. This page includes a beginner python polymorphism example you can copy and run. It is designed for searches such as "python polymorphism for beginners", "python polymorphism example", and "how to use python polymorphism".

What are common mistakes with python polymorphism?

Common python polymorphism mistakes include wrong syntax, mixing types, and skipping practice. Work through this Python Tutorial chapter in order, run every example, and check the output before moving on.

Why should I learn python polymorphism?

Python Polymorphism is used in real Python work. Learning python polymorphism helps you write clearer programs and continue the Python Tutorial tutorial on StudyGrid.

Is Python Polymorphism free to learn online?

Yes. You can learn python polymorphism free on StudyGrid (studygrid.in). This chapter is part of the Python Tutorial path and includes examples, syntax, and next-step links.