Skip to main content
Python beginner Lesson 9 of 28

Control Flow in Python

Master if/elif/else, match-case, for and while loops, break/continue, and list comprehensions.

if / elif / else

Conditional statements let your program make decisions based on runtime values. Python uses indentation (4 spaces by convention) to delimit blocks instead of braces, and there are no parentheses required around the condition.

score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"      # this branch runs because 85 >= 80
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)  # B

Inline Conditional (Ternary)

For simple two-branch conditions, Python’s ternary expression keeps the logic on one readable line. Reserve it for simple cases — nesting ternaries hurts readability fast.

# Returns "adult" or "minor" depending on age — no if block needed
status = "adult" if age >= 18 else "minor"

Truthiness Shortcuts

Python evaluates any object in a boolean context, not just explicit True/False. Leveraging this makes conditions more concise and idiomatic — the second form below is considered un-Pythonic.

items = []

# Idiomatic — empty list is falsy
if not items:
    print("Empty")

# Verbose — works but unnecessary
if len(items) == 0:
    print("Empty")

match-case (Python 3.10+)

match-case is structural pattern matching — a significant upgrade over a traditional value-switch. It can destructure sequences, match object attributes, and bind variables in a single step, which eliminates the nested if/elif chains that were previously needed for command parsing or protocol handling.

def handle_command(command):
    match command.split():
        case ["quit"]:
            return "Exiting."
        case ["go", direction]:
            # 'direction' is bound to the second word automatically
            return f"Going {direction}"
        case ["pick", "up", item]:
            return f"Picked up {item}"
        case ["drop", *items]:
            # *items captures all remaining words into a list
            return f"Dropped: {', '.join(items)}"
        case _:
            # _ is the wildcard — matches anything not caught above
            return f"Unknown command: {command}"

handle_command("go north")         # "Going north"
handle_command("pick up sword")    # "Picked up sword"
handle_command("drop key map")     # "Dropped: key, map"

Matching on Types and Attributes

match-case can match against class types and bind specific attribute values in the same case clause — something that would require multiple isinstance calls and attribute accesses with traditional if/elif.

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def classify(point):
    match point:
        case Point(x=0, y=0):
            return "Origin"
        case Point(x=0, y=y):
            return f"Y-axis at {y}"
        case Point(x=x, y=0):
            return f"X-axis at {x}"
        case Point(x=x, y=y):
            return f"Point at ({x}, {y})"

classify(Point(0, 5))   # "Y-axis at 5"

for Loops

Python’s for loop iterates over any iterable — lists, strings, ranges, generators, files, dictionaries, and more. There is no C-style for (i=0; i<n; i++) in Python; if you need an index, use enumerate().

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit)

# range() generates integers on demand — no list is created in memory
for i in range(5):        # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10, 2): # 2, 4, 6, 8  (start, stop, step)
    print(i)

enumerate() and zip()

enumerate() and zip() solve two of the most common looping patterns — getting an index alongside the value, and iterating two sequences together — without manually managing index variables.

# enumerate() — get index and value together; avoids error-prone manual indexing
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry

# zip() — iterate two sequences in lockstep
names = ["Alice", "Bob"]
scores = [95, 87]

for name, score in zip(names, scores):
    print(f"{name}: {score}")

for / else

The else clause on a for loop runs when the loop completes normally — that is, without hitting a break. This is useful for “search and report not found” patterns without needing a sentinel variable.

def find_prime(numbers):
    for n in numbers:
        for i in range(2, int(n**0.5) + 1):
            if n % i == 0:
                break       # composite — inner loop exits early
        else:
            # else runs only when the inner loop finished without break
            return n        # found a prime
    return None

while Loops

while loops repeat as long as their condition is truthy. They’re the right tool when you don’t know in advance how many iterations you need — waiting for user input, reading until EOF, or polling until a condition is met.

count = 0
while count < 5:
    print(count)
    count += 1

# Infinite loop — use break to exit when a condition is met
while True:
    user_input = input("Enter 'quit' to exit: ")
    if user_input == "quit":
        break
    print(f"You said: {user_input}")

Walrus Operator in while

The walrus operator (:=) is particularly useful in while loops — it reads a value, assigns it, and checks it in a single expression, eliminating the boilerplate of reading before the loop and again at the top of each iteration.

import sys

# Read lines until EOF — assign and check in one expression
while line := sys.stdin.readline():
    process(line.strip())

break and continue

break and continue give you fine-grained control over loop execution. They’re most useful when a condition inside the loop body is easier to express than rewriting the loop condition itself.

# break — exits the entire loop immediately
for n in range(10):
    if n == 5:
        break
    print(n)   # 0, 1, 2, 3, 4

# continue — skips the rest of this iteration, moves to the next
for n in range(10):
    if n % 2 == 0:
        continue       # skip even numbers
    print(n)           # 1, 3, 5, 7, 9

Comprehensions

Comprehensions provide a concise syntax for constructing new collections from existing iterables. They’re not just syntactic sugar — Python compiles them to optimized bytecode that’s typically faster than equivalent for loops with .append() calls.

List Comprehension

# Basic transformation — square every number from 0 to 9
squares = [x**2 for x in range(10)]

# With filter — only include even numbers
evens = [x for x in range(20) if x % 2 == 0]

# Nested — flatten a 2D matrix into a 1D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

Dict Comprehension

Dict comprehensions create dictionaries with the same concise syntax, useful for transforming or inverting mappings without verbose loop boilerplate.

words = ["hello", "world", "python"]
# Map each word to its length
word_lengths = {word: len(word) for word in words}
# {"hello": 5, "world": 5, "python": 6}

# Invert a dict — swap keys and values
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
# {1: "a", 2: "b", 3: "c"}

Set Comprehension

Set comprehensions automatically deduplicate results, making them useful when you want a unique collection of transformed values.

numbers = [1, 2, 2, 3, 3, 3, 4]
# {1, 4, 9, 16} — duplicates from the input are collapsed
unique_squares = {x**2 for x in numbers}

Generator Expression

A generator expression looks like a list comprehension but produces values lazily — one at a time, on demand. Use it when you only need to iterate once or when the full collection would use too much memory.

# List comprehension — builds the entire list in memory (1M integers)
total = sum([x**2 for x in range(1_000_000)])

# Generator expression — streams values one at a time, no list created
total = sum(x**2 for x in range(1_000_000))  # no square brackets

Common Patterns

Flat map

sentences = ["hello world", "foo bar baz"]
# Split each sentence into words and flatten into one list
words = [word for sentence in sentences for word in sentence.split()]
# ["hello", "world", "foo", "bar", "baz"]

Conditional expression in comprehension

values = [1, -2, 3, -4, 5]
# Take absolute value of each element inline — no math.fabs needed
abs_values = [x if x >= 0 else -x for x in values]
# [1, 2, 3, 4, 5]

Early exit with next()

next() with a generator expression efficiently finds the first match in a sequence without scanning the whole thing. The second argument is the default if nothing matches.

users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

# Stops at the first match — doesn't scan the rest of the list
alice = next((u for u in users if u["name"] == "Alice"), None)

Frequently Asked Questions

Does Python have a switch statement?
Python 3.10 introduced match-case, which is more powerful than a traditional switch — it supports structural pattern matching, not just value comparison.
What's the difference between break and continue?
break exits the loop entirely. continue skips the rest of the current iteration and jumps to the next one.
When should I use a list comprehension vs a for loop?
Use a comprehension when you're building a new list from an iterable with a simple transformation or filter. Use a for loop when the body has side effects or multiple steps.