Control Flow: Intermediate

Airbnb's booking system must validate dozens of conditions before confirming any reservation, including calendar availability, payment authorization, identity verification, age restrictions, and local legal requirements, and it uses Python's short-circuit evaluation and guard clauses to stop checking the moment any one condition fails. Rather than running every check regardless of outcome, the system exits at the first failure, saving computation and returning a precise error to the user instantly. This approach keeps the booking code flat and readable, with each validation rule standing alone as a clear guard rather than buried inside nested if-else blocks. The patterns you learn in this lesson are exactly how engineers at Airbnb keep complex multi-condition logic maintainable as regulations and business rules continue to evolve.

Guard Clauses

Daily Life
Interviews

Reject bad inputs before they cause bugs

A guard clause is a conditional statement at the beginning of a function or code block that checks for invalid or edge cases and exits early. Instead of nesting your main logic inside an if block, you check for the "bad" cases first and handle them immediately. This keeps your main logic at the top level of indentation.
The term "guard" comes from the idea that these clauses guard the main logic from invalid inputs. They stand at the entrance and turn away anything that should not proceed.

The Problem: Deep Nesting

Consider code that validates multiple conditions before proceeding. Without guard clauses, you end up with deeply nested code that is hard to follow:
1# Without guard clauses - deeply nested
2def process_user_deeply_nested(user):
3 if user is not None:
4 if user.get("active"):
5 if user.get("verified"):
6 if user.get("balance", 0) > 0:
7 return "Processing: " + user.get("name")
8 else:
9 return "Error: No balance"
10 else:
11 return "Error: Not verified"
12 else:
13 return "Error: Inactive"
14 else:
15 return "Error: No user"
16
17# Test it
18user = {"name": "Alice", "active": True, "verified": True, "balance": 100}
19print(process_user_deeply_nested(user))
>>>Output
Processing: Alice
Notice how the main logic (processing the user) is buried four levels deep. Each condition pushes the happy path further to the right. This is often called the "arrow anti-pattern" because the code forms an arrow shape.

Refactoring: Guard Clauses

Guard clauses invert the conditions and return early for invalid cases. The main logic stays at the base indentation level:
1# With guard clauses - flat and readable
2def process_user(user):
3 # Guard clauses handle edge cases first
4 if user is None:
5 return "Error: No user"
6
7 if not user.get("active"):
8 return "Error: Inactive"
9
10 if not user.get("verified"):
11 return "Error: Not verified"
12
13 if user.get("balance", 0) <= 0:
14 return "Error: No balance"
15
16 # Main logic at base indentation
17 return "Processing: " + user.get("name")
18
19# Test with various users
20valid = {"name": "Alice", "active": True, "verified": True, "balance": 100}
21inactive = {"name": "Bob", "active": False, "verified": True, "balance": 50}
22
23print(process_user(valid))
24print(process_user(inactive))
25print(process_user(None))
>>>Output
Processing: Alice
Error: Inactive
Error: No user
Each guard clause is independent and easy to understand. You can read them from top to bottom: "If no user, error. If inactive, error. If not verified, error. If no balance, error. Otherwise, process."
The guard clause pattern follows a consistent sequence that keeps your main logic clean and readable:
01
Identify bad input
List every edge case that should prevent normal processing
02
Check and exit early
Each guard tests one condition and returns immediately if it fails
03
Write main logic
After all guards pass, write the happy path at the base indentation

Guard Clauses Everywhere

You can apply the guard clause pattern even when not using functions, by continuing to the next iteration or breaking early:
1records = [
2 {"id": 1, "value": 100},
3 {"id": 2, "value": None},
4 {"id": 3, "value": -50},
5 {"id": 4, "value": 200},
6]
7
8valid_records = []
9
10for record in records:
11 # Guard: skip if value is None
12 if record.get("value") is None:
13 print("Skipping record", record["id"], "- no value")
14 continue
15
16 # Guard: skip negative values
17 if record["value"] < 0:
18 print("Skipping record", record["id"], "- negative value")
19 continue
20
21 # Main logic: process valid record
22 valid_records.append(record)
23
24print("Valid records:", len(valid_records))
>>>Output
Skipping record 2 - no value
Skipping record 3 - negative value
Valid records: 2
TIP
Guard clauses work best when you have many conditions to check. If you only have one or two simple conditions, a regular if-else might be clearer.
Fill in the Blank

