Polymorphism in Python — Missing .serialize() Export
A missing .
- Polymorphism: one interface, multiple behaviours
- Duck typing: any object with the right method qualifies — no inheritance needed
- Method overriding: subclasses replace or extend parent behaviour with same method name
- Operator overloading: define __add__, __eq__ etc. to make your objects work with Python syntax
- Performance insight: duck typing adds ~50ns per method call vs. direct calls — negligible until billions of calls
- Production insight: missing a duck‑typed method causes AttributeError at runtime, often far from the true source
- Biggest mistake: writing isinstance() chains instead of trusting duck typing — breaks extensibility
Imagine a universal TV remote. You press 'Volume Up' and it works whether the TV is a Samsung, Sony, or LG — you don't care what's inside the box, you just press the button and it responds correctly. Polymorphism is the same idea in code: one interface, many behaviours. The same function call or operator can produce different results depending on the object you hand it, without you needing to know what type that object is.
Most Python developers learn about classes and objects fairly quickly. But polymorphism is where OOP stops being a theoretical exercise and starts being genuinely useful in production code. It's the reason Django can swap database backends, why unittest works with any test class you throw at it, and how Python's built-in functions like len() and sorted() work seamlessly across dozens of different data types. Without polymorphism, you'd be writing a brittle forest of if/elif blocks just to handle different object types.
The problem polymorphism solves is coupling. When your code has to inspect an object's type before deciding what to do with it — isinstance() checks everywhere, type-specific branches all over the place — it becomes fragile. Add a new type and you have to hunt down every single branch. Polymorphism flips this: you define a contract (an interface, or simply a method name), and every object that honours that contract can be used interchangeably. Your calling code stays clean and stable even as the ecosystem of objects around it grows.
By the end of this article you'll understand Python's three main flavours of polymorphism — duck typing, method overriding through inheritance, and operator overloading — know exactly when to reach for each one, and have working code patterns you can drop into real projects today. You'll also know the gotchas that trip up intermediate developers and how to talk about polymorphism confidently in an interview.
Duck Typing — Python's Most Powerful (and Most Misunderstood) Form of Polymorphism
Duck typing comes from the phrase 'if it walks like a duck and quacks like a duck, it's a duck.' Python doesn't care about an object's class or inheritance chain. It cares whether the object has the method or attribute you're trying to call. That's it.
This is fundamentally different from Java or C#, where polymorphism typically requires a shared base class or interface declaration. In Python, the contract is implicit. Any object that implements the expected behaviour qualifies — no registration, no declaration needed.
This is why len() works on strings, lists, tuples, dicts, and any custom class that defines __len__. Python doesn't check types — it just calls the method. This design makes Python incredibly flexible for writing generic utilities that work across unrelated types.
The real-world payoff: you can write a function that processes any object with a .render() method — whether it's an HTML widget, a PDF template, or a console output formatter — and your function never needs to change as new types are added. That's open/closed principle in practice, powered by duck typing.
Method Overriding — Making Inheritance Actually Useful
Inheritance without polymorphism is just code reuse. Inheritance WITH polymorphism — method overriding — is where the real design power lives. When a subclass provides its own version of a method defined in a parent class, Python always calls the most specific version. This is method overriding, and it's the backbone of the Template Method and Strategy patterns.
The critical insight: the calling code doesn't need to change when you add a new subclass. You write code against the base class interface, and subclasses plug in seamlessly. This is the Open/Closed Principle — open for extension, closed for modification.
Python also gives you super() to call the parent's version when you want to extend rather than completely replace the parent's behaviour. Knowing when to extend vs. replace is a mark of an experienced developer.
A practical example: imagine a notification system. You have an abstract Notification base class with a .send() method. Email, SMS, and Slack subclasses each override .send() differently. The code that triggers notifications just calls .send() on whatever object it receives — it never needs to know which channel it's talking to.
super().__init__() in a subclass that adds its own __init__, you silently skip the parent's setup code. The object gets created without error, but self.recipient, self.timestamp and any other parent-set attributes won't exist — leading to confusing AttributeErrors later, not at the point of the mistake.super().__init__() is the most common production bug introduced by method overriding.super().__init__() as the first line of any overriding __init__ that extends the parent.super() is a bug waiting to happen.Operator Overloading — Teaching Python's Built-in Syntax to Understand Your Objects
When you write vector_a + vector_b or order_total > discount_threshold, Python is calling special dunder (double-underscore) methods behind the scenes. Operator overloading lets you define what those operators mean for your custom classes. This is polymorphism at the syntax level.
The + operator calls __add__, == calls __eq__, len() calls __len__, str() calls __str__, and so on. By implementing these, your objects participate in Python's native syntax seamlessly. They feel like built-in types.
This isn't just cosmetic. When your ShoppingCart supports len(), you can use it with Python's built-in sorted(), min(), max(), and any third-party library that expects standard Python behaviour. Your custom type becomes a first-class Python citizen.
The rule of thumb: implement dunder methods when your object represents a value or container that has a natural meaning for that operation. A Vector genuinely should support addition. A DatabaseConnection probably shouldn't define __add__ — that would be confusing, not clever.
Polymorphism with Abstract Base Classes — Explicit Contracts for Complex Systems
Duck typing works great for small utilities. But as your codebase grows, undocumented implicit contracts become a maintainability hazard. Abstract Base Classes (ABCs) solve this by making the contract explicit and enforceable at class creation time.
Using the abc module and @abstractmethod, you define a base class that cannot be instantiated directly. Any subclass must implement all abstract methods — Python raises TypeError at class creation if they're missing. This catches interface violations early, not at 2 AM in production.
Python also provides collections.abc — a set of ABCs for container types (Iterable, Sized, Mapping, etc.). Implementing these gives you standard methods (__iter__, __len__, __getitem__) for free, plus components like sorted() work automatically.
The trade-off: ABCs introduce a base class requirement. You lose the complete flexibility of duck typing. Choose ABCs when you control the hierarchy (e.g., a framework providing extension points) and duck typing when you don't (e.g., accepting arbitrary user-defined objects).
- ABCs: Explicit, enforceable at instantiation time. Good for framework code, public APIs, and team-owned hierarchies.
- Duck typing: Implicit, enforced at call time. Good for utility functions, third-party object integration, and small codebases.
- Hybrid approach: Use ABCs for your own base classes but accept duck-typing for parameters in public methods.
Polymorphism in Python's Standard Library — Lessons from Real Code
Python's standard library is a masterclass in polymorphism. The built-in functions len(), iter(), sorted(), reversed(), min(), max() — they all work through duck typing. They don't care about your object's type. They call __len__, __iter__, __lt__, and so on.
Consider sorted(). It accepts any iterable. Lists, tuples, dicts, sets, generators, custom iterables — all work. Why? Because sorted() doesn't check types. It calls iter() on the argument, which calls . Any object with __iter__() qualifies.__iter__()
This design pattern is called "protocol-based polymorphism." Python defines protocols (like Iterable, Sized, Callable) and any object that satisfies the protocol can be used in that context. It's the foundation of Python's flexibility.
Another example: the json module. json.dump() works with any object that implements .read() or .write() (file-like objects). You can pass it an open file, a StringIO, a BytesIO, or a custom class with .write() — it doesn't care.
Key lesson: when designing your own libraries, follow this pattern. Accept protocols, not concrete types. Your users will thank you.
hasattr() or use try/except and let them know what's expected.json.dump() in Python 3.6+ — the method must return the number of characters written.The Silent AttributeError: How a Missing .serialize() Broke Our Export Pipeline
super() and accidentally omitted the serialize method entirely.serialize() is missing, Python raises AttributeError. In this incident, the new CRM class had a serialize method that returned None because of a bug, but the symptom is similar. Let's refine: The new CRM class did not implement .serialize(); the export runner had a try/except that caught AttributeError but logged it at DEBUG level, which was not checked. So exports silently skipped the record and produced empty files.- Duck typing saves coupling but costs runtime safety — always use ABCs to enforce contracts across team boundaries.
- Never silence AttributeError in generic dispatch code. Log it loudly during development and at ERROR in production.
- Add integration tests that call every public method of every registered implementation with representative data.
hasattr() or try/except at the boundary, not scattered isinstance().Key takeaways
Common mistakes to avoid
3 patternsUsing isinstance() checks instead of embracing duck typing
hasattr() at the boundary — never scatter isinstance() throughout business logic.Forgetting to call super().__init__() in an overriding subclass
super().__init__(args, *kwargs) as the first line of a subclass __init__ that extends (rather than completely replaces) the parent's initialisation.Implementing __eq__ without also implementing __hash__
Interview Questions on This Topic
What is the difference between duck typing and traditional inheritance-based polymorphism, and when would you choose one over the other in Python?
Frequently Asked Questions
That's OOP in Python. Mark it forged?
5 min read · try the examples if you haven't