DEV Community

Rajesh Vemulakonda
Rajesh Vemulakonda

Posted on

Does Polymorphism Always Require Inheritance? A Python Perspective

Many developers learn polymorphism through inheritance and assume the two concepts are inseparable.

But does polymorphism actually require inheritance?

Python provides a surprising answer.

Introduction

Most OOP tutorials introduce polymorphism through inheritance using a base class, derived classes, and method overriding.

As a result, many developers naturally form a mental model like this:

Inheritance
      ↓
Polymorphism
Enter fullscreen mode Exit fullscreen mode

Over time, this often leads to a common assumption:

Polymorphism requires inheritance.

But is that really true?

Python approaches polymorphism differently.

In many cases, objects can participate in polymorphic behavior without inheriting from a common parent class. What matters is not where an object comes from, but what it can do.

This leads to an important realization:

Inheritance can enable polymorphism, but it is not always required.

In this article, we'll explore the relationship between inheritance and polymorphism and see how Python's duck typing allows polymorphism to exist even without inheritance.


The Common Assumption

If you've studied object-oriented programming, you've probably seen examples like this:

class Animal:
    def speak(self):
        pass


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


class Cat(Animal):
    def speak(self):
        print("Meow")
Enter fullscreen mode Exit fullscreen mode

A function can then work with any Animal object:

def make_sound(animal):
    animal.speak()
Enter fullscreen mode Exit fullscreen mode

Usage:

make_sound(Dog())
make_sound(Cat())
Enter fullscreen mode Exit fullscreen mode

Output:

Woof
Meow
Enter fullscreen mode Exit fullscreen mode

This is a classic example of polymorphism. The same function works with different object types, and each object provides its own implementation of speak().

Because examples like these are so common, many developers naturally form the following mental model:

Inheritance
      ↓
Method Overriding
      ↓
Polymorphism
Enter fullscreen mode Exit fullscreen mode

Over time, this often becomes:

Polymorphism requires inheritance.

The assumption is understandable because inheritance appears in almost every introductory explanation of polymorphism.

But inheritance and polymorphism are not the same thing.

Inheritance is one way to achieve polymorphism, but is it the reason polymorphism works?

Let's examine the traditional inheritance-based approach more closely.


Classic Polymorphism Through Inheritance

Traditionally, polymorphism is demonstrated using inheritance.

Consider the following example:

class Animal:

    def speak(self):
        raise NotImplementedError


class Dog(Animal):

    def speak(self):
        print("Woof")


class Cat(Animal):

    def speak(self):
        print("Meow")
Enter fullscreen mode Exit fullscreen mode

A function can work with any object derived from Animal:

def make_sound(animal):
    animal.speak()
Enter fullscreen mode Exit fullscreen mode

Usage:

make_sound(Dog())
make_sound(Cat())
Enter fullscreen mode Exit fullscreen mode

Output:

Woof
Meow
Enter fullscreen mode Exit fullscreen mode

The function doesn't need to know whether it received a Dog or a Cat. It simply calls:

animal.speak()
Enter fullscreen mode Exit fullscreen mode

Each object provides its own implementation, allowing the same operation to produce different behavior. That's the essence of polymorphism.

Here, inheritance provides a common interface that makes this possible.


Why This Works

Inheritance establishes a common parent class:

Animal
   │
   ├── Dog
   └── Cat
Enter fullscreen mode Exit fullscreen mode

Both Dog and Cat implement:

speak()
Enter fullscreen mode Exit fullscreen mode

allowing make_sound() to work with either object.

But this raises an important question:

Does make_sound() really require a common parent class, or does it simply require an object that provides speak()?

That distinction is important because if polymorphism is fundamentally about behavior, inheritance may not be the real reason the code works.

To answer that, we first need to understand what polymorphism really is.


What Is Polymorphism Really?

At its core, polymorphism is the ability of different objects to respond to the same operation in their own way.

For example:

class Dog:

    def speak(self):
        print("Woof")


class Cat:

    def speak(self):
        print("Meow")
Enter fullscreen mode Exit fullscreen mode

