Control flow
Every program you have written so far runs the same way every time: top to bottom, one line at a time. That works for simple scripts, but real programs need to make decisions and repeat work. A quiz needs to check whether the answer is right. A game needs to keep running until the player wins or loses. This chapter covers how to make your program branch and repeat.
Comparisons
Before you can make a decision, you need to compare things. Comparison operators return True or False. The most important one to get right early: = assigns a value, == checks whether two values are equal. Mixing them up is one of the most common beginner mistakes.
5 > 3 # True
5 < 3 # False
5 == 5 # True (note: double equals; = is assignment, == is comparison)
5 != 3 # True ("not equal to")
5 >= 5 # True ("greater than or equal to")
5 <= 4 # False ("less than or equal to")The = vs == distinction trips up almost everyone early on. Assignment (=) stores a value; comparison (==) checks whether two values are the same.
You can compare strings too. Python compares them alphabetically:
"apple" == "apple" # True
"apple" < "banana" # True (a comes before b)
"apple" == "Apple" # False (case-sensitive)Combining conditions
and, or, and not combine comparisons. and requires both sides to be true. or requires at least one side. not flips the result. These let you express real-world conditions like "score is passing AND user is active".
age = 25
score = 88
age >= 18 and score >= 80 # True (both must be true)
age < 18 or score >= 80 # True (at least one must be true)
not age >= 18 # False (flips the result)and requires both sides. or requires at least one side. not inverts.
Truthy and falsy
Every value in Python has a boolean interpretation, even if it is not True or False. Empty strings, zero, empty lists, and None all behave like False in a condition. Everything else behaves like True. This means if results: checks whether a list is non-empty without writing if len(results) > 0:.
# These all behave like False in a condition:
False, 0, 0.0, "", [], {}, (), None
# Everything else behaves like TrueThis means if results: is a natural way to say "if the list is not empty", and if name: checks whether a string has any content.
if / elif / else
The if statement runs a block of code only when its condition is True. elif adds more conditions to check if the first one was false. else catches everything that did not match any condition. Python uses indentation, not braces, to define what belongs inside each block.
score = 87
if score >= 90:
print("A grade")
elif score >= 80:
print("B grade")
elif score >= 70:
print("C grade")
else:
print("Below C")The rules:
ifis required and always comes firstelif(short for "else if") is optional and you can have as many as you needelseis optional, handles everything that did not match, and comes last- Python uses indentation (4 spaces) to mark what belongs inside each block; there are no braces
The indentation is not optional or cosmetic. Python uses it to define structure. Inconsistent indentation is a syntax error.
One-line conditions
For simple yes/no assignments, Python has a compact one-line form called a ternary expression: value_if_true if condition else value_if_false. Use it only when the logic is genuinely simple and reads like a sentence.
label = "pass" if score >= 50 else "fail"This is a ternary expression; it reads like a sentence. Use it when the logic is genuinely simple. For anything involving elif, write the full version.
while loops
A while loop repeats its block as long as its condition is True. Use it when you do not know in advance how many times the loop should run, for example waiting for valid input or retrying until a job succeeds.
lives = 3
while lives > 0:
print(f"Lives remaining: {lives}")
lives -= 1
print("Game over")while is best when you do not know in advance how many times the loop will run. When you do know, or when you are iterating over a collection, for is cleaner.
break and continue
break exits the loop immediately, no matter how many iterations remain. continue skips the rest of the current iteration and jumps back to the condition check. Both only affect the innermost loop they are inside.
break exits the loop immediately:
target = 5
num = 0
while True:
num += 1
if num == target:
print(f"Found {target}")
break # stop the loopwhile True: with a break is a valid and common pattern when the exit condition is complex or needs to happen at the end of the loop body.
continue skips the rest of the current iteration and goes back to the condition check:
num = 0
while num < 10:
num += 1
if num % 2 == 0:
continue # skip even numbers
print(num) # only odd numbers print: 1, 3, 5, 7, 9for loops
A for loop goes through a sequence one item at a time: a list, a string, a range of numbers. The variable you name after for receives each item in turn. You do not manage a counter or check the length yourself.
players = ["Alice", "Bob", "Charlie"]
for player in players:
print(f"Hello, {player}!")for loops also work on strings (iterating character by character) and on any other sequence type.
range()
range() generates a sequence of numbers for you to loop over. range(5) gives you 0, 1, 2, 3, 4. You can control the start, end, and step size. Use it when you need a loop to run a specific number of times.
for i in range(5):
print(i) # 0, 1, 2, 3, 4range() has three forms:
| Call | What it produces |
|---|---|
range(5) | 0, 1, 2, 3, 4 |
range(2, 6) | 2, 3, 4, 5 |
range(0, 10, 2) | 0, 2, 4, 6, 8 (step of 2) |
range(5, 0, -1) | 5, 4, 3, 2, 1 (counting down) |
range() does not create a list. It produces numbers one at a time, which is efficient even for very large ranges.
enumerate()
enumerate() gives you both the index and the value while you loop, so you do not need to track a counter separately. The i, player part automatically receives a pair of values on each iteration.
players = ["Alice", "Bob", "Charlie"]
for i, player in enumerate(players):
print(f"{i + 1}. {player}")
# 1. Alice
# 2. Bob
# 3. CharlieThe i, player syntax is called unpacking. Python splits the (index, value) pair into two names automatically.
By default enumerate() starts at 0. Pass a start value to change that:
for i, player in enumerate(players, start=1):
print(f"{i}. {player}") # starts at 1Nested loops
You can put a loop inside another loop. The inner loop runs fully for each single iteration of the outer loop. This is how you process grids, combinations, or any data with two levels of structure.
rows = [1, 2, 3]
cols = ["A", "B"]
for row in rows:
for col in cols:
print(f"{col}{row}", end=" ")
print() # newline after each row
# A1 B1
# A2 B2
# A3 B3break and continue inside a nested loop only affect the innermost loop.
Loop-else
Python loops can have an else clause that runs only if the loop finished without hitting a break. It is not commonly used, but it is the cleanest way to write "search a list, and if nothing was found, do this".
target = "Dave"
names = ["Alice", "Bob", "Charlie"]
for name in names:
if name == target:
print(f"Found {target}")
break
else:
print(f"{target} not in list") # runs because break never firedIf break runs, the else is skipped. If the loop exhausts the sequence, else runs. It is a niche pattern but cleaner than a flag variable.
Sorting
sorted() returns a new sorted list and leaves the original unchanged. .sort() sorts the list in place and returns None. The key= argument lets you sort by something other than the raw value. For example, sorting names case-insensitively or sorting player tuples by their score.
scores = [87, 42, 96, 55, 71]
ranked = sorted(scores) # [42, 55, 71, 87, 96] (new list)
scores.sort() # sorts the original list, returns None
scores.sort(reverse=True) # [96, 87, 71, 55, 42]Both accept a key= argument: a function applied to each item before comparison:
names = ["Charlie", "Alice", "Bob"]
sorted(names, key=str.lower) # case-insensitive sort
players = [("Alice", 87), ("Bob", 96), ("Charlie", 55)]
sorted(players, key=lambda p: p[1]) # sort by scoreWhat's a lambda?
lambda p: p[1] is a one-line function. It takes a player tuple and returns the score. Lambda functions are covered in the Lambda, comprehensions, and zip chapter.
For simple cases, use sorted(). For lists where you want to modify in place, use .sort().
In practice
Loop through scores, accumulate a total, count passing grades, and print a summary:
raw_scores = [87, 42, 96, 55, 71, 63]
total = 0
passing = 0
for score in raw_scores:
total += score
if score >= 60:
passing += 1
average = total / len(raw_scores)
print(f"Average: {average:.1f}")
print(f"Passing: {passing}/{len(raw_scores)}")
print(f"Top score: {sorted(raw_scores, reverse=True)[0]}")
