Conditionals and match

Updated

September 8, 2026

Conditionals and match

Control flow at the desk is mostly “if this status, do that.” The boring default is a short if / elif / else, a guard clause that returns early, and match / case when you are dispatching on a closed set of ticket statuses. A ternary (x if cond else y) is an expression. Use it for a tiny choice, not for a paragraph.

Mental model

if tests a condition and runs a block. elif is another test, only if the previous tests failed. else runs when none of them matched. Python has no switch statement. Since 3.10 it has match, which compares a value against patterns.

A guard clause is an if at the top of a function that returns (or raises) so the rest of the function can assume the happy path. That beats nesting if paid: if table: if notes: five layers deep.

match ticket["status"]: with case "open": is pattern matching on a string. case _: is the default. You can also match mapping patterns (case {"status": "open", "table": n}:) when the shape is the point. Keep patterns flat. A match that needs a comment to explain the pattern should have been an if.

The ternary label = "paid" if ticket["paid"] else "due" is fine. print("paid" if ticket["paid"] else "due" if ticket["open"] else "void") is not.

Worked examples

Case 1: if / elif / else on a check

Save as check_status.py.

# check_status.py
def describe(ticket):
    if ticket["paid"]:
        return "closed"
    elif ticket["cents"] == 0:
        return "comped"
    else:
        return "due"


def main():
    tickets = [
        {"paid": True, "cents": 1850},
        {"paid": False, "cents": 0},
        {"paid": False, "cents": 400},
    ]
    for t in tickets:
        print(describe(t))


if __name__ == "__main__":
    main()

Run:

uv run python check_status.py

Output:

closed
comped
due

Order matters. A paid zero-cent ticket is "closed" because that test runs first. Put the most specific real-world rule first, then the leftovers.

Case 2: Guard clauses, then the work

# seat.py
def seat(ticket):
    if ticket is None:
        raise ValueError("no ticket")
    if ticket["table"] <= 0:
        raise ValueError(f"table {ticket['table']}: must be positive")
    if ticket["status"] != "open":
        return f"ticket {ticket['id']} not seated"
    return f"seated ticket {ticket['id']} at table {ticket['table']}"


def main():
    print(seat({"id": 7, "table": 12, "status": "open"}))
    print(seat({"id": 8, "table": 4, "status": "paid"}))
    try:
        seat({"id": 9, "table": 0, "status": "open"})
    except ValueError as e:
        print(e)


if __name__ == "__main__":
    main()

Run:

uv run python seat.py

Output:

seated ticket 7 at table 12
ticket 8 not seated
table 0: must be positive

The happy path is one return at the bottom. Bad input raises. A legal-but-not-seatable ticket returns a string. Do not nest those three decisions.

Case 3: match on ticket status

# route_ticket.py
def route(ticket):
    match ticket["status"]:
        case "open":
            return f"send ticket {ticket['id']} to kitchen"
        case "fired":
            return f"ticket {ticket['id']} is on the pass"
        case "paid":
            return f"ticket {ticket['id']} is closed"
        case "void":
            return f"ticket {ticket['id']} was voided"
        case _:
            raise ValueError(f"unknown status {ticket['status']!r}")


def main():
    for status in ("open", "fired", "paid", "void"):
        print(route({"id": 7, "status": status}))
    try:
        route({"id": 7, "status": "lost"})
    except ValueError as e:
        print(e)


if __name__ == "__main__":
    main()

Run:

uv run python route_ticket.py

Output:

send ticket 7 to kitchen
ticket 7 is on the pass
ticket 7 is closed
ticket 7 was voided
unknown status 'lost'

case _: is required if you want a default. Leaving it off makes a non-match a silent no-op, which is how statuses disappear. Raise on unknown.

Case 4: A ternary, kept small

# due_label.py
def due_label(cents):
    return "even" if cents == 0 else f"{cents} due"


def main():
    print(due_label(0))
    print(due_label(400))


if __name__ == "__main__":
    main()

Run:

uv run python due_label.py

Output:

even
400 due

That is the whole point of a ternary: an expression that picks one of two values. The moment you want a third branch, write if / elif / else.

The trap

A chain of ifs that are not elif will run more than one branch. A match without case _: will drop unknown statuses on the floor.

# overlapping.py
def tags(ticket):
    labels = []
    if ticket["cents"] > 0:
        labels.append("has-total")
    if ticket["paid"]:
        labels.append("paid")
    else:
        labels.append("unpaid")
    return labels


def main():
    print(tags({"cents": 400, "paid": True}))


if __name__ == "__main__":
    main()

Run:

uv run python overlapping.py

Output:

['has-total', 'paid']

Here two ifs are correct: paid-ness is independent of the total. The trap is using that shape when you meant one outcome. If a ticket should be exactly one of due / comped / closed, use elif or match, not stacked ifs.

The boring rule

  • One outcome → if / elif / else or match.
  • Independent flags → separate ifs.
  • Guard clauses at the top; happy path last.
  • match on a closed set of statuses. Always handle _ (raise or log).
  • Ternary only for a two-value expression.

Try this

  1. Add a "held" status to route_ticket.py that returns "ticket {id} is held at the bar".
  2. In seat.py, guard on missing "table" with if "table" not in ticket and raise ValueError.
  3. Rewrite due_label with if / else instead of a ternary. Keep the same output.