AlgoMaster Logo

Polymorphism

High Priority18 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Polymorphism is the idea that one piece of code can work with many different types as long as they behave the right way. A function that prints an order summary should not care whether the items are books, T-shirts, or downloadable apps; it should just ask each item for its price and shipping cost. This lesson covers what polymorphism means in Python, how duck typing makes it cheap to achieve, when isinstance() is the right tool and when it gets in the way, and the EAFP style that ties it all together.

One Interface, Many Implementations

Polymorphism literally means "many shapes". In code, it means writing one piece of logic that handles objects of different types as long as each type supports the same operations. The caller asks for a behavior; each type decides how to do it.

Picture an online store with several kinds of products: physical books that ship in a box, T-shirts that need a different box size, and digital downloads that ship through the internet. Each one calculates shipping differently, but the checkout code shouldn't have to know that. It just wants a number.

Notice the sum expression. It loops over a list that mixes three completely different classes and calls calculate_shipping() on each one. The loop has no idea what type it's holding. It just relies on every item answering the same call. That is polymorphism in one sentence: the call site stays the same; the called code varies.

The opposite would be a checkout function that asks "what kind of thing are you?" and branches on the answer. We'll see that style later in this lesson, and we'll see why it ages badly the moment you add a fourth product type.

The diagram shows the shape every polymorphic design takes: one call site (orange) routes to one of several implementations (green) depending on the runtime type of the item, and the caller collects the results without caring which branch ran. Adding a fifth product type means writing one more class with a calculate_shipping method; the loop never changes.

Duck Typing: If It Walks Like a Duck

The classic phrasing goes: "If it walks like a duck and quacks like a duck, it's a duck." Python takes that seriously. It doesn't check whether an object's type sits in some hierarchy; it checks whether the object can do what you're asking. If you call .calculate_shipping() and the object responds with a number, Python doesn't care what class it came from.

This style is called duck typing, and it's the default way Python does polymorphism. There's no interface keyword you have to declare, no implements clause to add to a class. Two unrelated classes can both have a calculate_shipping method, and any code that needs that method will work with either one.

Book, CoffeeMug, and Subscription share no parent class. They aren't related at all in the class hierarchy. But shipping_estimate works with all three because all three quack the right way. The "interface" here is implicit: it's the set of methods the caller actually calls. As long as an object has those methods, it fits.

The other side of this is that Python won't catch a missing method until you actually run the call. If you pass an object that doesn't have calculate_shipping, you get an AttributeError at the moment of the call, not at the moment of construction:

That's the trade-off duck typing makes. You get flexibility (any class that fits the protocol works, no inheritance required) in exchange for losing static guarantees (a wrong type only shows up at call time). In practice, Python developers lean on tests and type hints to recover the safety, and most of the time it pays off.

Method Overriding in Subclasses

Polymorphism doesn't require inheritance, but inheritance is one of the most common ways to set it up. When a subclass defines a method with the same name as a parent's method, the subclass's version takes over for instances of the subclass. This is method overriding, and here we look at how it powers polymorphism.

Three different calculate_shipping methods get called inside the same loop. Python picks the right one for each object by looking at the object's actual class, not the class the variable was "supposed" to hold. That last point is the whole game: Python dispatches methods on the runtime type, not on any declared type.

There's no "virtual" keyword to flip, no special syntax to opt in. Every method in Python is overridable by default, and every method call goes through this dynamic lookup. If you've used languages where you have to mark methods as virtual or abstract to get this behavior, Python's approach can feel almost too easy. The cost is the one we already saw: a typo in a method name doesn't trip a compiler error, because there is no compiler in the usual sense.

The diagram captures the runtime lookup: the call site asks for calculate_shipping, Python checks the object's actual class to find a definition, and the matching version runs. The decision happens fresh on every call, which is why swapping out the object swaps out the behavior with no extra ceremony.

Inheritance Polymorphism vs Duck Typing

Both approaches give you polymorphism, but they reach it from different directions. Inheritance says "these classes share a parent, so they share a method". Duck typing says "these classes share a method, that's all I need". Python supports both and tends to prefer the second.

Compare two versions of the same idea. First, an inheritance-based design where every product is a subclass of Product:

This works. Every subclass overrides the placeholder method, and the function gets the right number. It's also explicit about the contract: anyone reading Product sees the method that subclasses are expected to define.

Now the duck-typed version. Same outcome, no shared parent:

Identical result. total_shipping doesn't ask anything about the types; it just calls the method. Adding a Subscription class to the mix needs zero work in the existing code, no parent class to extend, no registry to update.

ApproachProsCons
Inheritance polymorphismExplicit contract in the parent class; static checkers can verify the interface; shared default behavior is easy.Forces a class hierarchy; classes from different libraries can't easily share an interface.
Duck typingNo hierarchy needed; any class with the right methods fits; great for mixing types from unrelated libraries.Contract is implicit; a missing method only shows up at call time.

