Chapter 3 Β· Control Flow & Functions

Making Decisions & Loops

Branch with if, loop over anything with for, count with range(), and learn Python's unusual else clause on loops.

You will learn

  • if / elif / else, truthiness, and the one-line conditional expression.
  • How Python's for loop walks over items, not indexes.
  • How range() works (start, stop, step) and why it is "lazy".
  • break, continue, and the else clause on loops.
  • pass, the statement that does nothing.

if statements

An if statement runs a block only when a condition is true. elif ("else if") checks another condition, and else catches everything left over. Python checks them top to bottom and runs only the first matching block.

β˜• Java / 🟨 JS: Python has no switch. A chain of elif is the usual replacement, and for matching on shapes of data there is match (Chapter 4). There is no ternary ?: either; see the conditional expression below.

Truthiness

A condition does not have to be True or False. Python asks the object "are you true-ish?" These values count as false; everything else is true:

❌ Wordy

βœ… Pythonic

⚠️ Pitfall: unlike JS, the string "0" and the string "False" are true: they are non-empty strings. And [0] is true because the list has one item.

The conditional expression (Python's ternary)

When you just need to pick one of two values, use X if condition else Y. It reads like English.

for statements

Python's for is a for-each loop. It walks over the items of any iterable: anything that can hand out its items one at a time, such as lists, strings and ranges. There is no for (i = 0; i < n; i++) form.

β˜• Java: like for (String w : words). 🟨 JS: like for (const w of words). Python's in is JS's of, not JS's in.

Don't change a collection while you loop over it

Changing a list (or dict) while looping over it confuses the loop: items can be skipped. Loop over a copy, or build a new collection.

❌ Skips items

βœ… Build a new list

(Chapter 6 shows a one-line way to do this: [n for n in nums if n != 2].)

The range() function

When you need a sequence of numbers, use range(). Like slicing, it includes the start and excludes the stop.

CallNumbers produced
range(5)0, 1, 2, 3, 4
range(5, 10)5, 6, 7, 8, 9
range(0, 10, 3)0, 3, 6, 9
range(-10, -100, -30)-10, -40, -70
range(10, 0, -2)10, 8, 6, 4, 2
range(5, 2)(nothing: start is already past the stop)

range(10) does not build a list of ten numbers. It produces each number on demand ("lazily"), so range(1_000_000_000) uses almost no memory. Functions like list() and sum() accept any iterable and pull the numbers out.

Looping with indexes

If you need the index too, you can combine range() and len(), but enumerate() (Chapter 7) is the usual way:

❌ Works, but un-Pythonic

βœ… Pythonic

break and continue

A common pattern is an "infinite" loop that exits with break:

else clauses on loops

This feature surprises everyone coming from Java or JS. A for or while loop can have an else block. It runs when the loop finishes without hitting break.

Think of it as "no break". It is perfect for searches: the else means "I looked everywhere and didn't find it".

Look carefully: the else is lined up with the inner for, not with the if. That is what makes it a loop-else.

How the loop endedDoes else run?
Ran through every itemβœ… yes
Iterable was empty (zero passes)βœ… yes
break❌ no
return or an exception❌ no
continue (any number of times)βœ… yes (continue doesn't end the loop)

❌ Java-style flag

βœ… for … else

πŸ’‘ Tip: for a simple membership test, if "fun" not in categories: is even shorter. Save for…else for searches that also do work along the way.

pass statements

pass does nothing. You use it where Python's grammar needs a statement but you have nothing to write yet, for example an empty function or class while sketching.

You can also write ... (the Ellipsis object) as a placeholder body. Many people use it in stubs.

More examples

1. Categorise expenses

2. First expense over budget

3. Ask until valid (with a stand-in for input())

("-5".isdigit() is False because - is not a digit.)

4. FizzBuzz, the Python way

5. A multiplication table with nested loops

Recap

  • if / elif / else runs the first matching branch. Conditions use truthiness: empty and zero values are false.
  • a if cond else b is Python's ternary.
  • for x in iterable walks over items. Use enumerate() when you need indexes, and don't change a collection while looping over it.
  • range(start, stop, step) excludes the stop and is lazy.
  • break exits a loop, and continue skips to the next pass. A loop's else runs only if no break happened.
  • pass is a do-nothing placeholder.

Exercises

Exercise 1 🟒 easy predict the output

What is list(range(2, 12, 3))?

Show solution

[2, 5, 8, 11]. It starts at 2 and adds 3 each time. 14 would be past the stop (12), and the stop itself is never included.

Exercise 2 🟒 easy fix the bug

This Java-style code has three syntax errors. Fix them.

Show solution

Python uses a colon plus indentation instead of braces, and elif instead of else if.

Exercise 3 🟒 easy predict the output

Which of these print yes? if "0":, if 0:, if []:, if [0]:, if None:, if " ":

Show solution

"0", [0] and " " are true: they are non-empty. 0, [] and None are false.

Exercise 4 🟒 easy build it

Print the even numbers from 10 down to 0 (10, 8, …, 0) using one range() call.

Show solution

The stop must be -1 so that 0 is included, because the stop is excluded.

Exercise 5 🟑 medium predict the output

What does this print?

Show solution

all odd. The loop never hits break, so the loop's else runs.

Exercise 6 🟑 medium refactor it

Rewrite this using a conditional expression on one line.

Show solution

The conditional expression is an expression (it has a value), so it can sit on the right side of =.

Exercise 7 🟑 medium fix the bug

This should remove every negative number, but -2 survives. Why? Fix it.

Show solution

Removing -1 shifts -2 into index 1, but the loop moves on to index 2, so -2 is never checked. Never shrink a list while looping over it.

Exercise 8 🟑 medium build it · playground

Create playground/budget_check.py. Given expenses = [12, 30, 75, 8, 120] and budget = 100, add the expenses one by one and stop as soon as the running total goes over the budget. Print which expense pushed it over. If none did, print Within budget.

Show solution

break stops at the first overflow, and the else handles the "never happened" case without a flag variable.

Exercise 9 πŸ”΄ hard predict the output

How many lines does this print, and what is the last one?

Show solution

Three lines: 1 0, 2 0, 2 1. The last one is 2 1. When i == 0, j = 0 is skipped and j = 1 > 0 breaks out. break only leaves the inner loop, so the outer loop carries on.

Exercise 10 πŸ”΄ hard explain why

Does the else run here? Why?

Show solution

Yes, it prints else ran. The loop body never runs, so break never runs either. The else means "the loop wasn't broken out of", and that includes the empty case.

Exercise 11 πŸ”΄ hard mini-project Β· playground

Create playground/primes.py that prints every prime number below 50 on one line, separated by spaces. Use nested loops, break and a loop else. Bonus: only test divisors up to the square root (x * x <= n).

Show solution

while…else works just like for…else. If n has a factor bigger than √n, it also has one smaller than √n, so checking up to √n is enough.

Official docs