Skip to content
Unit 7 of 28

Control flow

Contents · 21

A program runs from top to bottom unless something tells it otherwise. Control flow statements are that something: they choose which block runs (if, match), repeat a block (while, for), and leave or skip part of a loop (break, continue). Almost every program you write from now on uses them.

This unit also covers the loop helpers that make Python loops short and safe (range, enumerate, zip, dict.items()), structural pattern matching with match, and two classic traps: leaving nested loops, and changing a list while you loop over it.

You will be able to:

  • Write if/elif/else chains that test truthiness directly instead of comparing with True
  • Use match/case with literal, capture, sequence, mapping and class patterns, guards and _
  • Choose between while and for, and loop with range, enumerate, zip and dict.items()
  • Control loops with break, continue, pass and the loop else clause
  • Leave nested loops cleanly, and avoid changing a list while you iterate over it
  • Write an input loop that keeps asking until the answer is valid

Blocks and indentation

A compound statement such as if, for or while has a header line ending in a colon, followed by an indented block of statements. The indentation is the only thing that marks where the block ends, so it must be consistent: PEP 8 asks for four spaces per level, and mixing tabs and spaces raises TabError.

Indentation defines blocks, not scopes. A name assigned inside an if or a loop is an ordinary variable of the surrounding code and still exists afterwards. if, for, while and match never create a new scope; functions do (Functions explains the lookup rules).

if, elif and else

Python checks each condition in turn, runs the block of the first one that is true and skips the rest. Both elif and else are optional, and there can be any number of elif branches.

The diagram traces the example below with x = 3: only the first true branch runs, and else catches everything else.

Rendering diagram…

Truthiness in conditions

A condition doesn't have to be a bool. Python uses the value's truthiness (Values, names and types), so test containers and strings directly:

Write if basket: rather than if len(basket) > 0:, and never if flag == True:. When 0 or "" is a valid value, as discount is here, test is None / is not None instead of truthiness. For choosing between two values, use the conditional expression a if condition else b from Operators and expressions.

Pitfall. if colour == "red" or "blue": is always true. It means (colour == "red") or "blue", and the nonempty string "blue" is truthy. Write if colour in ("red", "blue"):.

Pattern matching with match

Since Python 3.10, match compares one value, the subject, against a series of case patterns from top to bottom. The block of the first pattern that matches runs, and then the statement ends: there is no fall through to the next case, and if nothing matches, nothing runs. Patterns can do much more than compare values; they can also take a value apart and bind its pieces to names.

PatternExampleMatches
literalcase 404:an equal value (None, True, False by identity)
alternativescase 200 | 201:any of the alternatives
wildcardcase _:anything, and binds nothing
capturecase code:anything, and binds it to code
valuecase HTTPStatus.OK:a value equal to that dotted constant
sequencecase [x, *rest]:a list, tuple or other sequence of that shape (not a string)
mappingcase {"type": kind}:a dict or other mapping with those keys (extra keys are fine)
classcase int(n):, case Point(x=0):an instance of the class with matching attributes
guardcase [x, y] if x == y:the pattern matches and the condition is true
ascase [_, _] as pair:the pattern, binding the whole value to pair

The examples put each match inside a small function so that you can try several values with a loop.

Literals, constants, captures and _

A bare name in a pattern is a capture pattern: it matches anything and assigns the value to that name. It never compares against an existing variable. To compare against a named constant, use a dotted name such as HTTPStatus.NOT_FOUND, which Python treats as a value.

Pitfall. With NOT_FOUND = 404 and case NOT_FOUND:, the case doesn't compare with 404; it captures every value into a new variable called NOT_FOUND. When later cases follow it, Python refuses to compile the statement: SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable.

Sequence patterns

A sequence pattern matches a list or tuple of a given shape. *name collects any remaining items into a list. A common use is matching a command split into words:

Mapping patterns

A mapping pattern checks that a dict has certain keys, optionally with certain values, and captures the values you name. Extra keys are ignored, which suits JSON like data.

Class patterns and guards

A class pattern such as int() or str(text) checks the type with isinstance, and for built in types like these str(text) binds the value itself. A guard, if condition after the pattern, adds a test that must also pass.

Order matters: bool is a subclass of int, so case int(): would also match True. Put the more specific case first.

Class patterns also work with your own classes, matching attributes by name, as in case Point(x=0, y=y):. Classes and objects shows how to define such a class. Its section Matching objects with match has a runnable example, and explains what a positional pattern such as case Point(0, y): needs.

Tip. match is worth it when you are checking the shape of data: commands, messages, parsed JSON. For a plain "which of these values?" choice, if/elif or a dict lookup is often as clear.

while loops

A while loop repeats its block as long as its condition is true. The condition is tested before each pass, so the body may run zero times.

Something in the body must eventually make the condition false. If a loop runs forever by mistake, press Ctrl+C to stop it.

while True with break

Python has no do ... while loop. When the exit test belongs in the middle or at the end of the body, write while True: and leave with break. The body always runs at least once:

The random generator is seeded so that the output is repeatable; A tour of the standard library covers random.

for loops

A for loop takes the items of an iterable one at a time: a list, tuple, string, dict, set, file, range and many more. You don't manage an index or a stop condition; the loop ends when the items run out. Iterators and generators shows how this works underneath.

range(start, stop, step)

stop is never included, start defaults to 0, and step may be negative. A range doesn't build a list; it calculates each number when asked, so range(10**12) costs no memory. The list() calls above are only there to display the numbers.

enumerate, zip and dicts

To get positions as well as items, use enumerate rather than range(len(...)). To walk two sequences side by side, use zip. To loop over a dict's keys and values together, use .items(); looping over the dict itself gives only the keys, in insertion order.

