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 caller
Nothing 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

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.