Both classes provide:

speak()
Enter fullscreen mode Exit fullscreen mode

but each produces different behavior.

Now consider this function:

def make_sound(animal):
    animal.speak()
Enter fullscreen mode Exit fullscreen mode

Usage:

make_sound(Dog())
make_sound(Cat())
Enter fullscreen mode Exit fullscreen mode

Output:

Woof
Meow
Enter fullscreen mode Exit fullscreen mode

The same function works with different objects because each provides its own implementation of speak(). That's polymorphism.

A useful way to think about it is:

Same Operation
       +
Different Implementations
       =
Polymorphism
Enter fullscreen mode Exit fullscreen mode

Notice that this definition says nothing about inheritance.

The important question is not:

Do these objects inherit from the same parent class?

Instead, ask:

Can these objects respond to the same operation?

If the answer is yes, polymorphism is possible.


The Key Idea

Many developers think of polymorphism like this:

Inheritance
      ↓
Polymorphism
Enter fullscreen mode Exit fullscreen mode

A more accurate view is:

Same Operation
       +
Different Implementations
       =
Polymorphism
Enter fullscreen mode Exit fullscreen mode

The focus is not on whether objects share a parent class, but on whether they can respond to the same operation.


Polymorphism Is About Behavior

Consider a notification system:

class EmailNotifier:
    def send(self):
        print("Sending email")


class SMSNotifier:
    def send(self):
        print("Sending SMS")


class PushNotifier:
    def send(self):
        print("Sending push notification")
Enter fullscreen mode Exit fullscreen mode

A generic function can work with all of them:

def notify(notifier):
    notifier.send()
Enter fullscreen mode Exit fullscreen mode

The function only cares that the object provides:

send()
Enter fullscreen mode Exit fullscreen mode

This illustrates an important point: polymorphism depends on behavior, not the specific type of object.

If behavior is what matters, do we really need inheritance to achieve polymorphism?

Python's answer is:

Not always.

And that's where duck typing enters the picture.


Polymorphism Through Duck Typing

One of Python's most distinctive features is duck typing, often summarized as:

If it walks like a duck and quacks like a duck, then it can be treated as a duck.

Instead of focusing on an object's inheritance hierarchy, Python focuses on its behavior.

In other words, the question becomes:

Does this object provide the behavior I need?


A Polymorphic Example Without Inheritance

Consider the following classes:

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


class Cat:
    def speak(self):
        print("Meow")


class Cow:
    def speak(self):
        print("Moo")
Enter fullscreen mode Exit fullscreen mode

Notice that none of these classes inherit from a common parent.

A generic function can still work with all of them:

def make_sound(animal):
    animal.speak()
Enter fullscreen mode Exit fullscreen mode

Usage:

make_sound(Dog())
make_sound(Cat())
make_sound(Cow())
Enter fullscreen mode Exit fullscreen mode

Output:

Woof
Meow
Moo
Enter fullscreen mode Exit fullscreen mode

The function works because every object provides speak(). It depends on behavior, not inheritance.


Adding New Types Requires No Changes

Suppose we need to support ducks.

We can simply add:

class Duck:
    def speak(self):
        print("Quack")
Enter fullscreen mode Exit fullscreen mode

Usage:

make_sound(Duck())
Enter fullscreen mode Exit fullscreen mode

Output:

Quack
Enter fullscreen mode Exit fullscreen mode

No changes are required to make_sound(). We simply introduce another object that provides the expected behavior.


The Real Source of Polymorphism

Notice that make_sound() never checks whether an object inherits from Animal, for example:

isinstance(animal, Animal)
Enter fullscreen mode Exit fullscreen mode

Its only requirement is that the object supports:

animal.speak()
Enter fullscreen mode Exit fullscreen mode

This reveals an important insight:

The true source of polymorphism is shared behavior, not shared ancestry.

Inheritance can provide that behavior, but so can duck typing.


At this point, we can answer the original question:

Does polymorphism always require inheritance?

In Python, the answer is:

No.

Different objects can participate in polymorphic behavior as long as they support the same operation, even without an inheritance relationship.


