Control Flow: Advanced

Stripe's payment routing engine evaluates each transaction against a cascade of conditions, checking currency, payment method, country of origin, and real-time fraud risk score before committing to a processing path, and it handles over 500 million such decisions per year on behalf of millions of businesses worldwide. Hardcoding that logic as a chain of if-elif statements would make every new currency or payment type a high-stakes code change touching the core routing path. Instead, Stripe encodes routing rules as data structures, dispatches to handler functions through dictionaries, and tracks each transaction through a state machine that enforces valid lifecycle transitions. The advanced patterns in this lesson, Boolean simplification, De Morgan's laws, dict-based dispatch, and decision tables, are the same tools that let engineers build payment infrastructure that is simultaneously correct, auditable, and safe to extend.

Boolean Simplification

Daily Life
Interviews

Reduce complex conditions to their essence

Boolean expressions can often be simplified to more readable forms without changing their behavior. Just as algebraic expressions can be simplified, Boolean expressions follow rules that let you reduce complexity. Simpler conditions are easier to read, test, and maintain.

Removing Double Negatives

A double negative cancels out. not not x is equivalent to x. While you rarely write this explicitly, it appears in refactored code:

1# Double negative = original value
2x = True
3print("not not True:", not not x)
4
5x = False
6print("not not False:", not not x)
7
8is_disabled = False
9
10# Confusing
11if not is_disabled == False:
12 print("Confusing: enabled")
13
14if not is_disabled:
15 print("Clear: enabled")
16
17# Use positive variable names
18is_enabled = True
19if is_enabled:
20 print("Best: enabled")
>>>Output
not not True: True
not not False: False
Confusing: enabled
Clear: enabled
Best: enabled
TIP
Prefer positive variable names (is_active, is_valid, has_permission) over negative ones (is_not_active, is_invalid). This reduces the need for "not" and makes code more readable.
Boolean simplification follows a handful of identities. Memorizing these common patterns will help you spot redundant conditions at a glance:
x or Truex and Falsenot not xx or False
x or True
Always True
The True absorbs the or
x and False
Always False
The False absorbs the and
not not x
Just x
Double negation cancels
x or False
Just x
False is the or identity

Simplifying Conditions

Some compound conditions have simpler equivalents. Recognizing these patterns helps you write cleaner code:
1x = True
2y = False
3
4# x or True is always True
5print("x or True:", x or True)
6print("False or True:", False or True)
7
8# x and False is always False
9print("x and False:", x and False)
10print("True and False:", True and False)
11
12# x or False is just x
13print("x or False:", x or False)
14
15# x and True is just x
16print("x and True:", x and True)
17
18# Practical example
19is_premium = True
20has_coupon = False
21
22# Before: redundant condition
23# if is_premium or True: # Always true!
24
25# This is equivalent to:
26# if True: # Remove the condition
>>>Output
x or True: True
False or True: True
x and False: False
True and False: False
x or False: True
x and True: True

Simplifying Comparisons

Redundant comparisons can be eliminated. If you already know a value is in a certain range, additional checks may be unnecessary:
1age = 25
2
3if age >= 18 and age > 10:
4 print("Redundant check passed")
5
6if age >= 18:
7 print("Simplified check passed")
8
9# Another example
10score = 85
11
12# Redundant: checking overlapping ranges
13# Before:
14if score >= 70 and score >= 60:
15 print("Redundant")
16
17# After:
18if score >= 70:
19 print("Simplified")
20
21# However, these are NOT redundant:
22if score >= 70 and score < 80:
23 print("This range check is meaningful")
>>>Output
Redundant check passed
Simplified check passed
Redundant
Simplified
Fill in the Blank

> Some compound conditions have simpler equivalents. Pick the version that produces the same result with less code.

score = 85
if :
    print("Passing")
Simplifying boolean expressions makes code easier to audit. Redundant conditions add cognitive load without providing additional safety, and they can mask the actual intent.

Positive variable names reduce negation in conditions. A variable named is_active leads to if is_active: rather than if not is_inactive:, which is both shorter and clearer.

Boolean simplification is especially valuable in code review. When conditions are minimal and clear, reviewers can verify correctness quickly rather than mentally evaluating complex compound expressions.

De Morgan's Laws

Daily Life
Interviews

Transform negated conditions confidently

De Morgan's laws describe how to transform negations of compound Boolean expressions. These laws, named after mathematician Augustus De Morgan, are fundamental to Boolean algebra and appear frequently in interview questions and code simplification. Understanding these transformations helps you simplify complex negations and write more readable conditions.