zip stops silently at the end of the shortest input, which can hide a bug where two lists should have the same length. Since Python 3.10, zip(..., strict=True) raises an error instead:

This prints the first two pairs, then stops with ValueError: zip() argument 2 is shorter than argument 1. Use .values() when you need only the values, sorted(prices) for keys in sorted order, and reversed(colours) to walk a sequence backwards.

break, continue, else and pass

  • break leaves the innermost loop immediately.
  • continue skips the rest of the current pass and goes on to the next item.
  • A loop's else block runs when the loop ends without break: the iterable ran out, or the while condition became false. Read it as "no break".
  • pass does nothing. Use it where the syntax needs a statement but you have nothing to do yet. It does not skip anything; that is continue's job.

A while loop works the same way, except that it retests its condition instead of fetching the next item. Its else runs when the condition becomes false, and is skipped when break ends the loop:

Rendering diagram…

In the block below, the first loop uses continue to skip missing readings. The second shows where else shines: a search that uses break when it finds something and lets else handle "not found" without a flag variable.

Leaving nested loops

break leaves only the innermost loop, and Python has no labelled break. There are two common ways out of several loops at once: a flag that the outer loop checks, or putting the loops in a function and using return.

The function version is usually clearer: return leaves every loop at once, and the function gets a name that says what it does. A third option is itertools.product, which produces every pair for a single loop, so that one break is enough. Iterators and generators introduces it.

Changing a list while you loop over it

Removing items from a list while a for loop walks over it makes the loop skip items:

The loop keeps an internal position. Removing 2 shifts every later item one place left, so 4 moves into the slot that was already visited and is never checked. The fixes build a new list, or loop over a copy while changing the original. The new list is made by a list comprehension, covered in Collections and comprehensions; numbers[:] is a copy made by slicing. Dicts and sets are stricter than lists: adding or removing entries during a loop raises RuntimeError (dictionary changed size during iteration for a dict). Loop over list(the_dict) if you need to delete keys. Changing the values of existing keys is fine.

An input validation loop

input() always returns a string, and int() raises ValueError when the string isn't a whole number. A while True loop can keep asking until the answer is valid. The try/except statement catches the error; Errors and exceptions covers it in detail.

The answers abc, 42 and 7 were fed in automatically, so they don't appear in the output; when you type them yourself, each shows after its prompt.

Check yourself

  1. After if ready: status = "go" runs with ready true, does status exist outside the block?
  2. Why is if answer == "y" or "Y": always true, and how do you fix it?
  3. In a match, what is the difference between case limit: and case Limits.MAX:?
  4. In classify() above, what would True match if case int() | float(): came before case bool():?
  5. When does the else block of a for loop run?
  6. What does strict=True add to zip()?
  7. What does list(range(10, 0, -4)) give?
  8. Why does removing items from a list inside for item in that_list skip some of them?

Suggested answers

  1. Yes. Indentation creates blocks, not scopes, so status is an ordinary variable of the surrounding code.
  2. It means (answer == "y") or "Y", and "Y" is truthy. Write if answer in ("y", "Y"):, or if answer.lower() == "y":.
  3. case limit: is a capture pattern: it matches anything and binds it to limit. case Limits.MAX: is a value pattern: it matches only a value equal to Limits.MAX.
  4. case int() | float():, because bool is a subclass of int, and the first matching case wins.
  5. When the loop finishes without break, because the items ran out. It also runs if there were no items at all.
  6. It raises ValueError when the inputs have different lengths, instead of silently stopping at the shortest.
  7. [10, 6, 2]. The stop value 0 is never reached or included.
  8. Removing an item shifts the later items left, but the loop's position still moves forward, so the item that moved into the current slot is never visited.

Exercises

These exercises are optional practice. Pick the ones that help you.

  1. Sevens but not fives. Write sevens(start, stop) that returns every number from start to stop inclusive that is divisible by 7 but not by 5. Print the result for 1 to 100 on one line, separated by commas. Expected: 7,14,21,28,42,49,56,63,77,84,91,98.
  2. Factorial. Read a whole number n with input() and print n! (1 × 2 × … × n), computed with a loop. 0! is 1; for a negative n, raise ValueError. Input 8 → 40320.
  3. List and tuple from a line. Read one line of comma separated whole numbers, possibly with spaces around them, and print them as a list of ints and as a tuple. Input 3, 14,15 ,92,65 → [3, 14, 15, 92, 65] and (3, 14, 15, 92, 65). (line.split(",") cuts a string at each comma, and int() ignores surrounding spaces; Strings and text formatting covers both.)
  4. Shout until blank. Read lines until an empty line, and print each one in capitals (line.upper()). Input hello world, practice makes progress, then an empty line → HELLO WORLD and PRACTICE MAKES PROGRESS. Anything after the empty line is not read.
  5. FizzBuzz with match. Write fizzbuzz(n) that returns "FizzBuzz" if n is divisible by both 3 and 5, "Fizz" if only by 3, "Buzz" if only by 5, and otherwise the number as a string. Use match (n % 3, n % 5):. Print the results for 1 to 15 on one line: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz.

Solutions

1. Sevens but not fives. print(*items, sep=",") passes each item as a separate argument and puts commas between them.

2. Factorial. The loop starts at 2 because multiplying by 1 changes nothing; for n of 0 or 1 it doesn't run at all and the result stays 1.

3. List and tuple from a line.

4. Shout until blank. An empty string is falsy, so if not line detects the blank line. If the input might end without one, catch EOFError around input() as well.

5. FizzBuzz with match. Matching on a tuple of both remainders puts the whole decision in one place; _ means "any value" in that position.