> A function receives a value that might be None. Pick what the guard clause should do when it detects invalid input.

def process(value):
    if value is None:
        
    return value * 2
Guard clauses make the contract of a function explicit. When you list preconditions at the top, any reader immediately knows what the function requires before it will do anything meaningful.
The "return early" style keeps the happy path at the leftmost indentation level. This also makes it easy to add new guards later without restructuring the entire function body.

In loops, guard clauses use continue instead of return to skip invalid records while keeping the loop running. This pattern is common in data pipelines where you want to process as many records as possible and quarantine the bad ones.

Chained Comparisons

Daily Life
Interviews

Validate ranges with readable expressions

Python allows you to chain comparison operators in a way that reads naturally. Instead of writing x > 5 and x < 10, you can write 5 < x < 10. This matches how you would express ranges in mathematics and makes your code more readable.

Basic Chained Comparisons

You can chain any comparison operators together. Python evaluates them left to right, and all comparisons must be true for the entire expression to be true:
1x = 15
2
3# Instead of: x > 10 and x < 20
4if 10 < x < 20:
5 print(x, "is between 10 and 20 (exclusive)")
6
7# Instead of: x >= 10 and x <= 20
8if 10 <= x <= 20:
9 print(x, "is between 10 and 20 (inclusive)")
10
11# Chain more than two comparisons
12y = 5
13if 0 < y < 10 < x < 20:
14 print("Both y and x are in their expected ranges")
15
16# Works with variables on all sides
17low = 10
18high = 20
19if low < x < high:
20 print(x, "is between", low, "and", high)
>>>Output
15 is between 10 and 20 (exclusive)
15 is between 10 and 20 (inclusive)
Both y and x are in their expected ranges
15 is between 10 and 20

The expression 10 < x < 20 is equivalent to 10 < x and x < 20, but shorter and more readable. Python evaluates x only once.

Chained comparisons are especially useful in data validation, where you frequently need to confirm values fall within acceptable ranges:
a < x < ba <= x <= ba == b == ca < b < c
a < x < b
Exclusive
Value between, not equal
a <= x <= b
Inclusive
Value between or on edge
a == b == c
All equal
Every value is identical
a < b < c
Sorted order
Values are ascending left

Practical Range Checking

Chained comparisons are perfect for validating that values fall within expected ranges:
1def validate_percentage(value):
2 """Validate percentage."""
3 if 0 <= value <= 100:
4 return "Valid percentage"
5 return "Invalid: must be 0-100"
6
7def validate_age(age):
8 """Validate age."""
9 if 0 < age < 150:
10 return "Valid age"
11 return "Invalid age"
12
13def categorize_temperature(temp):
14 """Categorize temperature."""
15 if temp < 32:
16 return "freezing"
17 elif 32 <= temp < 50:
18 return "cold"
19 elif 50 <= temp < 70:
20 return "cool"
21 elif 70 <= temp < 85:
22 return "warm"
23 else:
24 return "hot"
25
26print(validate_percentage(75))
27print(validate_percentage(150))
28print(categorize_temperature(65))
>>>Output
Valid percentage
Invalid: must be 0-100
cool

Chaining Comparisons

You can also chain equality operators, which is useful for checking if multiple values are equal:
1a = 5
2b = 5
3c = 5
4
5# Check if all three are equal
6if a == b == c:
7 print("All three values are equal")
8
9# Check if sorted in order
10x = 1
11y = 2
12z = 3
13
14if x < y < z:
15 print("Values are in ascending order")
16
17if x <= y <= z:
18 print("Values are in non-descending order")
19
20# Mix of operators
21score = 85
22if 80 <= score < 90:
23 print("Grade: B")
>>>Output
All three values are equal
Values are in ascending order
Values are in non-descending order
Grade: B
Debug Challenge

> This percentage validator has a condition that can never be True. Fix the logic so invalid values are caught correctly.

Logic error: the elif condition can never be True. A number cannot be both less than 0 AND greater than or equal to 100.

Chained comparisons are especially easy to get wrong when combining inequality operators. The safest approach is to read the chain aloud as a mathematical expression: "Is value at least 0 and at most 100?" maps directly to "0 <= value <= 100".
When the valid range is clear, an else clause handles everything outside it without needing a second chained comparison. Trying to express the invalid range as its own chain often leads to a condition that can never be satisfied.