The Two Laws

De Morgan's laws state that negating an "and" flips it to "or" (and vice versa), while also negating each operand:
1# De Morgan's First Law:
2# not (A and B) == (not A) or (not B)
3
4# De Morgan's Second Law:
5# not (A or B) == (not A) and (not B)
6
7# In words:
8# "not both" = "at least one is not"
9# "not either" = "both are not"
10
11# Let's verify both laws
12print("De Morgan's Laws Verification:")
13print("Law 1: not (A and B) == (not A) or (not B)")
14print("Law 2: not (A or B) == (not A) and (not B)")
>>>Output
De Morgan's Laws Verification:
Law 1: not (A and B) == (not A) or (not B)
Law 2: not (A or B) == (not A) and (not B)
1A = True
2B = False
3
4# not (A and B) = (not A) or (not B)
5left1 = not (A and B)
6right1 = (not A) or (not B)
7print("First law check:")
8print(" not (A and B):", left1)
9print(" (not A) or (not B):", right1)
10print(" Equal:", left1 == right1)
11
12print()
13
14# not (A or B) = (not A) and (not B)
15left2 = not (A or B)
16right2 = (not A) and (not B)
17print("Second law check:")
18print(" not (A or B):", left2)
19print(" (not A) and (not B):", right2)
20print(" Equal:", left2 == right2)
>>>Output
First law check:
not (A and B): True
(not A) or (not B): True
Equal: True
 
Second law check:
not (A or B): False
(not A) and (not B): False
Equal: True

Practical Applications

De Morgan's laws help simplify negated conditions, making them easier to understand:
1is_admin = False
2is_owner = False
3
4# Original: hard to parse
5if not (is_admin and is_owner):
6 print("User is not both admin AND owner")
7
8# Apply De Morgan: easier to understand
9if (not is_admin) or (not is_owner):
10 print("User is missing admin OR owner status")
11
12# Even clearer with positive logic
13has_full_access = is_admin and is_owner
14if not has_full_access:
15 print("User does not have full access")
>>>Output
User is not both admin AND owner
User is missing admin OR owner status
User does not have full access

Simplifying Validation

De Morgan's laws are especially useful when inverting validation conditions:
1age = 25
2has_id = True
3
4# To enter, must be 18+ AND have ID
5can_enter = age >= 18 and has_id
6
7# When is someone NOT allowed to enter?
8# not (age >= 18 and has_id)
9# = (not age >= 18) or (not has_id) # De Morgan
10# = age < 18 or not has_id # Simplify
11
12# These are equivalent:
13cannot_enter_v1 = not (age >= 18 and has_id)
14cannot_enter_v2 = age < 18 or not has_id
15
16print("Cannot enter (v1):", cannot_enter_v1)
17print("Cannot enter (v2):", cannot_enter_v2)
18
19# For a minor without ID:
20age = 16
21has_id = False
22cannot_enter = age < 18 or not has_id
23print("Minor without ID cannot enter:", cannot_enter)
>>>Output
Cannot enter (v1): False
Cannot enter (v2): False
Minor without ID cannot enter: True
Memorize these two transformations. They come up constantly when simplifying negated conditions in validation logic and access control:
Law 1Law 2
Law 1
not (A and B)
Becomes (not A) or (not B)
Law 2
not (A or B)
Becomes (not A) and (not B)
Python Quiz

> De Morgan's two laws swap AND/OR when distributing NOT. Place the correct operator in each law to make the equivalences hold.

a = True
b = False
law1 = (not a) ___ (not b)
law2 = (not a) ___ (not b)
print(law1, law2)
not
or
and
in
is
When you negate an "and" condition, the result splits into an "or" with each part individually denied. This mirrors everyday language: "not (hungry and thirsty)" means "not hungry, or not thirsty" because at least one requirement is unmet.
De Morgan's laws are a reliable tool for eliminating "not" wrapped around compound expressions. The resulting conditions are easier to scan because each operand is negated independently.

Access control logic is a classic application of these laws. Converting not (has_role and is_active) to not has_role or not is_active makes the denial condition explicit and readable at a glance.

Debug Challenge

> Two conditions should behave identically, but the second one uses AND instead of OR after negation. Apply De Morgan's Law to fix it.

The two conditions should behave identically but "Denied v2" uses AND instead of OR after negation

De Morgan's laws are easiest to apply by reading the negated expression aloud. If it sounds like "not both X and Y", the expanded form is "not X or not Y". If it sounds like "neither X nor Y", the expanded form is "not X and not Y".

