LBYL — Look Before You Leap. Check whether the operation will succeed; if so, do it. if key in d: return d[key]. EAFP — Easier to Ask Forgiveness than Permission. Just try the operation; handle the exception if it fails. try: return d[key]; except KeyError: return default.
Why Python prefers EAFP
Three reasons. (1) Python exceptions are cheap when they don't fire (just a few extra cycles in the success path) and only expensive when they do fire. For operations that usually succeed, the EAFP path is faster. (2) LBYL has race conditions — checking that the file exists, then opening it, leaves a window where it could disappear. (3) EAFP keeps the "happy path" logic prominent; LBYL clutters it with checks.
When LBYL is still right
When the failure case is the common one (most lookups miss), exceptions become slow. When the operation has a side effect that can't be undone (you've already half-written a file), failing partway is worse than not starting. When you really do need to query state without the side effect of trying, LBYL is correct.
The dict idiom — .get vs in vs try
For dict access, you have three idiomatic ways. d[key] raises KeyError on miss (EAFP). d.get(key, default) returns the default silently. if key in d: ... is LBYL. The choice depends on whether the missing case is exceptional, expected, or both. None is universally right.
Pythonic Way: Default to EAFP for the common "try, on failure fall back" pattern. Reach for LBYL when checking is genuinely cheaper, when the operation has side effects, or when the "is this even possible" check is itself the question. The two coexist; use whichever expresses your intent more clearly.
Code
LBYL vs EAFP — same problem, different style·python
d = {"x": 1, "y": 2}
# LBYL — Look Before You Leap
if "x" in d:
value = d["x"]
else:
value = 0
# EAFP — Easier to Ask Forgiveness than Permission
try:
value = d["x"]
except KeyError:
value = 0
# In Python, EAFP is generally idiomatic for this case.
# But for dict specifically, .get() is even better:
value = d.get("x", 0)
EAFP wins on speed when the success path is common·python
import time
d = {i: i*2 for i in range(10000)}
# LBYL — checks each access
def lbyl_sum():
total = 0
for i in range(10000):
if i in d:
total += d[i]
return total
# EAFP — just access; rare misses raise
def eafp_sum():
total = 0
for i in range(10000):
try:
total += d[i]
except KeyError:
pass
return total
# When most accesses succeed, EAFP is faster (no double-check)
start = time.perf_counter(); lbyl_sum(); print("LBYL:", time.perf_counter() - start)
start = time.perf_counter(); eafp_sum(); print("EAFP:", time.perf_counter() - start)
EAFP avoids race conditions·python
import os
# LBYL — RACE CONDITION
if os.path.exists("data.txt"):
# what if the file is deleted RIGHT HERE between check and open?
with open("data.txt") as f:
contents = f.read()
# This pattern is buggy under concurrency / external file system events.
# EAFP — atomic
try:
with open("data.txt") as f:
contents = f.read()
except FileNotFoundError:
contents = ""
# No window between check and use. Open succeeds or it doesn't.
When LBYL is correct — the side-effect case·python
# If the operation has a side effect, EAFP halfway-through is bad
def create_directory(path):
# LBYL — check first, only act if safe
if os.path.exists(path):
raise FileExistsError(f"{path} already exists")
os.makedirs(path)
# If we'd just tried os.makedirs and caught FileExistsError,
# we'd have a more complex check (was it ours? was it pre-existing?)
# Here LBYL is the cleaner expression — the operation is non-idempotent.
Write two versions of count_word(haystack, needle) that counts occurrences of needle in a list of strings haystack. (a) LBYL: check isinstance(s, str) and needle in s before counting. (b) EAFP: just call s.count(needle) and catch any AttributeError from non-strings. Test both with a list including some non-string entries — verify both give the right count. Discuss which one feels closer to Python's defaults.
Progress
Progress is local-only — sign in to sync across devices.