Skip to content

EAFP vs LBYL Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on EAFP vs LBYL, why try/except is preferred, the race conditions LBYL invites, and dict.get versus checking for a key.

Read the in-depth guidePython EAFP vs LBYL Explained — Why Pythonistas Ask Forgiveness, Not Permission(opens in new tab)
15 of 15

EAFP = "Easier to Ask Forgiveness than Permission": just attempt the operation and handle the exception if it fails. LBYL = "Look Before You Leap": check the preconditions first, then act only if the checks pass. They are two styles for handling things that might go wrong.

# LBYL — check first
if "key" in config:
    value = config["key"]

# EAFP — try and handle failure
try:
    value = config["key"]
except KeyError:
    value = default

EAFP is the Pythonic default — it reads naturally and avoids redundant checks. Rule of thumb: in Python, try the operation and catch the specific exception rather than pre-validating every condition.

EAFP fits Python's design: exceptions are cheap and idiomatic, and the style avoids duplicating logic. With LBYL you often check a condition and then perform the same lookup/operation again, doing the work twice. EAFP also stays correct when an object merely behaves like the expected type (duck typing) rather than passing an explicit type check.

# LBYL duplicates the access and breaks on duck-typed objects
if hasattr(obj, "read") and callable(obj.read):
    data = obj.read()

# EAFP — just use it; let the exception surface real problems
try:
    data = obj.read()
except AttributeError:
    data = None

The happy path stays uncluttered and the error handling is explicit. Rule of thumb: write the common case as straight-line code and catch the specific exception for the rare failure.

LBYL introduces a TOCTOU bug — "Time Of Check to Time Of Use". Between the moment you check a condition and the moment you act on it, another thread or process can change the state, so the check is stale and the action fails or corrupts data. EAFP avoids the gap by acting atomically and handling failure.

import os
# LBYL — file can be deleted between the check and the open (race!)
if os.path.exists(path):
    with open(path) as f:    # may still raise FileNotFoundError
        data = f.read()

# EAFP — no window; the open either works or raises
try:
    with open(path) as f:
        data = f.read()
except FileNotFoundError:
    data = None

The check-then-act pattern is unsafe in concurrent or filesystem contexts. Rule of thumb: for files, sockets, and shared state, attempt the operation and handle the exception rather than checking first.

dict.get(key, default) returns the value if present and the default (None if unspecified) otherwise — a clean one-liner that avoids both the if key in d check and a try/except KeyError. Use a membership check or try/except only when you must distinguish a missing key from a stored None, or run different logic in each branch.

counts = {"a": 1}
counts.get("b")          # None — no KeyError
counts.get("b", 0)       # 0    — supply a default

# need to tell "missing" from "stored None"? then check explicitly
if "b" in counts:
    ...
# or accumulate with setdefault / defaultdict
counts.setdefault("c", 0)

For counting/grouping, collections.defaultdict or Counter is even cleaner. Rule of thumb: reach for .get() with a default for "value or fallback", and only branch explicitly when missing-ness itself is meaningful.

EAFP done carelessly causes two problems. First, a too-broad except (bare except: or except Exception) can swallow unrelated bugs — catching a KeyError you didn't intend, or hiding a NameError. Second, the try block should wrap only the line that can fail, so you don't accidentally catch exceptions from surrounding code.

# Bad — hides real errors and over-wide try block
try:
    value = config["key"]
    result = expensive_call(value)   # its errors get caught too!
except Exception:
    value = default

# Good — narrow exception, minimal try body
try:
    value = config["key"]
except KeyError:
    value = default
result = expensive_call(value)       # outside the try

Always catch the most specific exception and keep the try body small. Rule of thumb: EAFP means "catch the one expected failure", never "catch everything and hope".

LBYL wins when the check is cheap and the failure is expensive or common, when you'd otherwise catch a too-broad exception, or when validating user input up front gives clearer errors. If most attempts would fail, paying for exceptions each time is wasteful.

# LBYL is clearer here — validate before a costly operation
if not isinstance(age, int) or age < 0:
    raise ValueError("age must be a non-negative int")
process(age)

# LBYL avoids catching an over-broad exception you can't distinguish
if denominator != 0:
    result = numerator / denominator

Rule of thumb: prefer EAFP by default, but use LBYL for cheap pre-validation, input checking, or when the "failure" isn't a clean single exception.

getattr(obj, "name", default) is the attribute analogue of dict.get — it returns the attribute if present, else the default, without a try/except AttributeError or a separate hasattr check (which duplicates the lookup).