One practical use of these laws is rewriting guard clauses. A condition that rejects users who are not admin and not verified can be written as not (is_admin or is_verified), which is easier to scan than two separate negated checks.

Boolean transformations become especially important when writing unit tests. Tests that verify a condition is false often benefit from the De Morgan equivalent, which expresses the same requirement in a more affirmative form.

State Machine Patterns

Daily Life
Interviews

Model system lifecycles with explicit states

A state machine is a model where a system can be in exactly one of a finite number of states at any time. The system transitions between states based on events or conditions. State machines are powerful because they make complex behavior explicit and predictable. Unlike implicit state tracked through multiple boolean flags, a state machine makes the current state crystal clear.
Many real-world systems are naturally state machines: an order goes from "placed" to "paid" to "shipped" to "delivered". A user session transitions from "logged out" to "logged in" to "timed out". A document moves from "draft" to "review" to "approved" to "published". Modeling these as explicit state machines makes the logic clear and bugs easier to find.

States and Transitions

A state machine has states (the possible conditions), events (what triggers changes), and transitions (the rules for moving between states):
1# State transitions: current_state -> {event: next_state}
2TRANSITIONS = {
3 "draft": {"submit": "pending"},
4 "pending": {"approve": "approved", "reject": "rejected"},
5 "approved": {"publish": "published"},
6 "published": {},
7}
8
9def transition(state, event):
10 return TRANSITIONS.get(state, {}).get(event, "invalid")
11
12state = "draft"
13for event in ["submit", "approve", "publish", "submit"]:
14 new = transition(state, event)
15 print(f"{state} + {event} -> {new}")
16 if new != "invalid":
17 state = new
>>>Output
draft + submit -> pending
pending + approve -> approved
approved + publish -> published
published + submit -> invalid
The transition logic is simple lookup, not a chain of if-elif statements. Adding new states or transitions means updating the data, not rewriting code. Invalid transitions are rejected automatically, preventing bugs that could occur if state changes were scattered throughout the codebase.

State Machine Class

For more complex state machines, encapsulate the logic in a class:
1class OrderState:
2 T = {"created": {"pay": "paid"}, "paid": {"ship": "shipped"},
3 "shipped": {"deliver": "delivered"}, "delivered": {}}
4
5 def __init__(self):
6 self.state = "created"
7 self.log = [self.state]
8
9 def trigger(self, event):
10 if event in self.T.get(self.state, {}):
11 self.state = self.T[self.state][event]
12 self.log.append(self.state)
13
14order = OrderState()
15for e in ["pay", "ship", "deliver"]:
16 order.trigger(e)
17print("History:", order.log)
>>>Output
History: ['created', 'paid', 'shipped', 'delivered']
State machines give you explicit valid states, clear transition rules, easy-to-test logic, and protection against invalid states. They shine in order and workflow processing, session management, document lifecycles, game logic, and protocol handling.
Do
  • Define all valid states upfront
  • Make transitions explicit in a dict
  • Log every state change for debugging
  • Reject invalid transitions clearly
Don't
  • Track state with multiple booleans
  • Allow implicit state transitions
  • Skip validation on state changes
  • Mix state logic with business logic
Fill in the Blank

> A traffic light uses a dictionary for state transitions. Pick the correct syntax to look up the next state.

state = "green"
transitions = {"green": "yellow", "yellow": "red", "red": "green"}
state = transitions
print(state)
State machines work best when every valid state and every valid transition is defined upfront. Implicit or undocumented states are a common source of bugs in complex systems because they allow the system to enter conditions the code never anticipated.

Using a dictionary of transitions instead of nested if-elif blocks makes it possible to inspect the full set of rules at runtime, which is useful for generating documentation or visualizing the state graph.

State logging is a valuable companion to state machines. Recording each transition with a timestamp creates a complete audit trail that makes debugging and support much easier in production systems.

Dict-Based Dispatch

Daily Life
Interviews

Replace long if-elif chains with lookups

Dict-based dispatch replaces if-elif chains with dictionary lookups. Instead of checking each condition sequentially, you use a key to look up the appropriate handler directly. This is faster for many conditions and makes it easy to add new cases without modifying existing code. The technique is sometimes called a dispatch table or jump table, and it is a fundamental pattern in language interpreters and event-driven systems.

Basic Dispatch Pattern