Python developers generally reach for duck typing first and add a base class only when one of these is true: there's real shared code (not just a shared method name) that belongs in a parent, the contract is complex enough that writing it down helps, or you need isinstance checks for some external reason (covered in the next section). Abstract base classes formalize the "write it down" case.

Said plainly: in Python, the question "do these objects share a parent?" matters less than the question "do these objects share the methods I need?" The second question is the one most code actually has to answer.

isinstance() and issubclass()

Python gives you two builtins for type checking. isinstance(obj, cls) returns True if obj is an instance of cls (or any subclass). issubclass(sub, parent) returns True if sub is parent or a subclass of parent.

Both functions accept a tuple as the second argument to test against several types at once: isinstance(value, (int, float)) returns True if value is a number.

These tools have real uses. The two most common are:

  1. Reacting to truly different kinds of inputs at an API boundary, for example, accepting either a single product or a list of products and normalizing them.
  2. Friendly error messages or fast-path optimizations where knowing the concrete type matters.

That's a legitimate use. The function genuinely needs to branch on the input's shape, and the branches are short and don't grow.

The misuse is dispatching on type to pick which method to call:

This is the anti-pattern duck typing was designed to replace. The polymorphic version is what we wrote earlier: every product class has its own calculate_shipping, and the caller just calls it. The next section digs into why the isinstance chain hurts.

Type-Based Dispatch vs Polymorphic Methods

The "long chain of isinstance checks" pattern shows up in almost every codebase that hasn't fully embraced polymorphism yet. It works at first, but it ages badly. Each time someone adds a new product type, they have to find every chain of checks and add another branch. Miss one, and the new type silently falls through to an error or, worse, the wrong default.

Here's the type-dispatched version of a checkout function:

Now imagine the team adds gift cards. Then subscription boxes. Then refurbished electronics. Each addition means hunting for every chain of isinstance checks across the codebase, adding a branch, hoping nothing was missed. The shipping function might have one chain. The tax function probably has another. The receipt printer has a third. The chains drift apart, and one day a new product type prints a correct shipping cost but the wrong tax line.

The polymorphic version puts each type's behavior on the type itself:

Same answer, but the structure is different in an important way. Adding a gift card now means writing one class with one calculate_shipping method. No existing code changes. The caller's loop doesn't grow. The function isn't a list of ifs waiting to forget the new case. The knowledge of "how does this product calculate shipping?" lives next to the data it depends on.

PatternWhere the type knowledge livesWhat happens when you add a new type
Type-based dispatch (isinstance chain)In every caller that branches on typeEdit every chain in the codebase; hope none are missed
Polymorphic methodsOn the type itselfAdd one class with the method; callers unchanged

The polymorphic version isn't always the right call. If the behavior really depends on two things at once (the product type and, say, the destination country), no single method can own the whole decision, and you may need a different pattern. But for the common case (one operation, behavior that varies by type) putting the method on the type is the cleaner default, and it's the path Python is built to support.

EAFP vs LBYL

Two style names show up constantly in Python writing: EAFP ("easier to ask forgiveness than permission") and LBYL ("look before you leap"). They describe two ways to handle the question "will this operation work?" EAFP just tries the operation and catches the exception if it fails. LBYL checks first and only attempts the operation if the check passes.

These styles connect to polymorphism because they're the practical face of duck typing. If you're going to trust that an object has a charge method, the most Pythonic way to use it is to call the method and handle the failure if it isn't there, rather than checking ahead of time.

Compare both styles for processing a payment:

The LBYL version checks before calling:

The EAFP version just tries and handles the failure:

Same outcome, different style. Python prefers EAFP for several reasons:

  • It plays well with duck typing. EAFP doesn't care what type the object is, only what it can do. The LBYL check has to know what to look for and can miss things (a property that raises on access, a __getattr__ that lies about hasattr).
  • It avoids a race condition window. With LBYL, between the check and the call, something else could change the object's state. With EAFP, the check and the call are the same operation.
  • It's often faster on the happy path. The check costs something every call, the exception costs nothing unless it fires, and the happy path is usually most of the calls.

The simple "does this file exist before I open it?" example shows EAFP's case clearly:

That code has a bug. Between the exists check and the open, the file could be deleted, renamed, or have its permissions changed. The check passed, the open still fails.

The EAFP version has no race condition, no double work, and reads like the actual intent: "read the file; if there isn't one, use an empty string."

StyleWhen to prefer
EAFP (try/except)Duck typing, file/network operations, anywhere a check and an action could disagree, most Python code by default.
LBYL (check first)When the check is much cheaper than the operation, when failure is the common case, when you need to validate input before any side effects.

EAFP is Python's default for a reason: it matches the language's dynamic nature and avoids redundant checks. LBYL still has a place, but it should be a deliberate choice, not the reflex.

Quiz

Polymorphism Quiz

10 quizzes