C.W.K.
Stream
Lesson 01 of 05 · published

EAFP, Duck Typing, and Trust

~18 min · eafp, duck-typing, idiom, trust

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Pythonic — what the word actually means

"Pythonic" isn't about syntax tricks. It's about a few load-bearing assumptions Python culture made differently from C-family languages. The two biggest: EAFP over LBYL (try, catch the exception; don't pre-validate every condition), and duck typing (if it walks like a duck and quacks like a duck, treat it as one).

Trust the protocol, not the type

Java says "this method takes a List." Python says "this method takes anything that supports iteration." The first is restrictive — you can't pass a generator, a tuple, a custom iterable. The second is flexible — anything that implements __iter__ works. Python culture leans hard on this; functions accept duck-typed inputs and let the caller pick the shape that fits.

The practical consequence — fewer isinstance checks

Pythonic code rarely checks isinstance(x, list) before iterating. It just iterates. If x doesn't support iteration, the caller passed the wrong thing — let the natural error rise. Adding isinstance checks "to be safe" rejects valid inputs (other iterables) and adds noise.

Where strict types DO win

Type hints (typing track) give you static checking without runtime isinstance overhead. The hint says "I expect an Iterable"; mypy verifies it; the runtime stays duck-typed. This is the modern compromise: structural protocols (typing.Protocol) for contract, no isinstance walls.

Principle: Trust the caller knew what they were doing. Document the contract (with type hints), then call methods. Errors from invalid inputs become callsite bugs to fix once, not validation gates that punish every legitimate caller.

Code

EAFP — the Python way·python
# LBYL — defensive, verbose, has a race condition
def get_field_lbyl(obj, name):
    if hasattr(obj, name):
        attr = getattr(obj, name)
        if callable(attr):
            return attr()
        return attr
    return None

# EAFP — try, fall back
def get_field_eafp(obj, name):
    try:
        attr = getattr(obj, name)
        return attr() if callable(attr) else attr
    except AttributeError:
        return None

# Both work; EAFP is shorter and matches Python culture
Duck typing — accept any iterable·python
# UNPYTHONIC — narrow type check
def sum_strict(xs):
    if not isinstance(xs, list):
        raise TypeError("need a list")
    total = 0
    for x in xs:
        total += x
    return total

# PYTHONIC — accept any iterable
def sum_pythonic(xs):
    total = 0
    for x in xs:           # works for list, tuple, generator, set, ...
        total += x
    return total

print(sum_pythonic([1, 2, 3]))
print(sum_pythonic((1, 2, 3)))
print(sum_pythonic(x*x for x in range(4)))
print(sum_pythonic({1, 2, 3}))
Type hints + duck typing — modern compromise·python
from typing import Iterable

def sum_typed(xs: Iterable[int]) -> int:
    """Type hint says 'iterable of int'; runtime stays duck-typed."""
    return sum(xs)

# Type checker (mypy) verifies the caller passes an iterable.
# Runtime doesn't isinstance-check — just iterates.
# Best of both worlds: static contract, runtime flexibility.
When isinstance IS appropriate·python
# At system boundaries — validating untrusted data:
def parse_user_input(data):
    if not isinstance(data, dict):
        raise TypeError("expected dict from API")
    # ... process data

# When you genuinely need to dispatch on type:
def format_value(x):
    if isinstance(x, (int, float)):
        return f"{x:.2f}"
    if isinstance(x, str):
        return f'"{x}"'
    return repr(x)
# (singledispatch is even cleaner here — see oop-advanced track)

# These are valid uses. The anti-pattern is isinstance defensiveness
# inside a function whose contract is already clear from type hints.

External links

Exercise

Rewrite this LBYL function in EAFP style, then add type hints with Iterable[int]: def first_positive(items): if not isinstance(items, list): return None; for x in items: if isinstance(x, int) and x > 0: return x; return None. Show that the EAFP version works with a tuple, a generator, and a set, while the LBYL version rejects all of them.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.