Store functions or values in a dictionary, keyed by the conditions you would otherwise check:
1# Instead of long if-elif chains:
2def calculate_v1(operation, a, b):
3 if operation == "add":
4 return a + b
5 elif operation == "subtract":
6 return a - b
7 elif operation == "multiply":
8 return a * b
9 elif operation == "divide":
10 return a / b if b != 0 else None
11 else:
12 return None
13
14# Use dict-based dispatch:
15def calculate_v2(operation, a, b):
16 operations = {
17 "add": lambda x, y: x + y,
18 "subtract": lambda x, y: x - y,
19 "multiply": lambda x, y: x * y,
20 "divide": lambda x, y: x / y if y != 0 else None,
21 }
22
23 func = operations.get(operation)
24 if func is None:
25 return None
26 return func(a, b)
27
28print("v1 add:", calculate_v1("add", 10, 3))
29print("v2 add:", calculate_v2("add", 10, 3))
30print("v2 multiply:", calculate_v2("multiply", 10, 3))
31print("v2 unknown:", calculate_v2("power", 10, 3))
>>>Output
v1 add: 13
v2 add: 13
v2 multiply: 30
v2 unknown: None

Dispatch: Named Functions

For more complex operations, use named functions instead of lambdas:
1def handle_create(data):
2 return "Creating: " + str(data)
3
4def handle_update(data):
5 return "Updating: " + str(data)
6
7def handle_delete(data):
8 return "Deleting: " + str(data)
9
10def handle_unknown(data):
11 return "Unknown action for: " + str(data)
12
13HANDLERS = {
14 "create": handle_create,
15 "update": handle_update,
16 "delete": handle_delete,
17}
18
19def process_action(action, data):
20 handler = HANDLERS.get(action, handle_unknown)
21 return handler(data)
22
23print(process_action("create", {"id": 1}))
24print(process_action("update", {"id": 2}))
25print(process_action("archive", {"id": 3}))
>>>Output
Creating: {'id': 1}
Updating: {'id': 2}
Unknown action for: {'id': 3}

Dispatch with Classes

You can also dispatch to methods or class constructors:
1class FileProcessor:
2 def process_csv(self, path):
3 return "Processing CSV: " + path
4
5 def process_json(self, path):
6 return "Processing JSON: " + path
7
8 def process_xml(self, path):
9 return "Processing XML: " + path
10
11 def process(self, path):
12 extension = path.rsplit(".", 1)[-1].lower()
13 handlers = {
14 "csv": self.process_csv,
15 "json": self.process_json,
16 "xml": self.process_xml,
17 }
18 handler = handlers.get(extension)
19 if handler is None:
20 return "Unsupported format: " + extension
21 return handler(path)
22
23processor = FileProcessor()
24print(processor.process("data.csv"))
25print(processor.process("config.json"))
26print(processor.process("report.pdf"))
>>>Output
Processing CSV: data.csv
Processing JSON: config.json
Unsupported format: pdf
TIP
Dict dispatch is O(1) lookup time, while if-elif chains are O(n) where n is the number of conditions. For many conditions (10+), dict dispatch is significantly faster.
There are three common ways to organize your dispatch handlers, each suited to different levels of complexity:
Lambda dispatch
Lambda dispatch
Best for simple one-line operations like arithmetic or formatting
Named function dispatch
Named function dispatch
Better for multi-step logic that benefits from descriptive names
Method dispatch
Method dispatch
Ideal when handlers need shared state or configuration from the class
Python Quiz

> Look up a key that does not exist in the dispatch dictionary. Pick the method that returns a default instead of raising KeyError, and the function that proves the dictionary was not modified.

dispatch = {
    "csv": "parse CSV",
    "json": "parse JSON"
}
result = dispatch.___(
    "xml", "unsupported"
)
print(result)
print(___(dispatch))
type
get
len
pop
setdefault

Always use .get() with a default handler when doing dict dispatch. Direct bracket access raises KeyError for unknown keys, which turns a missing configuration entry into an unhandled exception.

The default handler in a dispatch table serves the same role as the "else" branch in an if-elif chain. It provides a safe fallback that ensures all inputs produce a defined outcome rather than crashing.
Dispatch tables are easy to extend at runtime. You can register new handlers by inserting entries into the dictionary, which enables plugin architectures where behavior is added without modifying the core dispatch logic.
Debug Challenge

> This dict-based dispatch crashes when it encounters an unknown file type. Fix it so unrecognized types are handled gracefully.

KeyError: 'xml' -- the dict has no handler for xml files

