| name | early-return-guards |
| description | Use when writing or modifying a Python function with conditional branches โ about to nest an if inside an if, write an else after a branch that returns/raises/continues, or assign a result variable in branches to return at the end. |
Early Return Guards
Overview
The happy path stays at indentation level zero. Every precondition, edge case, and failure exits at the top with return / raise / continue; what remains reads straight down. Nesting and else-after-exit are how a 5-line function becomes a pyramid nobody can review.
A branch that ends in return / raise / continue / break is never followed by else (or elif doing else's job). Preconditions are checked as guards at the top, inverted. No exceptions.
The rule
| Situation | โ banned | โ
required |
|---|
| precondition / edge case | wrap the body in if valid: | invert and exit: if not valid: return None |
| branch ends in return/raise | if c: return a + else: return b | drop the else โ the code after the if is the else branch |
| result decided by branches | assign result in if/else arms, single return result at the end | return directly from each arm; last line returns the default |
| loop item filtering | nest the loop body in if keep: | if not keep: continue, body stays flat |
| adding a condition to an already-nested function | add one more nesting level "to keep the diff small" | that's the signal โ invert the function you're touching to guards first |
Still allowed: if/else where neither arm exits (two live continuations); conditional expressions (a if c else b); try/except/else; match arms (see branching-modeled-state-with-match).
Before โ after
def pick_camera(devices, preferred_index, require_hd):
result = None
if len(devices) > 0:
if preferred_index is not None:
for device in devices:
if device.index == preferred_index:
if not require_hd or device.width >= 1280:
result = device
if result is None:
for device in devices:
if not require_hd or device.width >= 1280:
result = device
break
return result
def is_eligible(device, require_hd):
return not require_hd or device.width >= 1280
def pick_camera(devices, preferred_index, require_hd):
if len(devices) == 0:
return None
for device in devices:
if device.index != preferred_index:
continue
if not is_eligible(device, require_hd):
continue
return device
for device in devices:
if is_eligible(device, require_hd):
return device
return None
Note the shape of each loop: disqualify with continue, then return. The eligibility condition, needed twice, became its own function instead of being copy-pasted into two nests.
Common mistakes
| Mistake | Fix |
|---|
else: return default after an if ... return | Delete the else, dedent: return default as the last line. |
| Keeping a nest "because the hotfix diff should be minimal" | Guard-inverting the one function you touch is minimal. Adding a 4th nesting level is the churn. |
result: T | None = None at the top of a branching function | That variable exists only to escape the nest. Return from the arms directly. |
Guarding with if ok: around 20 lines | Invert: if not ok: return/raise, then the 20 lines dedent. |
elif chain where every arm returns, ending in else: | Sequential if ... return statements; the final else body becomes plain trailing code. |
Red Flags โ STOP
- About to press Tab to indent the function body under a condition โ invert the condition and exit instead.
- About to write
else: under a branch whose last statement is return/raise/continue โ delete it, dedent.
- About to declare
result = None before an if โ return from the branches.
- Editing a function and your cursor is at nesting depth โฅ 3 โ flatten that function to guards before adding your change.
Ruff flags the mechanical part (RET505โRET508); this skill exists for the part it can't โ structuring new conditions as guards in the first place.