When Inheritance Is Still Useful

At this point, it might be tempting to conclude:

If polymorphism can exist without inheritance, then inheritance is unnecessary.

That would be the wrong conclusion.

The lesson is not:

Avoid inheritance
Enter fullscreen mode Exit fullscreen mode

The lesson is:

Use inheritance when it solves a problem
Enter fullscreen mode Exit fullscreen mode

Shared Behavior

Consider the following example:

class Animal:

    def eat(self):
        print("Eating")

    def sleep(self):
        print("Sleeping")


class Dog(Animal):

    def speak(self):
        print("Woof")


class Cat(Animal):

    def speak(self):
        print("Meow")
Enter fullscreen mode Exit fullscreen mode

Here, Dog and Cat inherit common behavior from Animal instead of duplicating the same code, improving maintainability and reducing repetition.


"Is-A" Relationships

Inheritance is often appropriate when one type is a specialized form of another:

Dog is an Animal

Manager is an Employee

SavingsAccount is an Account
Enter fullscreen mode Exit fullscreen mode

In such cases, inheritance naturally models the domain.


Inheritance and Polymorphism Can Work Together

Inheritance and polymorphism often complement each other:

class PaymentMethod:

    def process(self, amount):
        raise NotImplementedError


class CreditCard(PaymentMethod):

    def process(self, amount):
        print(f"Processing {amount} using credit card")


class UPI(PaymentMethod):

    def process(self, amount):
        print(f"Processing {amount} using UPI")
Enter fullscreen mode Exit fullscreen mode

A generic function can work with any payment method:

def make_payment(method, amount):
    method.process(amount)
Enter fullscreen mode Exit fullscreen mode

This is a perfectly valid example of inheritance-based polymorphism.


The Real Lesson

Inheritance and polymorphism are related, but they solve different problems:

Inheritance
      ↓
Code Reuse and Relationships

Polymorphism
      ↓
Behavioral Flexibility
Enter fullscreen mode Exit fullscreen mode

Inheritance can enable polymorphism, but it is not a prerequisite for it.

Understanding this distinction helps us choose the right tool for the problem at hand.

Key Takeaways

Let's return to the original question:

Does polymorphism always require inheritance?

The answer is:

No. Polymorphism does not require inheritance.

Throughout this article, we've seen that:

Inheritance and polymorphism are different concepts.

Inheritance models relationships and promotes code reuse, while polymorphism enables different objects to respond to the same operation.


Inheritance can enable polymorphism.

Traditional OOP often achieves polymorphism through inheritance and method overriding.


Duck typing also enables polymorphism.

In Python, objects don't need a common parent class—they simply need to provide the expected behavior.


Behavior is the key.

Same Operation
       +
Different Implementations
       =
Polymorphism
Enter fullscreen mode Exit fullscreen mode

Focus on what objects can do, not necessarily what they inherit from.


Inheritance is still valuable.

Use inheritance to model relationships, share behavior, and reduce duplication.


The Most Important Insight

Many developers start with this mental model:

Inheritance
      ↓
Polymorphism
Enter fullscreen mode Exit fullscreen mode

A more accurate view is:

Inheritance
      ↓
One Way to Enable Polymorphism
Enter fullscreen mode Exit fullscreen mode

That's perhaps the most important lesson Python teaches about polymorphism.


Conclusion

Polymorphism is often taught through inheritance, making it easy to assume the two concepts are inseparable.

As we've seen, inheritance is one way to enable polymorphism—but it is not the only way.

Python demonstrates this particularly well through duck typing, where objects can participate in polymorphic behavior simply by providing the expected methods.

This leads to an important realization:

Polymorphism is fundamentally about behavior, not inheritance.

Inheritance remains a valuable tool for code reuse and modeling relationships between classes. It should be used because it solves those problems—not because polymorphism depends on it.

The next time you design a system, ask yourself:

Do these objects need a common parent class, or do they simply need to provide the same behavior?

Perhaps that's the most Pythonic lesson of all:

What an object can do is often more important than what it inherits from.


Top comments (0)