Dict dispatch scales better than if-elif chains because adding a new case is a one-line dictionary entry rather than an additional branch in the middle of existing logic. This makes pull requests smaller and review easier.
When a dispatch table grows large, consider splitting it into sub-tables grouped by category. This keeps each group manageable and makes it easy to swap out an entire category of handlers at once.
Dict dispatch pairs naturally with dependency injection. Instead of hardcoding handler functions, the dictionary can be constructed by the caller and passed in, making the dispatching logic easy to test with mock handlers.

Decision Table Lookups

Daily Life
Interviews

Encode business rules as configurable data

A decision table is a data structure that captures business rules as data rather than code. Each row represents a combination of conditions and the resulting action. Decision tables make complex rules explicit, easy to modify, and simple to test. This approach separates the rules themselves from the logic that applies them, enabling non-programmers to review and validate the business logic.
This pattern is essential when business logic changes frequently. Instead of modifying if-elif chains (and risking bugs), you update the decision table. Non-programmers can even review and validate the rules.

Basic Decision Table

Define rules as a list of tuples or dictionaries containing conditions and outcomes. The table is checked top to bottom, and the first matching rule wins. This makes rule priority explicit and easy to understand. Here is a shipping cost calculator implemented as a decision table:
1# Shipping cost decision table
2# (weight_max, zone, cost)
3SHIPPING_RULES = [
4 (1, "domestic", 5.00),
5 (5, "domestic", 10.00),
6 (100, "domestic", 25.00),
7 (1, "international", 15.00),
8 (5, "international", 35.00),
9 (100, "international", 75.00),
10]
11
12def get_shipping_cost(weight, zone):
13 for max_weight, rule_zone, cost in SHIPPING_RULES:
14 if weight <= max_weight and zone == rule_zone:
15 return cost
16 return None
17
18print("0.5kg domestic:", get_shipping_cost(0.5, "domestic"))
19print("3kg domestic:", get_shipping_cost(3, "domestic"))
20print("2kg international:", get_shipping_cost(2, "international"))
21print("50kg domestic:", get_shipping_cost(50, "domestic"))
>>>Output
0.5kg domestic: 5.0
3kg domestic: 10.0
2kg international: 35.0
50kg domestic: 25.0

Decision Tables with Dicts

For more complex rules, use dictionaries for clarity:
1# Discount rules as decision table
2DISCOUNT_RULES = [
3 {"is_premium": True, "cart_min": 100, "discount": 0.20},
4 {"is_premium": True, "cart_min": 0, "discount": 0.10},
5 {"is_premium": False, "cart_min": 200, "discount": 0.10},
6 {"is_premium": False, "cart_min": 100, "discount": 0.05},
7 {"is_premium": False, "cart_min": 0, "discount": 0.00},
8]
9
10def get_discount(is_premium, cart_total):
11 for rule in DISCOUNT_RULES:
12 if (rule["is_premium"] == is_premium and
13 cart_total >= rule["cart_min"]):
14 return rule["discount"]
15 return 0.0
16
17# Test cases
18print("Premium, 150 cart:", get_discount(True, 150))
19print("Premium, 50 cart:", get_discount(True, 50))
20print("Regular, 250 cart:", get_discount(False, 250))
21print("Regular, 80 cart:", get_discount(False, 80))
>>>Output
Premium, 150 cart: 0.2
Premium, 50 cart: 0.1
Regular, 250 cart: 0.1
Regular, 80 cart: 0.0
Rules are checked in order; the first matching rule wins. This means more specific rules should come before general rules.

External Configuration

Decision tables can be loaded from external files, making rules configurable without code changes:
1# Simulate loading from JSON/YAML configuration
2config_data = """ [ {"status": "new", "priority": "high", "assign_to": "senior_team"}, {"status": "new", "priority": "medium", "assign_to": "regular_team"}, {"status": "new", "priority": "low", "assign_to": "queue"}, {"status": "reopened", "priority": "high", "assign_to": "senior_team"}, {"status": "reopened", "priority": "medium", "assign_to": "senior_team"}, {"status": "reopened", "priority": "low", "assign_to": "regular_team"} ] """
3
4import json
5ROUTING_RULES = json.loads(config_data)
6
7def route_ticket(status, priority):
8 for rule in ROUTING_RULES:
9 if rule["status"] == status and rule["priority"] == priority:
10 return rule["assign_to"]
11 return "default_queue"
12
13print("new/high:", route_ticket("new", "high"))
14print("new/low:", route_ticket("new", "low"))
15print("reopened/medium:", route_ticket("reopened", "medium"))
>>>Output
new/high: senior_team
new/low: queue
reopened/medium: senior_team
Explicit and reviewable
Explicit and reviewable
Business rules are laid out as data, not buried in code
Easy to modify
Easy to modify
Add, change, or remove rules without touching code logic
External configuration
External configuration
Load rules from JSON, YAML, or a database at runtime
Straightforward testing
Straightforward testing
Each rule row is an independent test case to validate
Decision tables have a long history in software engineering and business analysis.

