EAFP vs LBYL
Python favors EAFP (Easier to Ask Forgiveness than Permission): try the operation and handle failure with exceptions. LBYL (Look Before You Leap) checks conditions first. Both are valid; EAFP is idiomatic when the happy path is common and failures are exceptional.
Recipe
Quick-reference recipe card - copy-paste ready.
# EAFP - try, then handle
try:
value = mapping[key]
except KeyError:
value = default
# LBYL - guard, then act
if key in mapping:
value = mapping[key]
else:
value = defaultWhen to reach for this:
- EAFP when the success path dominates and a failed lookup is rare
- LBYL when failure is expected and you want to avoid exception overhead
- EAFP when checking all preconditions is harder than trying (duck typing)
- LBYL in tight loops where exceptions would fire thousands of times per second
- EAFP when multiple failure modes collapse into one
exceptblock
Working Example
A parser accepts dict or object inputs using EAFP duck typing, with LBYL only for cheap type branching.
from typing import Any
def get_name(record: Any, default: str = "unknown") -> str:
"""Extract a name from a dict-like or attribute-based record."""
if isinstance(record, dict):
return record.get("name", default)
try:
return record.name
except AttributeError:
return default
def parse_user_id(raw: str) -> int:
try:
return int(raw)
except ValueError as exc:
raise ValueError(f"invalid user id: {raw!r}") from exc
samples = [
{"name": "Ada"},
type("User", (), {"name": "Grace"})(),
object(),
]
for sample in samples:
print(get_name(sample))
print(parse_user_id("42"))What this demonstrates:
- Dict path uses
.get()(LBYL-style guard via API) - no exception for missing keys - Attribute access uses EAFP - one
trycovers many object shapes - Re-raising with
from excpreserves the original traceback chain - Duck typing avoids
hasattrchains that race with dynamic attributes
Deep Dive
How It Works
- EAFP executes the operation; Python raises a specific exception on failure
- LBYL evaluates predicates (
in,isinstance,hasattr) before the operation - Exception handling has a small setup cost; in hot loops, millions of exceptions hurt
try/exceptaround the success path keeps code linear and readable- Multiple guard clauses (LBYL) often duplicate the work the operation would do anyway
EAFP vs LBYL at a Glance
| Style | Strength | Weakness |
|---|---|---|
| EAFP | Linear happy path, handles races | Exception cost if failure is common |
| LBYL | Predictable when failure is frequent | Verbose guards, TOCTOU races with mutable state |
Python Notes
# Prefer EAFP for file existence when you will open immediately anyway
try:
with open(path) as f:
data = f.read()
except FileNotFoundError:
data = ""
# Prefer LBYL when failure is the norm (scanning optional keys)
for key in optional_keys:
if key in cache:
process(cache[key])Gotchas
- TOCTOU with LBYL - checking
key in dictthendict[key]can fail if another thread deletes the key. Fix: use EAFP (try/except KeyError) or.get(). - Bare
except:- EAFP tempts catching everything. Fix: catch specific exceptions (KeyError,ValueError,AttributeError). - hasattr + getattr duplication -
if hasattr(x, "foo"): x.foocallsgetattrtwice. Fix: use EAFP withtry/except AttributeError. - Exception-driven control flow in loops - parsing every line with
int()in a tight loop is slow when most lines fail. Fix: pre-filter with regex or LBYL validation. - Swallowing exceptions - empty
except: passhides bugs. Fix: log, re-raise, or return a sentinel with explicit typing.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
.get() / setdefault() | Dict missing-key defaults | You need to distinguish missing vs None value |
isinstance guards | Type branching is the real decision | You only need duck-typed behavior |
match / structural pattern matching | Complex type shapes (3.10+) | Simple key lookup |
| Optional types + static checks | API contracts are fixed at compile time | Runtime duck typing is required |
FAQs
What does EAFP stand for?
"Easier to Ask Forgiveness than Permission" - try the operation and handle exceptions instead of checking every precondition first.
Is EAFP always more Pythonic?
No. Use EAFP when success is common and exceptions are rare. Use LBYL when failure is expected or exceptions would dominate performance.
When should I use dict.get() instead of try/except?
When the container is a dict and a missing key is a normal outcome. .get() is LBYL built into the API - no exception overhead.
Does EAFP work with async code?
Yes. try/except works around await the same way as sync code. Do not use exceptions for flow control in tight async loops either.
How do I preserve tracebacks when re-raising?
Use raise NewError("msg") from exc to chain exceptions, or bare raise inside except to re-raise the original.
Is hasattr safe before getattr?
Between hasattr and access, another coroutine can delete the attribute. Prefer EAFP or getattr(obj, "name", default).
What about permission checks before file access?
For security-sensitive paths, explicit checks may be required by policy. For ordinary I/O, EAFP with FileNotFoundError is fine.
Can EAFP replace all if-statements?
No. Business logic branches (if user.is_admin) are not exception cases. Reserve EAFP for operational failures.
How does this relate to duck typing?
EAFP embraces duck typing: call the method you need; handle AttributeError if the object does not support it.
Should I catch Exception or BaseException?
Catch specific types first. Never catch BaseException (includes KeyboardInterrupt, SystemExit) in application code.
Related
- Context Managers as a Pattern - EAFP for resource cleanup
- Null Object & Sentinels - avoid ambiguous missing values
- Pythonic Patterns Basics - section overview
- Common Anti-Patterns - when guards go wrong
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.