# verbose LBYL
if hasattr(obj, "timeout"):
    t = obj.timeout
else:
    t = 30

# concise — single lookup, default fallback
t = getattr(obj, "timeout", 30)

Rule of thumb: use getattr(obj, name, default) for "attribute or fallback"; reserve hasattr/try for when presence itself drives different logic.

contextlib.suppress(Exc) is a context manager that silently ignores the named exception(s) — a tidy replacement for try/except Exc: pass when you genuinely want to skip a failure.

from contextlib import suppress

# instead of:
try:
    os.remove(path)
except FileNotFoundError:
    pass

# write:
with suppress(FileNotFoundError):
    os.remove(path)

Rule of thumb: use suppress for "do this, ignore if it fails" — but only for specific exceptions you truly mean to discard, never a blanket Exception.

EAFP uses an object and lets it work if it has the right behavior — so any duck-typed object qualifies. LBYL with isinstance rejects valid objects that aren't the exact type but behave correctly, defeating duck typing.

# LBYL — rejects anything not literally a list, even list-like objects
if isinstance(x, list):
    x.append(1)

# EAFP — works for any object supporting append (list, deque, custom)
try:
    x.append(1)
except AttributeError:
    ...

Rule of thumb: EAFP asks "can it do what I need?" not "is it the exact type?" — keeping code flexible across compatible types.

It depends on the failure rate. Setting up a try is nearly free when no exception fires, so EAFP is faster when failures are rare. But raising and catching an exception is relatively expensive, so LBYL wins when failures are frequent.

# rare misses -> EAFP is faster (no exception in the common case)
try:
    v = cache[key]
except KeyError:
    v = compute(key)        # only occasionally

# frequent misses -> a membership check avoids many raised exceptions
v = cache[key] if key in cache else compute(key)

Rule of thumb: EAFP for the "usually succeeds" case; switch to LBYL only when profiling shows exceptions are firing often enough to matter.

The else block runs only if the try body raised no exception, letting you keep the try minimal (just the risky line) while putting the "on success" code where it can't be accidentally caught.

try:
    value = config["key"]
except KeyError:
    value = default
else:
    # runs only if the lookup succeeded — not inside the try
    log(f"found {value}")

Rule of thumb: use else to separate "the risky operation" from "what to do on success", keeping the try body as small as possible.

For building up collections (grouping, counting), defaultdict auto-creates the missing value so you can mutate it directly — cleaner than get/setdefault in a loop. get is for read-with-fallback, not in-place accumulation.

from collections import defaultdict

groups = defaultdict(list)
for name in names:
    groups[name[0]].append(name)   # no key-existence check needed

# vs the clunkier get/setdefault style:
# groups.setdefault(name[0], []).append(name)

Rule of thumb: defaultdict(list/int/set) for accumulation loops; dict.get for a one-off "value or default" read.

EAFP only works safely if you catch the exact exception the operation can raise. Catching too broadly hides bugs; catching the wrong type lets the real failure escape. Knowing each operation's exceptions is part of writing good EAFP.

# int() raises ValueError, not KeyError — catch the right one
try:
    n = int(user_input)
except ValueError:
    n = 0

# list indexing raises IndexError; dict access raises KeyError

Rule of thumb: match the except to the operation's documented exception (ValueError, KeyError, IndexError, AttributeError) — generic Exception defeats the point.

Validate (LBYL) at the boundaries — where untrusted input enters (API requests, CLI args, file parsing) — then trust the data internally and use EAFP for the rare runtime failure. This concentrates checks and keeps core logic clean.

def handle_request(payload):
    # boundary: validate up front, fail fast with clear errors
    if "user_id" not in payload:
        raise ValueError("user_id required")
    user_id = int(payload["user_id"])
    # internal code can now assume user_id is a valid int (EAFP for the rest)
    return load_user(user_id)

Rule of thumb: LBYL at the edges for clear input errors; EAFP in the interior where data is already trusted.

The walrus := lets you assign and test in one expression, so you can act on a value only when it's present — a clean "try to get it, use it if you got it" pattern without a separate check line.

# process queue items until exhausted
while (item := queue.get()) is not None:
    handle(item)

# use a match only if found
if (m := pattern.search(text)):
    print(m.group())

Rule of thumb: := collapses "fetch, then check the fetched value" into one step — handy for loop conditions and guard clauses without re-computing.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel