Worked examples — Polymorphism — duck typing, same interface different behavior
You have met polymorphism as an idea: one call, many behaviors. This page drills every corner case so that when you meet a duck-typing situation in the wild — or an exam — you have already walked through its exact shape.
We treat "polymorphism" the way an engineer treats a machine: not "does it work in the happy case?" but "does it survive every input the world can throw?" So first we list every kind of case, then we solve one example per case.
Two anti-patterns will recur below. They come from the parent topic (Polymorphism) — here is the short version so you can follow this page standalone:
The scenario matrix
Think of each row below as a cell — a distinct situation the topic can produce. A method call x.method() can land in wildly different places depending on what x is and whether the method even exists. Below, "duck" means "object with the required method"; "non-duck" means it is missing that method.
| Cell | Situation | What could go wrong / be surprising |
|---|---|---|
| C1 | Pure duck typing, unrelated classes | People expect a shared base class — there is none |
| C2 | Inheritance + override (subtype flavor) | Which speak runs? The subclass's, via MRO |
| C3 | Missing method (non-duck) | AttributeError at the call — the degenerate/failing input |
| C4 | EAFP recovery from the failure in C3 | Catch and continue vs crash |
| C5 | hasattr capability check (LBYL flavor) |
Guard behavior, never class |
| C6 | Operator polymorphism via __add__ |
Same +, different meaning per type |
| C7 | Empty / zero-length collection (degenerate input) | sum([]) gives 0, no crash, but is 0 right? |
| C8 | Partial interface — object has some methods, not all | Works for one call, blows up on another |
| C9 | Real-world word problem | Payment processors: card, wallet, crypto |
| C10 | Exam-style twist: isinstance breaks extensibility |
Show the anti-pattern failing on a new type |
| C11 | Diamond multiple inheritance | Which parent's method wins? C3 linearization decides |
Every example below is tagged with the cell(s) it covers. Together they touch all eleven.
Forecast: guess the three lines before reading on.
Dog().speak()gives"Woof". Why this step? Python looks upspeakon the object, finds it onDog, calls it. It never asks "is this an Animal?".Cat().speak()gives"Meow". Why this step? Same lookup, different class, different result — same interface, different behavior.Robot().speak()gives"Beep boop". Why this step?Robotshares zero inheritance withDog/Cat. It fits purely because it hasspeak(). This is the whole point of duck typing.
Verify: The output, joined, is
"Woof Meow Beep boop". Three objects, one call site, three behaviors — no base class needed.
Forecast: one of these does NOT say "...generic...". Which?
Puppy().speak()gives"Yip". Why this step? Python walks the Method Resolution Order (MRO) — the ordered list of classes to search. ForPuppythat is[Puppy, Animal, object]. It findsspeakonPuppyfirst and stops. See Method Resolution Order (MRO).Kitten().speak()gives"...generic...". Why this step?Kittendefines nospeak, so the search continues toAnimaland finds it there. The parent's method is inherited, not copied — this is Inheritance — overriding and super().- The parent is untouched.
Animal().speak()still returns"...generic...". Overriding changed only Puppy's lookup, not the class for everyone — this is exactly Mistake C from the box above.
Verify: Outputs are
"Yip"then"...generic...". First object resolves atPuppy, second falls through toAnimal. Both are polymorphic responses to the identical callx.speak().
Forecast: does this return None, "", or crash?
- Python looks up
speakon theFishobject. Not there. Why this step? This is the exact lookup from Example 1 — but this time it fails. - Search the MRO
[Fish, object]. Neither hasspeak. Why this step? When the search runs out of classes with no match, Python cannot invent a method. - Python raises
AttributeError: 'Fish' object has no attribute 'speak'. Why this step? Duck typing's rule "checks method at call time" means the failure surfaces only when you call it, not when you defineFish.
Verify: Calling raises
AttributeError. This is the boundary case that motivates the next two examples — how to handle a non-duck gracefully.
Forecast: what are the two strings in results?
Dog()gives"Woof". Thetrysucceeds; no exception. Why this step? EAFP = Easier to Ask Forgiveness than Permission: we just try the call. See EAFP vs LBYL — Pythonic error handling.Fish()raisesAttributeErrorinsidetry. Why this step? Exactly the failure from Example 3, but now it is caught.- The
exceptreturns"(silent)". Why this step? We recover instead of crashing — the loop continues. This is the Pythonic fix to Mistake B (don't guard withisinstance; just try and catch).
Verify:
results == ["Woof", "(silent)"]. Two objects, one caught exception, program survives.
Forecast: which one says "(cannot speak)"?
hasattr(Cat(), "speak")givesTrue. Why this step?hasattrinternally tries the lookup and reports whether it worked. We test the capabilityspeak, never the type nameCat. LBYL (Look Before You Leap) means "check the condition before acting", the opposite of EAFP's "act, then handle failure". See EAFP vs LBYL — Pythonic error handling.Cat().speak()gives"Meow". The guard passed, so we call. Why this step? This still supports any future class withspeak— extensibility preserved (contrast Mistake B).hasattr(Fish(), "speak")givesFalse, so we return"(cannot speak)". Why this step? NospeakinFish's MRO, so we skip the call entirely — no exception ever raised.
Verify: Prints
"Meow"then"(cannot speak)". Capability-based LBYL keeps polymorphism open, unlike class-based checks.
Forecast: write down each value and whether it's a number, string, or list.
1 + 2gives3(int). Python callsint.__add__(1, 2). Why this step?a + bis operator overloading: the+token is translated at runtime intotype(a).__add__(a, b). The token is fixed; the method it dispatches to depends on the runtime type ofa. See Dunder methods — __add__, __len__, __str__."a" + "b"gives"ab"(str). Callsstr.__add__, which does concatenation. Why this step? Same operator token, butstr's version glues text instead of doing arithmetic.[1] + [2]gives[1, 2](list). Callslist.__add__, which also concatenates. Why this step? This is duck typing applied to the dispatch behind+: Python doesn't check the operands' class, it just calls whatever__add__they define. The+symbol is syntax; the behavior is chosen dynamically per type.
Verify: Results are
3,"ab",[1, 2]. Same operator token, three behaviors selected at runtime — operator overloading powered by dynamic lookup.
Forecast: does the empty case crash, or return something? What?
total_area([]): the generator yields nothing. Why this step? With no items,s.area()is never called — so a missing-method risk cannot even arise. The degenerate input is safe here.sum(<empty>)gives0. Why this step? Python'ssumuses a default start value of0. Empty means "nothing added to zero", which is0. No crash.total_area([Circle(2)])gives (plain text: pi times 2 squared = 4·pi, about 12.566). Why this step? Confirms the non-empty branch: the area formula pi·r² with r = 2.
The figure below draws both branches side by side. On the left it shows a single Circle(2) with its radius r = 2 marked in violet, and the one call s.area() running the area formula pi·r² = 4·pi ≈ 12.566. On the right it shows the empty list [ ], where the same call never fires and sum falls back to its start value 0. Notice the orange "same call" arrow reaching only the left shape — the empty branch produces 0 without ever touching area. That is why the degenerate input is safe: no object means no method to be missing.

Verify: Empty gives
0; one circle of radius 2 gives 4·pi ≈ 12.566370614. The zero case is graceful, and the numeric result checks out below.
Forecast: how far does this get before failing — line a or line p?
shape.area()succeeds, returning pi·1² = pi (about 3.14159). Why this step?Circledoes satisfy theareapart of the interface. Partial ducks quack partially.shape.perimeter()raisesAttributeError. Why this step?Circlenever definedperimeter; the MRO search fails at the moment of the second call, not before.- Lesson: "duck typing" means the whole interface you use, method by method. Satisfying one call doesn't guarantee the next. To document the full required interface, use Abstraction — interfaces and abstract base classes (ABC).
Verify:
area()returns pi (about 3.14159), thenperimeter()raisesAttributeError. A partial duck passes the first call and fails the second.
Forecast: estimate the three charges, then the total.
CardProcessor.charge(50)gives 50 × 1.02 = 51.0. Why this step? The 2% card fee.checkoutnever names the class — it just callscharge.WalletProcessor.charge(50)gives 50 × 1.0 = 50.0;CryptoProcessor.charge(50)gives 50 × 1.01 = 50.5. Why this step? Same callprocessor.charge(50), three fee policies — the textbook same interface, different behavior.total= 51.0 + 50.0 + 50.5 = 151.5, and its type isfloat. Why this step?sumstarts at the integer0, then repeatedly does0 + 51.0,+ 50.0,+ 50.5. Because eachchargereturns afloat(multiplying by1.02etc. produces a float), the running total is promoted tofloaton the first addition — this is operator polymorphism from Example 6 again (int.__add__with a float operand yields a float). Add aBankTransferProcessortomorrow with its ownchargeandcheckoutneeds zero edits — the extensibility payoff.
Verify: Individual charges
51.0, 50.0, 50.5(units: dollars), total151.5, typefloat. Units consistent (dollars in, dollars out), and the sum matches.
Forecast: Wolf clearly can speak. Does bad_speak accept it?
bad_speak(Dog())gives"Woof".isinstance(Dog(), Dog)isTrue, so we proceed. Why this step? The happy path — but it only works by class name, not capability.bad_speak(Wolf())raisesTypeError("not a Dog"). Why this step?Wolfis not aDog, so the class check rejects it — even though it has a perfectly goodspeak(). This is Mistake B in action: the guard blocks a valid new type.- The fix is Example 5's
hasattr(check capability) or Example 4's EAFP (just call). Both keep the door open forWolf,Robot, and anything else withspeak. Rejecting valid substitutes also violates the Liskov Substitution Principle (SOLID) spirit.
Verify:
Doggives"Woof";WolfraisesTypeError. The class check breaks polymorphism — the exact anti-pattern you must recognise.
Forecast: the two candidates are "B" and "A". Which prints — and in what order does Python search?
- Python builds
D's MRO with C3 linearization, giving[D, B, C, A, object]. Why this step? This is the "diamond" shape:DreachesAthrough bothBandC. C3 linearization is the algorithm Python uses to flatten that diamond into one unambiguous search order. Its two rules: a class comes before its parents, and if you listedclass D(B, C), thenBcomes beforeC. Crucially,A(shared by both) is placed once, after bothBandC— never duplicated. See Method Resolution Order (MRO). - Search that order for
greet.Dhas none, so tryB;B.greetexists, so stop. Why this step? Lookup always walks the MRO left to right and takes the first match, exactly as in Example 2 — the diamond changes only the order of the list, not the lookup rule. - Result:
D().greet()gives"B".A'sgreetis never reached, even thoughCwould have inherited it, becauseBsits ahead of bothCandAin the MRO. Why this step? This resolves the ambiguity a naive "search all parents" scheme could not — without C3, "is itBorA?" would be undefined. C3 gives a single deterministic answer.
Verify:
D().greet() == "B", andD.__mro__names are["D", "B", "C", "A", "object"]. The shared baseAappears exactly once, after bothBandC— the diamond is safely linearized.
Recall Which cell was which?
C1 duck typing no inheritance ::: Example 1 C2 override + MRO ::: Example 2 C3 missing method → AttributeError ::: Example 3 C4 EAFP recovery ::: Example 4 C5 hasattr capability check ::: Example 5 C6 operator polymorphism (+) ::: Example 6 C7 empty / zero-length input ::: Example 7 C8 partial interface ::: Example 8 C9 real-world payments ::: Example 9 C10 isinstance breaks extensibility ::: Example 10 C11 diamond inheritance / C3 linearization ::: Example 11
Connections
- Polymorphism — duck typing, same interface different behavior (index 2.1.12)
- Inheritance — overriding and super()
- Abstraction — interfaces and abstract base classes (ABC)
- Dunder methods — __add__, __len__, __str__
- Method Resolution Order (MRO)
- EAFP vs LBYL — Pythonic error handling
- Liskov Substitution Principle (SOLID)