Choosing the Right Pattern

Each pattern has its ideal use case. Choose based on your specific needs:
If-Elif Chains
  • Few conditions (2-5)
  • Complex boolean logic
  • Conditions depend on each other
  • Simple, one-off logic
Dict Dispatch
  • Many discrete cases (5+)
  • Action determined by single key
  • Adding cases frequently
  • Need O(1) lookup speed
For problems that involve state transitions or complex rule combinations, two more patterns come into play.
State Machines
  • System has distinct states
  • Transitions have rules
  • Need to prevent invalid states
  • Audit trail required
Decision Tables
  • Many condition combinations
  • Rules change frequently
  • Non-dev review needed
  • External configuration desired

Putting It All Together

Let us combine the advanced patterns from this lesson into a realistic data processing system. This example demonstrates a configurable event processor that uses dict dispatch for handlers, a state machine for tracking processing status, and decision tables for routing rules:
1# Event processor combining multiple patterns
2class EventProcessor:
3 # State machine for event lifecycle
4 STATES = {
5 "received": {"validate": "validated", "reject": "rejected"},
6 "validated": {"process": "processed", "reject": "rejected"},
7 "processed": {"complete": "completed", "retry": "validated"},
8 "rejected": {},
9 "completed": {},
10 }
11
12 # Decision table for routing
13 ROUTING = [
14 {"priority": "critical", "type": "error", "handler": "alert_team"},
15 {"priority": "critical", "type": "metric", "handler": "dashboard"},
16 {"priority": "high", "type": "error", "handler": "log_error"},
17 {"priority": "high", "type": "metric", "handler": "aggregate"},
18 {"priority": "low", "type": "error", "handler": "log_warning"},
19 {"priority": "low", "type": "metric", "handler": "batch"},
20 ]
21
22 def route_event(self, event):
23 """Use decision table for routing."""
24 for rule in self.ROUTING:
25 if (event.get("priority") == rule["priority"] and
26 event.get("type") == rule["type"]):
27 return rule["handler"]
28 return "default_handler"
29
30 def transition(self, state, action):
31 """State machine transition."""
32 valid = self.STATES.get(state, {})
33 return valid.get(action, state)
34
35# Test the combined system
36processor = EventProcessor()
37
38events = [
39 {"id": 1, "priority": "critical", "type": "error"},
40 {"id": 2, "priority": "high", "type": "metric"},
41 {"id": 3, "priority": "low", "type": "error"},
42]
43
44for event in events:
45 handler = processor.route_event(event)
46 state = "received"
47 state = processor.transition(state, "validate")
48 state = processor.transition(state, "process")
49 print("Event", event["id"], "-> handler:", handler, "state:", state)
>>>Output
Event 1 -> handler: alert_team state: processed
Event 2 -> handler: aggregate state: processed
Event 3 -> handler: log_warning state: processed
This example shows how advanced patterns work together. The state machine ensures events follow a valid lifecycle. The decision table makes routing rules explicit and easy to modify. Dict-based dispatch could be added to execute the actual handlers. Each pattern handles one aspect of the system cleanly.

Data Pipeline Application

Here is another example showing how these patterns apply to a data pipeline that processes records with configurable transformation rules:
1# Configurable data transformation pipeline
2TRANSFORM_RULES = {
3 "uppercase": lambda x: x.upper() if isinstance(x, str) else x,
4 "lowercase": lambda x: x.lower() if isinstance(x, str) else x,
5 "double": lambda x: x * 2 if isinstance(x, (int, float)) else x,
6 "abs": lambda x: abs(x) if isinstance(x, (int, float)) else x,
7}
8
9FIELD_CONFIG = [
10 {"field": "name", "transform": "uppercase"},
11 {"field": "email", "transform": "lowercase"},
12 {"field": "score", "transform": "double"},
13 {"field": "balance", "transform": "abs"},
14]
15
16def transform_record(record):
17 """Apply configured transformations to a record."""
18 if record is None:
19 return None
20
21 result = dict(record)
22
23 for config in FIELD_CONFIG:
24 field = config["field"]
25 transform_name = config["transform"]
26
27 # Skip if field not in record
28 if field not in result:
29 continue
30
31 # Get transform function from dispatch table
32 transform_func = TRANSFORM_RULES.get(transform_name)
33 if transform_func is None:
34 continue
35
36 # Apply transformation
37 result[field] = transform_func(result[field])
38
39 return result
40
41# Test the pipeline
42records = [
43 {"name": "alice", "email": "Alice@Test.COM", "score": 50, "balance": -100},
44 {"name": "bob", "email": "BOB@example.org", "score": 75, "balance": 200},
45]
46
47for record in records:
48 transformed = transform_record(record)
49 print("Original:", record["name"], record["score"])
50 print("Transformed:", transformed["name"], transformed["score"])
51 print()
>>>Output
Original: alice 50
Transformed: ALICE 100
 
Original: bob 75
Transformed: BOB 150
 
This pipeline is entirely configurable through data structures. Adding a new transformation means adding an entry to TRANSFORM_RULES. Changing which fields get transformed means updating FIELD_CONFIG. No conditional logic needs to change. This separation of configuration from code is a hallmark of well-designed systems. The same pattern can be extended to load transformations from external configuration files, enabling runtime customization without code changes.

Performance Considerations

When choosing between control flow patterns, consider performance implications:
Performance Guidelines
  • Dict dispatch is O(1) vs O(n) for if-elif chains
  • Decision tables are O(n) but very fast per rule check
  • State machines add minimal overhead for safety gains
  • Boolean simplification reduces runtime evaluations
  • Short-circuit evaluation (and/or) can skip expensive checks
  • Place most likely conditions first in if-elif chains
TIP
For most code, readability matters more than micro-optimization. Use the pattern that makes your intent clearest. Only optimize after profiling shows a bottleneck.

Rule Engine Pattern

Here is a final example that combines multiple patterns into a simple rule engine. This demonstrates how dict dispatch, decision tables, and Boolean simplification work together to create a flexible, maintainable system:
1discount_rules = [
2 {"min_amount": 500, "min_items": 5, "discount": 0.20},
3 {"min_amount": 200, "min_items": 3, "discount": 0.10},
4 {"min_amount": 0, "min_items": 0, "discount": 0.00},
5]
6
7def calculate_discount(amount, item_count):
8 for rule in discount_rules:
9 if amount >= rule["min_amount"] and item_count >= rule["min_items"]:
10 return rule["discount"]
11 return 0.0
12
13def process_order(order):
14 amount = order["amount"]
15 items = order["items"]
16 discount = calculate_discount(amount, items)
17 final_amount = amount * (1 - discount)
18 discount_percent = int(discount * 100)
19 return f"${amount} -> ${final_amount:.0f} ({discount_percent}% off)"
20
21orders = [
22 {"amount": 600, "items": 6},
23 {"amount": 250, "items": 4},
24 {"amount": 50, "items": 2},
25]
26
27for order in orders:
28 print(process_order(order))
>>>Output
$600 -> $480 (20% off)
$250 -> $225 (10% off)
$50 -> $50 (0% off)
This rule engine is highly extensible. Adding a new discount rule means adding a row to the discount_rules table. Supporting a new payment method means adding an entry to payment_handlers. The core logic never needs to change. This is the power of data-driven design. Notice how the complex business logic is now separated into easily testable, modifiable components. Each discount rule can be tested independently, and each payment handler can be validated in isolation.

Refactoring Conditionals

How do you know when your conditional logic needs refactoring? Here are the warning signs that suggest moving to more advanced patterns:
01
5+ elif cases
Long if-elif chains should be replaced with dict dispatch for O(1) lookups
02
Repeated conditions
The same condition checked in multiple places should be extracted into a function or table
03
Frequent changes
Business rules that change often belong in decision tables, not hardcoded logic
04
Deep nesting
Three or more levels of indentation signal a need for guard clauses or refactoring
05
Implicit states
Multiple boolean flags tracking state should be replaced with an explicit state machine
The patterns in this lesson are not just theoretical elegance. They solve real problems that arise as codebases grow. A 50-case if-elif chain might work initially, but becomes unmaintainable over time. Recognizing when to refactor and knowing the right pattern to apply is a key skill for any developer working on production systems.
Many enterprise applications use commercial rule engines that can have thousands of business rules. These systems allow business analysts to modify rules without developer involvement, dramatically reducing time-to-market for business logic changes.
Pipeline Error StrategyStep 1
>

