Conditionals and match
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.pyOutput:
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.pyOutput:
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.pyOutput:
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.pyOutput:
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.pyOutput:
['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/elseormatch. - Independent flags → separate
ifs. - Guard clauses at the top; happy path last.
matchon a closed set of statuses. Always handle_(raise or log).- Ternary only for a two-value expression.
Try this
- Add a
"held"status toroute_ticket.pythat returns"ticket {id} is held at the bar". - In
seat.py, guard on missing"table"withif "table" not in ticketand raiseValueError. - Rewrite
due_labelwithif/elseinstead of a ternary. Keep the same output.