Chained comparisons only work in one direction. Python evaluates a < x < b left to right, so a < x and x < b. If you reverse the direction inconsistently, like a < x and x >= b combined, you may accidentally overlap or leave gaps in your range logic.

Pattern Matching with match-case

Daily Life
Interviews

Dispatch on value patterns cleanly

Python 3.10 introduced match-case, also known as structural pattern matching. It provides a cleaner way to handle multiple conditions compared to long if-elif-else chains. The match statement compares a value against several patterns and executes the code for the first matching pattern.

Basic match-case Syntax

The match keyword is followed by the value to match, then case clauses define patterns to match against:

1def get_day_type(day):
2 match day:
3 case "Saturday" | "Sunday":
4 return "weekend"
5 case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
6 return "weekday"
7 case _:
8 return "unknown"
9
10print(get_day_type("Saturday"))
11print(get_day_type("Wednesday"))
12print(get_day_type("Holiday"))
>>>Output
weekend
weekday
unknown

The | operator matches multiple patterns (like "or"). The underscore _ is a wildcard that matches anything, like a default case.

Matching Values

Match-case is excellent for handling discrete values like status codes, commands, or types:
1def handle_http_status(code):
2 match code:
3 case 200:
4 return "OK"
5 case 201:
6 return "Created"
7 case 400:
8 return "Bad Request"
9 case 401:
10 return "Unauthorized"
11 case 403:
12 return "Forbidden"
13 case 404:
14 return "Not Found"
15 case 500:
16 return "Internal Server Error"
17 case _:
18 return "Unknown Status"
19
20print(handle_http_status(200))
21print(handle_http_status(404))
22print(handle_http_status(418))
>>>Output
OK
Not Found
Unknown Status

Matching with Guards

You can add an if clause after a pattern to add additional conditions. This is called a guard:

1def categorize_number(n):
2 match n:
3 case 0:
4 return "zero"
5 case n if n < 0:
6 return "negative"
7 case n if n % 2 == 0:
8 return "positive even"
9 case _:
10 return "positive odd"
11
12print(categorize_number(0))
13print(categorize_number(-5))
14print(categorize_number(8))
15print(categorize_number(7))
>>>Output
zero
negative
positive even
positive odd
Guards let you add conditions that go beyond simple value matching. The pattern variable (n in this case) captures the matched value for use in the guard and the block.
Match-case supports several pattern types. Each serves a different matching strategy:
LiteralWildcardCaptureGuardOR |
Literal
Exact values
Matches a specific number
Wildcard
Catch-all _
Default if nothing else
Capture
Bind variable
Stores value for later use
Guard
if condition
Adds extra boolean check
OR |
Alternatives
Matches any of the options

Matching Sequences

Match-case can destructure sequences like lists and tuples, matching both structure and content. This is powerful for parsing data structures where the shape of the data determines how to handle it. Pattern matching automatically extracts values from the matched structure and binds them to variables:
1def describe_point(point):
2 match point:
3 case (0, 0):
4 return "origin"
5 case (x, 0):
6 return "on x-axis at " + str(x)
7 case (0, y):
8 return "on y-axis at " + str(y)
9 case (x, y):
10 return "point at (" + str(x) + ", " + str(y) + ")"
11 case _:
12 return "not a point"
13
14print(describe_point((0, 0)))
15print(describe_point((5, 0)))
16print(describe_point((0, 3)))
17print(describe_point((2, 4)))
>>>Output
origin
on x-axis at 5
on y-axis at 3
point at (2, 4)
Use match-case When
  • Many discrete values to handle
  • Pattern matching on structure
  • Command/event dispatching
  • Cleaner than long elif chains
Use if-elif When
  • Complex boolean conditions
  • Only 2-3 conditions
  • Range-based comparisons
  • Python version < 3.10
Fill in the Blank

> A command classifier uses match-case. Pick the correct pattern for the default case that catches unrecognized commands.

def classify(command):
    match command:
        case "start":
            return "Starting"
        case "stop":
            return "Stopping"
        case :
            return "Unknown"
print(classify("restart"))

The wildcard pattern _ is the match-case equivalent of the else clause. Always include it to handle unexpected values gracefully rather than silently returning None when no case matches.

