A nested if and its guard equivalent. Notice where the ordinary case ends up in each.
# Nested: the answer is at the deepest point
def price(user):
if user is not None:
if user['active']:
return 10
else:
return 0
else:
raise ValueError('no user')
# Guards: the awkward cases leave first, the answer is last
def price(user):
if user is None:
raise ValueError('no user')
if not user['active']:
return 0
return 10
- Invert the condition and return early
`if ok: <everything>` becomes `if not ok: return` followed by everything, unindented. Each transformation removes one level and one else, and the meaning is identical — the only thing that changes is that the reader no longer has to remember which branch they are in.
- An else after a return is dead weight
If the if-branch returns or raises, the code after it can only be reached when the condition was false — so the else adds a level of indentation and says nothing the control flow had not already said. Removing it is what actually flattens the function.
- The ordinary case belongs at the bottom, unindented
Once the exceptional cases have each left by their own door, what remains is the answer for an ordinary input, sitting at the lowest indentation with nothing wrapped round it. That is the line most readers came for, and in the nested version it was the hardest one to find.
Work from the outside in. Take the outermost condition, flip it, and return or raise immediately — then everything that was inside it loses a level.
`if order is not None:` becomes `if order is None: raise`. `if order['items']:` becomes `if not order['items']: return 0`. Each one removes a level and its else.
Keep the order of the guards the same as the nesting order — the empty check must stay above the country check, because an empty order ships free wherever it is going. Four ifs, no elses, and the 4.99 at the bottom with no indentation.
def shipping_cost(order): if order is ____: raise ValueError('no order') if ____ order['items']: return 0 if order['country'] ____ 'GB': return 12.99 if order['total'] >= 50: return ____ return 4.99
def shipping_cost(order):
"""Return the shipping cost for an order."""
if order is None:
raise ValueError("no order")
if not order["items"]:
return 0
if order["country"] != "GB":
return 12.99
if order["total"] >= 50:
return 0
return 4.99
Each guard replaces one level, so keeping them in the same sequence guarantees the behaviour is unchanged — the empty-order check above the country check, because that is where it was. Reordering guards is a separate decision, and one that can quietly change the answer: move the country check up and an empty French order starts costing 12.99.
In the nested version, `order['items']` was on line 4 and the 0 it produces was on line 11, with three unrelated rules in between. Now each condition sits immediately above its answer, which is what makes the function skimmable — you can check one rule without reading any of the others.
return 0 if order['total'] >= 50 else 4.99 is one line shorter and breaks the pattern the four guards above it established. Consistency of shape is worth more than a line here: a reader who has understood the first three guards can read the fourth without thinking.
Guards work when the awkward cases genuinely end the function. When two conditions have to be true together and the code between them matters, flattening into guards can scatter a single decision across the function — a match statement or a lookup table is often the better answer there. And a function with eight guards is telling you something else: that it has eight reasons to change, and probably wants splitting rather than flattening. The shape to avoid in every case is the one this exercise started with, where the answer is the most deeply indented line on the page.
- Add a rule for orders over 200 shipping free everywhere, and notice where in the guard sequence it has to go.
- Move the country check above the empty check and work out from the cases which one changes — that is the behaviour the guard order is protecting.
- Find the most deeply nested function in code you have written this month and flatten it the same way.