skip to content

questions

4

EAFP and LBYL in Python: what do the two idioms mean, and which does the language favour?

level: juniorimportance: must knowfreq 70%

answer

  1. Two ways to face an operation that may fail
  2. Permission versus forgiveness
  3. The guard can drift from the call
  4. Catch the specific exception instead
  5. Zero-cost try, so the happy path is free

basics

~20 s

EAFP means easier to ask forgiveness than permission: attempt the operation and catch the exception it raises. LBYL means look before you leap: check the preconditions with an if first. Python favours EAFP, because a separate guard can drift away from the operation it protects.

solid answer

~50 s

**LBYL** guards an operation with an explicit precondition test — `if` the key is there, `if` the object has the method, `if` the path exists — and only then acts. **EAFP** just performs the operation inside a `try` and handles the specific exception the operation raises when the precondition does not hold. Python leans EAFP for two reasons. First, its protocols are built to raise informative, specific exceptions — `KeyError`, `AttributeError`, `ValueError`, `FileNotFoundError` — so the failure signal already exists and re-implementing it in a guard duplicates logic. Second, the guard and the operation are two separate steps: they can disagree today (the check is not quite the operation's real precondition) and can drift apart tomorrow, whereas a `try` block cannot get out of sync with the call inside it. EAFP is not "catch everything": you catch the *named* exception, around the smallest block that can raise it.

code

python · 14 lines
python
def parse_port_eafp(raw):
    try:
        return int(raw)
    except (TypeError, ValueError):
        return 8080


def parse_port_lbyl(raw):
    if isinstance(raw, str) and raw.isdigit():
        return int(raw)
    return 8080


print(parse_port_eafp('-1'), parse_port_lbyl('-1'))

go deeper

for a junior

Be ready to expand both acronyms and give one small example of each. Know that Python's normal style is to attempt the operation and catch the specific exception, rather than testing every precondition first.

for a middle

Explain the mechanics: the guard is a hand-written approximation of the operation's real precondition, it lives on a separate line so it can drift, and the exception already carries a precise cause. Be able to name concrete exceptions you would catch.

for a senior

Show judgement about where the try boundary goes and how narrow the except clause is, and name the cases where a check genuinely wins — expensive or partially irreversible work, failures that dominate the loop, or an ambiguous exception type.

for a principal

Own the codebase-wide convention: which exceptions are part of a module's contract, where validation happens once at a boundary versus per call site, and how you stop the EAFP style from decaying into broad handlers that hide defects.

### The two idioms Every language has to answer the question "what do I do about an operation that might not be valid right now?" Python has two named answers, and the acronyms show up constantly in code review. **LBYL — Look Before You Leap.** Test the precondition, then act: ```python if isinstance(raw, str) and raw.isdigit(): port = int(raw) else: port = 8080 ``` **EAFP — Easier to Ask Forgiveness than Permission.** Act, and handle the exception if the precondition did not hold: ```python try: port = int(raw) except (TypeError, ValueError): port = 8080 ``` Both are legal Python. The second is the idiomatic one, and being able to say *why* is the point of the question — an interviewer is checking whether you write Python or transliterated Java/C, where exceptions are reserved for the exceptional and every call is guarded. ### Why Python leans EAFP **The failure signal already exists, and it is specific.** Python's data model is defined in terms of exceptions: a missing mapping key raises `KeyError`, a missing attribute raises `AttributeError`, an unparseable value raises `ValueError`, a wrong type raises `TypeError`, an absent file raises `FileNotFoundError`. Every one of those carries what failed and, in a traceback, exactly where. An LBYL guard throws that away and re-derives the same answer with a weaker test. **The guard can be wrong, and it can drift.** This is the strongest argument, and the example above shows it: `str.isdigit()` is *not* the precondition of `int()`. `int('-1')` succeeds and `'-1'.isdigit()` is `False`, so the LBYL version silently returns the default for a perfectly good input; `' 7 '.isdigit()` is also `False` while `int(' 7 ')` is `7`. The guard is an approximation of the operation's real rules, written by hand, and every approximation is a bug waiting for an input. Even when the guard starts correct, it lives in a different line from the call, so a later change to the call — a different parser, an extra accepted format — does not automatically update it. A `try` block cannot drift from the statement inside it. **Duck typing makes exhaustive guards impossible.** Python code routinely accepts "anything that behaves like X". You cannot enumerate the types that support an operation, so checking types up front narrows what your function accepts for no benefit. Attempting the operation accepts everything that actually works. **A guard can go stale between the check and the use.** The state you tested is not necessarily the state you act on: another thread, another process, or a callback can change it in between, and for a dynamic attribute or a filesystem path the window is real. The guard then gives false confidence — you still need the exception handler, so you have written two failure paths instead of one. ### Doing EAFP properly EAFP is not an excuse for a blanket handler. Three rules make it disciplined rather than sloppy: 1. **Catch the specific exception**, or an explicit tuple of them, never bare `except:` and rarely `except Exception:`. A handler that catches everything catches your own bugs — `NameError`, `AttributeError` from a typo — and reports them as the expected condition. 2. **Keep the `try` block small.** Only the call that can raise belongs inside it. A wide `try` accidentally captures a failure from an unrelated line and misclassifies it. 3. **Handle, don't discard.** A handler that does nothing but `pass` converts a failure into a silent wrong answer. ### When LBYL is the right call EAFP is the default, not a law. Prefer a check when the operation is **expensive or partially irreversible** — you do not want to write half a file, spend a network round trip, or mutate shared state and then unwind. Prefer it when the failure is the **common case rather than the exception**, so the work of raising and unwinding happens on most iterations of a loop. Prefer it at a **validation boundary**, where you want to collect several problems and report them all to a user rather than abort on the first. And prefer it when the exception is **ambiguous** — if two very different causes raise the same exception type, a cheap up-front check can separate them. ### A note on speed A common objection is "but exceptions are slow, so I should guard." On modern CPython, *entering* a `try` block costs nothing at runtime: since Python 3.11 the compiler emits a zero-cost exception table instead of a setup instruction, so the successful path is as fast as no `try` at all. Cost is paid only when an exception is actually raised and unwound. That flips the naive intuition: EAFP is the cheaper idiom precisely when failures are rare, which is the normal case, and LBYL becomes the performance argument only when failures dominate.

  • Where is LBYL still the better choice in Python?
    When the operation is expensive or leaves partial state behind if it fails halfway, when failure is the common case rather than the rare one, when you are validating input at a boundary and want to report several problems instead of aborting on the first, and when one exception type would be raised by two causes you need to tell apart. A cheap, exact, side-effect-free check in front of a costly operation is good engineering, not un-Pythonic.
  • Does wrapping code in try/except slow down the successful path on CPython 3.14?
    No. Since Python 3.11, CPython uses zero-cost exception handling: the compiler records handler ranges in a side table instead of executing a setup instruction, so a try block that does not raise costs nothing at runtime. Work is done only when an exception is actually raised and the interpreter unwinds to find a handler. Before 3.11, entering the block did cost a small amount each time.
  • How does EAFP interact with duck typing?
    They reinforce each other. Duck typing means a function accepts anything that supports the operations it performs, and that set cannot be enumerated with isinstance checks without narrowing the function for no gain. Attempting the operation and catching AttributeError or TypeError accepts every object that genuinely works, including ones written after your function. When you do need a declared shape, typing.Protocol expresses it for a static checker without turning it into a runtime guard.

LBYL is reading the whole restaurant menu aloud to confirm they still serve the dish; EAFP is ordering it and accepting a polite "sorry, we're out" — one round trip, and the answer is authoritative at the moment it matters.

saying these in an interview costs you the question

  • Says exceptions must never be used for expected conditions
  • Claims LBYL is always safer because it avoids exceptions
  • Believes a try block slows down the successful path on modern CPython
  • Treats EAFP as licence for a blanket except Exception handler
  • Assumes a passing precondition check guarantees the next call succeeds
  • Wraps twenty lines in one try to save writing handlers

context

open as a page

Why is checking os.path.exists() before open() weaker than catching FileNotFoundError?

level: middleimportance: must knowfreq 45%

basics

~20 s

The check and the open are two separate system calls, and the filesystem can change in between, so a True answer does not promise the open will succeed. os.path.exists() also answers False for a path you simply cannot stat, and it says nothing about whether the path is a file you can read.

open as a page

`except Exception: continue` wraps each of 6,800 invoice rows in a nightly PDF renderer — what is wrong with that EAFP boundary?

level: seniorimportance: should knowfreq 40%

basics

~20 s

That is not EAFP, it is a swallowed exception. A handler that broad catches genuine bugs alongside bad rows, discards the traceback, and reports a batch where every row failed as a clean success. Narrow the try to the failing call, name the exceptions, log with the traceback, and fail the job when the failures look systemic.

open as a page

Why can Python's built-in hasattr() report False for an attribute that is genuinely defined?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

hasattr() is defined as calling getattr() and returning False if AttributeError is raised. So if the attribute is a property or a dynamic lookup whose own code raises AttributeError, the guard reports the attribute as absent and hides the real error inside it.

open as a page