if it quacks — until it doesn't
Duck Typing Without Contracts
Duck typing is convenient until the duck is missing a method. Without an explicit contract — a protocol, an interface, an abstract base — 'it probably has .save()' is a runtime gamble that pays off until the one caller that passes something almost-compatible.
A value is used as if it has a shape no one ever guaranteed it has.
01in the wild
In the wild
The Duck That Couldn't
If it quacks like a duck, we assume it flies like one, and then it doesn't.
example.py
# Works for files, breaks for the StringIO someone passes later
def archive(f):
f.flush()
f.fileno() # AttributeError: StringIO has no fileno()
# The bug only appears for inputs that are ALMOST file-like.Duck typing accepts anything until it reaches the one method the substitute lacks. The contract was implicit, so nothing flagged the mismatch.
// observed
real file: ok StringIO: AttributeError: 'fileno'
example.rb
# reduce is assumed, so a non-enumerable blows up mid-iteration
def total(collection)
collection.reduce(0) { |sum, x| sum + x } # NoMethodError if no #reduce
end
total(42) # Integer has no #reduce -> crash far from the callerNothing declared that collection must be enumerable. The assumption surfaces as a NoMethodError deep inside the method.
// observed
total([1,2,3]) -> 6 total(42) -> NoMethodError: reduce
Give the Duck a Contract
An explicit protocol turns 'probably has the method' into a checkable guarantee.
example.py
from typing import Protocol
class Saveable(Protocol):
def save(self) -> None: ...
def persist(item: Saveable) -> None:
item.save() # type checker verifies item has .save()
# A type that lacks save() is now a mypy error, before runtime.typing.Protocol keeps duck typing's flexibility but makes the expected shape explicit and statically checkable.
// observed
with .save(): type-checks, runs without .save(): mypy error before runtime
example.go
// An interface is a contract the compiler enforces structurally.
type Saver interface { Save() error }
func persist(s Saver) error {
return s.Save() // anything passed MUST implement Save()
}
// Passing a type without Save() simply won't compile.Go's interfaces are duck typing checked at compile time: the shape is named, and conformance is verified before the program runs.
// observed
implements Saver: compiles missing Save(): compile error
02cross-pollination
Where this compounds
Runtime Errors
- Shape-Drift AttributeError × Improper Initialization
- Unexpected-Type Element in a Heterogeneous Collection × this Mutation
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.