You are building a Python pipeline that ingests CSV files from external vendors, validates each row, transforms values, and loads them into a database. The first batch of 50,000 rows arrives and you discover that roughly 2% contain malformed data.

raw_vendor_data
row_idamountstatusdate
142.50active2024-01-15
2N/Aactive2024-01-16
318.00unknown
May 2026

Row 2 has "N/A" instead of a number, and row 3 has a blank date. How do you handle these bad rows?

Advanced control flow patterns help you handle complex logic cleanly and reliably.
The four patterns in this lesson complement each other well. Boolean simplification reduces noise in individual conditions. De Morgan's laws let you rewrite negations clearly. State machines and dispatch tables lift complexity out of conditional chains entirely and into data structures.
Applying these patterns consistently across a codebase reduces the surface area where conditional bugs can hide and makes it easier for new team members to understand the rules governing system behavior.
PUTTING IT ALL TOGETHER

> You are a senior data engineer at Square building a nightly batch ETL pipeline that classifies each transaction record, catches and logs individual failures, and recovers to continue processing the rest of the batch without crashing the job.

Boolean simplification collapses redundant flag checks in filter conditions so each record enters the classifier with a single clean predicate.
De Morgan's laws rewrite not (is_void or is_duplicate) as not is_void and not is_duplicate, making the skip condition explicit and auditable.
State machine patterns track each record through pending, processing, and done states so the pipeline never re-processes or skips a row.
Dict-based dispatch maps transaction type strings to handler functions with O(1) lookup, and decision table lookups resolve fee rules from a data structure instead of an if-elif chain.
KEY TAKEAWAYS
Simplify boolean expressions by eliminating double negatives and redundant conditions
De Morgan's laws: not (A and B) == (not A) or (not B) and vice versa
State machines model systems with discrete states and controlled transitions
Dict-based dispatch replaces if-elif chains with O(1) dictionary lookups
Decision tables express business rules as data, not code
Choose patterns based on number of conditions, change frequency, and complexity
Separating rules from code makes systems more maintainable and testable
Prefer positive variable names to reduce negation in conditions

Elegant patterns for complex decisions

Category
Python
Difficulty
advanced
Duration
28 minutes
Challenges
0 hands-on challenges

Topics covered: Boolean Simplification, De Morgan's Laws, State Machine Patterns, Dict-Based Dispatch, Decision Table Lookups

Lesson Sections

  1. Boolean Simplification

    Boolean expressions can often be simplified to more readable forms without changing their behavior. Just as algebraic expressions can be simplified, Boolean expressions follow rules that let you reduce complexity. Simpler conditions are easier to read, test, and maintain. Removing Double Negatives Boolean simplification follows a handful of identities. Memorizing these common patterns will help you spot redundant conditions at a glance: Simplifying Conditions Some compound conditions have simple

  2. De Morgan's Laws

    De Morgan's laws describe how to transform negations of compound Boolean expressions. These laws, named after mathematician Augustus De Morgan, are fundamental to Boolean algebra and appear frequently in interview questions and code simplification. Understanding these transformations helps you simplify complex negations and write more readable conditions. The Two Laws De Morgan's laws state that negating an "and" flips it to "or" (and vice versa), while also negating each operand: Practical Appl

  3. State Machine Patterns (concepts: pyMergeIntervals)

    A state machine is a model where a system can be in exactly one of a finite number of states at any time. The system transitions between states based on events or conditions. State machines are powerful because they make complex behavior explicit and predictable. Unlike implicit state tracked through multiple boolean flags, a state machine makes the current state crystal clear. Many real-world systems are naturally state machines: an order goes from "placed" to "paid" to "shipped" to "delivered"

  4. Dict-Based Dispatch (concepts: pyFrequencyCount)

    Basic Dispatch Pattern Store functions or values in a dictionary, keyed by the conditions you would otherwise check: Dispatch: Named Functions For more complex operations, use named functions instead of lambdas: Dispatch with Classes You can also dispatch to methods or class constructors: There are three common ways to organize your dispatch handlers, each suited to different levels of complexity: The default handler in a dispatch table serves the same role as the "else" branch in an if-elif cha

  5. Decision Table Lookups

    A decision table is a data structure that captures business rules as data rather than code. Each row represents a combination of conditions and the resulting action. Decision tables make complex rules explicit, easy to modify, and simple to test. This approach separates the rules themselves from the logic that applies them, enabling non-programmers to review and validate the business logic. This pattern is essential when business logic changes frequently. Instead of modifying if-elif chains (and