Skip to content
FollowtheDamnCode

Refactoring: Simplifying Conditionals

Tame nested ifs with guard clauses, named conditions, merged branches, polymorphism, and null objects.

Big idea

Conditionals are where logic gets hard to read and easy to break. Simplifying them means making each condition say what it means, getting edge cases out of the way early, and — when the same branching repeats — letting objects decide instead of if statements.

The techniques

TechniqueMove
Decompose ConditionalExtract the condition and each branch into named functions
Consolidate Conditional ExpressionSeveral checks with the same result → one named check
Consolidate Duplicate Conditional FragmentsCode repeated in every branch → move it outside
Replace Nested Conditional with Guard ClausesHandle special cases first with early returns; keep the main path flat
Replace Conditional with PolymorphismA switch on type → a method each class overrides
Introduce Null ObjectRepeated if x is None → an object with harmless default behaviour
Introduce AssertionAn assumption the code relies on → an explicit check
Remove Control FlagA done = True flag → break / return

Think of it like

A nightclub door. A bad bouncer checks your shoes, then if the shoes are fine checks your age, then if you're old enough checks the guest list, then — inside all of that — finally lets you in. A good bouncer turns people away as soon as one rule fails ("no ID? not tonight"), and whoever's still standing walks straight in. Same rules, far less nesting.

Nested arrows versus guard clauses

In code

Guard clauses and a null object, behaviour unchanged
def pay_before(emp):
    if emp["dead"]:
        result = 0
    else:
        if emp["separated"]:
            result = 500
        else:
            if emp["retired"]:
                result = 800
            else:
                result = emp["salary"] / 12
    return result

def pay(emp):                                   # guard clauses: special cases leave early
    if emp["dead"]:
        return 0
    if emp["separated"]:
        return 500
    if emp["retired"]:
        return 800
    return emp["salary"] / 12


class Customer:
    def __init__(self, name, plan): self.name, self.plan = name, plan
    def discount(self): return 0.1 if self.plan == "gold" else 0.0

class GuestCustomer:                            # null object: no more "if customer is None"
    name, plan = "guest", "none"
    def discount(self): return 0.0

def checkout(customer, amount):
    return f"{customer.name}: {amount * (1 - customer.discount()):.2f}"


staff = [{"dead": False, "separated": False, "retired": False, "salary": 60000},
         {"dead": False, "separated": True, "retired": False, "salary": 0},
         {"dead": True, "separated": False, "retired": True, "salary": 0}]
print([pay(e) for e in staff], "same:", [pay(e) for e in staff] == [pay_before(e) for e in staff])
for c in (Customer("Ana", "gold"), GuestCustomer()):
    print(checkout(c, 50))
output
[5000.0, 500, 0] same: True
Ana: 45.00
guest: 50.00

Run and edit it in the app.

Watch it

Hands on: replace the conditional with polymorphism

Bird speeds are computed with a switch on kind, duplicated in describe(). The duplicate forgot the load penalty for laden swallows. Give each kind its own class with a speed() method so the rule exists once, matching the output.

Fix it: a type switch repeated twice
def speed(bird):
    if bird["kind"] == "swallow":
        return 35 - 2 * bird["load"]
    elif bird["kind"] == "parrot":
        return 0 if bird["nailed"] else 20
    return 10

def describe(bird):
    if bird["kind"] == "swallow":
        s = 35
    elif bird["kind"] == "parrot":
        s = 0 if bird["nailed"] else 20
    else:
        s = 10
    return f"{bird['kind']} flies at {s}"

flock = [{"kind": "swallow", "load": 3}, {"kind": "parrot", "nailed": False}, {"kind": "duck"}]
for b in flock:
    print(describe(b), "| speed()", speed(b))
output
swallow flies at 29 | speed() 29
parrot flies at 20 | speed() 20
duck flies at 10 | speed() 10

Run and edit it in the app.

What does replacing nested conditionals with guard clauses achieve?

When does Introduce Null Object help?

How it relates

Key terms

  • Guard clause — An early return that handles a special case before the main logic.
  • Decompose conditional — Extracting a condition and its branches into well-named functions.
  • Null object — An object with neutral behaviour used instead of None.
  • Replace Conditional with Polymorphism — Moving each branch of a type switch into a class's method.