The | operator in match-case lets you group values that share the same handling. This is cleaner than a long elif chain with many == comparisons when several inputs lead to the same outcome.

Match-case was introduced in Python 3.10, so it is not available in older environments. In data pipelines running on legacy infrastructure, if-elif chains remain the appropriate alternative.
Python Quiz

> Classify a day as weekend or weekday using pattern matching. Pick the keyword that starts pattern matching, and the operator that combines alternative patterns in a single case.

def day_type(day):
    ___ day:
        case "Sat" ___ "Sun":
            return "weekend"
        case _:
            return "weekday"
print(day_type("Sat"))
if
&
match
|
switch
Match-case reads naturally when each case corresponds to a meaningful category. The structure forces you to be explicit about every value the code handles, which makes exhaustive handling of a set of inputs clearer than a scattered chain of elif statements.
Guard clauses inside match cases (using "if" after the pattern) let you add conditions that go beyond simple value equality. This keeps complex logic organized inside a single match block rather than spreading it across nested if statements.

When you find yourself writing a long elif chain that compares one variable against many literal values, that is usually a signal to consider replacing it with match-case for improved readability and intent clarity.

Conditional Assignment

Daily Life
Interviews

Assign values based on conditions inline

Conditional assignment lets you assign a value to a variable based on a condition, all in a single line. This is also called a ternary expression or conditional expression. It makes your code more concise when you need to choose between two values.

Ternary Expression Syntax

The syntax is: value_if_true if condition else value_if_false. The condition goes in the middle:

1age = 20
2
3# Instead of:
4# if age >= 18:
5# status = "adult"
6# else:
7# status = "minor"
8
9# Use conditional expression:
10status = "adult" if age >= 18 else "minor"
11print("Status:", status)
12
13# More examples
14score = 85
15grade = "pass" if score >= 60 else "fail"
16print("Grade:", grade)
17
18temperature = 25
19weather = "warm" if temperature > 20 else "cool"
20print("Weather:", weather)
>>>Output
Status: adult
Grade: pass
Weather: warm

The conditional expression evaluates the condition first. If True, it returns the value before if. If False, it returns the value after else.

Using in Function Calls

Conditional expressions are especially useful when passing arguments to functions or building strings:
1items = ["apple", "banana", "cherry"]
2empty_list = []
3
4# Use in function calls
5print("Items:" if items else "No items")
6print("Items:" if empty_list else "No items")
7
8# Building messages
9count = 5
10message = str(count) + " item" + ("s" if count != 1 else "")
11print(message)
12
13count = 1
14message = str(count) + " item" + ("s" if count != 1 else "")
15print(message)
16
17# Choose which function result to use
18x = -5
19result = abs(x) if x < 0 else x
20print("Result:", result)
>>>Output
Items:
No items
5 items
1 item
Result: 5

Nested Conditionals

You can nest conditional expressions, but this quickly becomes hard to read. Use with caution:
1score = 75
2
3# Nested conditional expression - hard to read!
4grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "F"
5print("Grade:", grade)
6
7# Better: use regular if-elif for more than 2 choices
8# or split into multiple lines for readability:
9grade = (
10 "A" if score >= 90 else
11 "B" if score >= 80 else
12 "C" if score >= 70 else
13 "F"
14)
15print("Grade (formatted):", grade)
>>>Output
Grade: C
Grade (formatted): C
TIP
Limit conditional expressions to simple two-way choices. If you have more than two options or complex conditions, use regular if-elif-else for clarity.
Ternary expressions are powerful but easy to misuse. Follow these guidelines to keep your conditional assignments readable:
Do
  • Use for simple two-way value selection
  • Keep both values short and clear
  • Split long expressions across lines
  • Use parentheses for nested ternaries
Don't
  • Chain more than two ternaries
  • Put side effects inside ternaries
  • Use when logic is complex
  • Sacrifice readability for brevity

Default Values with or

A common pattern uses or to provide default values. If the left side is falsy (None, empty, 0, False), the right side is used:

1# Get name or use default
2user_name = None
3display_name = user_name or "Anonymous"
4print(display_name)
5
6# Get from dict with fallback
7config = {"timeout": 30}
8timeout = config.get("timeout") or 60
9retries = config.get("retries") or 3
10
11print("Timeout:", timeout)
12print("Retries:", retries)
13
14# Careful with 0 - it's falsy!
15value = 0
16result = value or 100 # This returns 100, not 0!
17print("Result:", result)
18
19# Use "is not None" for values that might be 0
20value = 0
21result = value if value is not None else 100
22print("Safe result:", result)
>>>Output
Anonymous
Timeout: 30
Retries: 3
Result: 100
Safe result: 0
Fill in the Blank

> The value is 0, which is valid but falsy. Pick the ternary condition that correctly preserves 0 instead of replacing it.

value = 0
result = value if  else 100
print(result)

The or default pattern is convenient but dangerous when 0, False, or empty string are valid values. Switching to an explicit is not None check makes the intent unambiguous and prevents hard-to-find data loss bugs.

Ternary expressions work best for straightforward two-option assignments. When the condition or either value is long, split the expression across lines with parentheses to preserve readability.
Conditional assignment keeps related logic together on one line rather than spreading a simple value choice across four lines of if-else. This makes code scanning faster when you are reviewing a function's outputs at a glance.

Edge Case Handling

Daily Life
Interviews

Handle None, empty, and boundary inputs

Edge cases are inputs or situations at the boundaries of what your code handles: empty collections, zero values, negative numbers, None values, and extreme values. Robust code anticipates and handles these cases explicitly. Failure to handle edge cases is one of the most common sources of bugs.

Common Edge Cases

Here are the most common edge cases you should always consider:
None / null values
Missing or unset data that causes crashes when accessed
Empty collections
Lists, strings, and dicts with zero elements
Zero values
Division by zero, zero-length strings, index zero edge cases
Negative numbers
When only positive values are expected by the logic
Boundary values
First and last elements, min and max limits
Invalid types
A string where an integer was expected, or vice versa

Handling None Values

None represents the absence of a value. Always check for None before using a value that might not exist:

1def get_length(items):
2 # Guard against None
3 if items is None:
4 return 0
5 return len(items)
6
7print(get_length([1, 2, 3]))
8print(get_length(None))
9
10# Using "is" for None
11value = None
12
13# Check for None
14if value is None:
15 print("Value is None")
16
17if value == None:
18 print("Value equals None")
19
20# if not value: # Risky
>>>Output
3
0
Value is None
Value equals None

Always use is None rather than == None. The is operator checks identity, which is what you want for None.

Handling Empty Collections

Empty lists, strings, and dicts are falsy in Python, but you should often handle them explicitly:
1def calculate_average(numbers):
2 # Guard against empty list
3 if not numbers:
4 return 0
5
6 return sum(numbers) / len(numbers)
7
8print(calculate_average([10, 20, 30]))
9print(calculate_average([]))
10
11def get_first(items):
12 # Guard against empty
13 if not items:
14 return None
15 return items[0]
16
17print(get_first(["a", "b", "c"]))
18print(get_first([]))
19
20# Check before accessing
21data = {"users": []}
22if data.get("users"):
23 print("Has users")
24else:
25 print("No users")
>>>Output
20.0
0
a
None
No users
Python Quiz

> Guard against division by zero by checking the denominator first. Pick the comparison that detects zero, and the division operator that returns a float.

def safe_divide(a, b):
    if b ___ 0:
        return 0
    return a ___ b
print(safe_divide(10, 0))
print(safe_divide(10, 2))
==
//
/
>=
!=
Division by zero and index-out-of-range errors are among the most common runtime crashes in data pipelines. A single guard clause at the start of a function is all it takes to make these errors safe and explicit.

None checks deserve special attention. Unlike other edge cases, None can appear anywhere a value is expected: from missing dictionary keys, unset function parameters, and failed lookups. Checking is None before use is a habit that pays off in reliability.

Combining None and empty-collection guards in the right order matters. If you call len(items) before checking items is None, you will get a TypeError before your None guard even has a chance to run.

Debug Challenge

> This function crashes on empty lists and None inputs. Add guard clauses so it handles edge cases safely.

IndexError: list index out of range (for empty list) and TypeError (for None)

Boundary Conditions

Pay special attention to boundary values: the first and last elements, minimum and maximum values:
1def safe_divide(a, b):
2 # Guard against division by zero
3 if b == 0:
4 return None
5 return a / b
6
7print(safe_divide(10, 2))
8print(safe_divide(10, 0))
9
10def get_element(items, index):
11 # Guard against out-of-bounds
12 if index < 0 or index >= len(items):
13 return None
14 return items[index]
15
16data = [10, 20, 30]
17print(get_element(data, 1))
18print(get_element(data, 10))
19print(get_element(data, -5))
>>>Output
5.0
None
20
None
None

Defensive Programming

A defensive programming approach handles edge cases at the start of every function:
1def process_records(records):
2 """Process a list of records safely."""
3 # 1. Handle None
4 if records is None:
5 print("Error: records is None")
6 return []
7
8 # 2. Handle wrong type
9 if not isinstance(records, list):
10 print("Error: records must be a list")
11 return []
12
13 # 3. Handle empty
14 if len(records) == 0:
15 print("Warning: no records to process")
16 return []
17
18 # 4. Main logic - records is non-empty
19 processed = []
20 for record in records:
21 if record is not None:
22 processed.append(str(record).upper())
23
24 return processed
25
26print(process_records(["a", "b", "c"]))
27print(process_records(None))
28print(process_records("not a list"))
29print(process_records([]))
>>>Output
['A', 'B', 'C']
Error: records is None
[]
Error: records must be a list
[]
Warning: no records to process
[]

Putting It All Together

Let us combine the patterns from this lesson into a realistic example. This data validation function uses guard clauses, chained comparisons, conditional expressions, and proper edge case handling:
1def validate_user_record(record):
2 """Validate a user record with comprehensive checks."""
3 # Guard: handle None input
4 if record is None:
5 return {"valid": False, "error": "Record is None"}
6
7 # Guard: check required fields exist
8 required = ["name", "email", "age"]
9 for field in required:
10 if field not in record:
11 return {"valid": False, "error": "Missing " + field}
12
13 # Validate name (non-empty string)
14 name = record["name"]
15 if not name or not isinstance(name, str):
16 return {"valid": False, "error": "Invalid name"}
17
18 # Validate email (contains @)
19 email = record["email"]
20 if not email or "@" not in email:
21 return {"valid": False, "error": "Invalid email"}
22
23 # Validate age (reasonable range)
24 age = record["age"]
25 if not 0 < age < 150:
26 return {"valid": False, "error": "Invalid age"}
27
28 # Determine user category
29 category = (
30 "minor" if age < 18 else
31 "adult" if age < 65 else
32 "senior"
33 )
34
35 return {
36 "valid": True,
37 "name": name.strip(),
38 "email": email.lower(),
39 "age": age,
40 "category": category
41 }
42
43# Test with various inputs
44tests = [
45 {"name": "Alice", "email": "alice@example.com", "age": 25},
46 {"name": "", "email": "test@test.com", "age": 30},
47 {"name": "Bob", "email": "invalid", "age": 40},
48 {"name": "Carol", "email": "carol@test.com", "age": 200},
49 None,
50]
51
52for test in tests:
53 result = validate_user_record(test)
54 if result["valid"]:
55 print("Valid:", result["name"], "-", result["category"])
56 else:
57 print("Invalid:", result["error"])
>>>Output
Valid: Alice - adult
Invalid: Invalid name
Invalid: Invalid email
Invalid: Invalid age
Invalid: Record is None
This function demonstrates several intermediate control flow patterns working together. Guard clauses at the top handle invalid inputs immediately. Chained comparisons validate the age range naturally. A conditional expression assigns the user category concisely. The function returns early for each error case, keeping the success path at the end. This structure makes the code self-documenting and easy to maintain as requirements evolve.

Data Processing Flow

Here is another practical example showing how to filter and transform a dataset using the patterns from this lesson. This kind of batch processing is common in ETL pipelines and data validation systems:
1# Process a batch of transaction records
2transactions = [
3 {"id": 1, "amount": 150, "type": "purchase", "status": "completed"},
4 {"id": 2, "amount": -50, "type": "refund", "status": "completed"},
5 {"id": 3, "amount": 0, "type": "purchase", "status": "pending"},
6 {"id": 4, "amount": 500, "type": "purchase", "status": "completed"},
7 {"id": 5, "amount": None, "type": "unknown", "status": "error"},
8]
9
10valid_total = 0
11invalid_count = 0
12
13for tx in transactions:
14 # Guard: skip if amount is invalid
15 amount = tx.get("amount")
16 if amount is None or amount <= 0:
17 invalid_count += 1
18 continue
19
20 # Guard: only process completed transactions
21 if tx.get("status") != "completed":
22 continue
23
24 # Categorize by amount
25 size = "large" if amount >= 200 else "small"
26
27 valid_total += amount
28 print("Processed tx", tx["id"], ":", amount, "(" + size + ")")
29
30print("---")
31print("Valid total:", valid_total)
32print("Invalid count:", invalid_count)
>>>Output
Processed tx 1 : 150 (small)
Processed tx 4 : 500 (large)
---
Valid total: 650
Invalid count: 2
This data processing loop uses guard clauses with continue to skip invalid records, keeping the main processing logic clean and readable. Each guard clause handles one type of invalid data, making it easy to understand and modify the validation rules. The pattern scales well to complex pipelines with many validation steps, as each check remains independent and testable.

Chained Comparisons

Here is a more comprehensive example showing how chained comparisons simplify range-based validation and categorization in data pipelines:
1def analyze_metrics(metrics):
2 """Analyze performance metrics with range-based categorization."""
3 results = []
4
5 for metric in metrics:
6 name = metric["name"]
7 value = metric["value"]
8
9 # Categorize using chained comparisons
10 if value < 0:
11 status = "error"
12 message = "Negative value detected"
13 elif 0 <= value < 50:
14 status = "critical"
15 message = "Below acceptable threshold"
16 elif 50 <= value < 80:
17 status = "warning"
18 message = "Room for improvement"
19 elif 80 <= value <= 100:
20 status = "healthy"
21 message = "Within normal range"
22 else:
23 status = "error"
24 message = "Value exceeds maximum"
25
26 results.append({
27 "name": name,
28 "status": status,
29 "message": message
30 })
31
32 return results
33
34# Test the analyzer
35metrics = [
36 {"name": "CPU Usage", "value": 75},
37 {"name": "Memory", "value": 92},
38 {"name": "Disk Space", "value": 45},
39 {"name": "Network", "value": 88},
40]
41
42for result in analyze_metrics(metrics):
43 print(result["name"] + ":", result["status"], "-", result["message"])
>>>Output
CPU Usage: warning - Room for improvement
Memory: healthy - Within normal range
Disk Space: critical - Below acceptable threshold
Network: healthy - Within normal range
The chained comparisons make the range logic immediately clear. Reading "50 <= value < 80" is more intuitive than "value >= 50 and value < 80". This clarity is especially valuable when you return to code after months and need to quickly understand the boundaries. Python evaluates the expression efficiently, checking each comparison in sequence and stopping as soon as one fails.

Guard Clauses: API Handlers

Guard clauses are particularly valuable in functions that handle API requests or user input, where many things can go wrong. Here is a realistic example of a user registration handler:
1def register_user(request):
2 """Handle user registration with comprehensive validation."""
3 # Guard: request must exist
4 if request is None:
5 return {"error": "No request data", "code": 400}
6
7 # Guard: required fields
8 if "email" not in request:
9 return {"error": "Email is required", "code": 400}
10
11 if "password" not in request:
12 return {"error": "Password is required", "code": 400}
13
14 # Guard: email format
15 email = request["email"]
16 if "@" not in email or "." not in email.split("@")[-1]:
17 return {"error": "Invalid email format", "code": 400}
18
19 # Guard: password strength
20 password = request["password"]
21 if len(password) < 8:
22 return {"error": "Password must be at least 8 characters", "code": 400}
23
24 if password.lower() == password:
25 return {"error": "Password must contain uppercase letters", "code": 400}
26
27 # Guard: check if email already exists (simulated)
28 existing_users = ["alice@example.com", "bob@example.com"]
29 if email.lower() in existing_users:
30 return {"error": "Email already registered", "code": 409}
31
32 # Success case - all guards passed
33 return {
34 "success": True,
35 "message": "User registered successfully",
36 "email": email.lower(),
37 "code": 201
38 }
39
40# Test with various inputs
41test_requests = [
42 {"email": "new@example.com", "password": "SecurePass123"},
43 {"email": "invalid-email", "password": "Test1234"},
44 {"email": "alice@example.com", "password": "Test1234"},
45 {"email": "test@example.com", "password": "short"},
46]
47
48for req in test_requests:
49 result = register_user(req)
50 if result.get("success"):
51 print("OK:", result["message"])
52 else:
53 print("Error:", result["error"])
>>>Output
OK: User registered successfully
Error: Invalid email format
Error: Email already registered
Error: Password must be at least 8 characters
Each guard clause handles one specific validation failure. The function reads from top to bottom like a checklist: "If no request, error. If no email, error. If email invalid, error." This structure makes it easy to add new validations or modify existing ones without affecting other parts of the function. The pattern also makes testing straightforward, as each guard clause can be tested independently with inputs designed to trigger it.
Well-structured control flow makes code easier to read, test, and maintain.
PUTTING IT ALL TOGETHER

> You are a data engineer at Adyen building a transaction routing script that directs payments to different processing paths based on currency, amount, and risk score. The script must reject invalid inputs early, validate numeric bounds, dispatch by currency type, assign fee tiers conditionally, and handle missing or zero-value fields without crashing.

Guard clauses return early when currency is None or amount <= 0 so invalid transactions never reach the routing logic below.
Chained comparisons like 0 < amount <= 10000 validate that a transaction amount falls within an accepted processing band in one expression.
match-case dispatches each currency string to its corresponding processor path, with the wildcard _ case catching any unsupported currency.
Conditional assignment sets fee_tier = "premium" if risk_score > 80 else "standard" in one line, and edge case handling guards against a None risk_score with is None.
KEY TAKEAWAYS
Guard clauses exit early for invalid cases, keeping main logic at base indentation
Chained comparisons like a < x < b are more readable than using "and" for range checks
match-case provides cleaner multi-way branching than long elif chains (Python 3.10+)
Use _ as the wildcard/default case in match statements
Conditional expressions x if condition else y assign values based on conditions in one line
Always handle edge cases: None values, empty collections, zero, and boundaries
Use is None rather than == None for None checks
The "or" operator provides default values but treats 0 and "" as falsy

Control Flow: Intermediate

Writing cleaner conditional logic

Category
Python
Difficulty
intermediate
Duration
33 minutes
Challenges
0 hands-on challenges

Topics covered: Guard Clauses, Chained Comparisons, Pattern Matching with match-case, Conditional Assignment, Edge Case Handling

Lesson Sections

  1. Guard Clauses (concepts: pyGuardClauses)

    A guard clause is a conditional statement at the beginning of a function or code block that checks for invalid or edge cases and exits early. Instead of nesting your main logic inside an if block, you check for the "bad" cases first and handle them immediately. This keeps your main logic at the top level of indentation. The term "guard" comes from the idea that these clauses guard the main logic from invalid inputs. They stand at the entrance and turn away anything that should not proceed. The P

  2. Chained Comparisons

    Basic Chained Comparisons You can chain any comparison operators together. Python evaluates them left to right, and all comparisons must be true for the entire expression to be true: Chained comparisons are especially useful in data validation, where you frequently need to confirm values fall within acceptable ranges: Practical Range Checking Chained comparisons are perfect for validating that values fall within expected ranges: Chaining Comparisons You can also chain equality operators, which i

  3. Pattern Matching with match-case (concepts: pyMatchCase)

    Basic match-case Syntax Matching Values Match-case is excellent for handling discrete values like status codes, commands, or types: Matching with Guards Guards let you add conditions that go beyond simple value matching. The pattern variable (n in this case) captures the matched value for use in the guard and the block. Match-case supports several pattern types. Each serves a different matching strategy: Matching Sequences Match-case can destructure sequences like lists and tuples, matching both

  4. Conditional Assignment

    Conditional assignment lets you assign a value to a variable based on a condition, all in a single line. This is also called a ternary expression or conditional expression. It makes your code more concise when you need to choose between two values. Ternary Expression Syntax Using in Function Calls Conditional expressions are especially useful when passing arguments to functions or building strings: Nested Conditionals You can nest conditional expressions, but this quickly becomes hard to read. U

  5. Edge Case Handling

    Common Edge Cases Here are the most common edge cases you should always consider: Handling None Values Handling Empty Collections Empty lists, strings, and dicts are falsy in Python, but you should often handle them explicitly: Division by zero and index-out-of-range errors are among the most common runtime crashes in data pipelines. A single guard clause at the start of a function is all it takes to make these errors safe and explicit. Boundary Conditions Pay special attention to boundary value