Prepstellar

Python Fundamentals · Python Runtime and Core Syntax

26 cards

Control Flow and Patterns

Swipe, scroll or use ← →
  1. Pick one path from ordered conditions

    The if statement exists to choose exactly one branch out of several, and its shape is deliberately loose: an if statement can contain zero or more elif clauses, and its else clause is optional.

    >>> x = int(input("Please enter an integer: "))
    Please enter an integer: 42
    >>> if x < 0:
    ...     x = 0
    ...     print('Negative changed to zero')
    ... elif x == 0:
    ...     print('Zero')
    ... elif x == 1:
    ...     print('Single')
    ... else:
    ...     print('More')
    ...
    More
    

    The conditions are tested in order, and the first true one wins. elif is short for "else if" and keeps a long chain from drifting further and further to the right. A chain of if … elif … elif … does the work that a switch or case statement does in other languages.

    1 / 26
  2. Quick check

    Which description of an `if` chain is correct?

    1. AIt may carry any number of `elif` clauses, and the final `else` is optional

      Right. There can be zero or more `elif` parts, and the `else` part is optional, so a bare `if` is a complete statement.

    2. BIt must always finish with an `elif`, even when no alternative path is wanted

      A chain can stop after its first condition; nothing obliges it to end with an alternative clause.

    3. CIt can repeat `else` as often as needed, but it cannot contain any `elif`

      A chain has at most one `else`, and `elif` is precisely the clause used for further ordered conditions.

    2 / 26

  3. Loop over the items, not over a counter

    Python's for statement is not the counter-driven loop of C or Pascal. It iterates over the items of a sequence — a list, a string — in the order in which they appear. There is no start value to set and no halting condition to maintain:

    >>> words = ['cat', 'window', 'defenestrate']
    >>> for w in words:
    ...     print(w, len(w))
    ...
    cat 3
    window 6
    defenestrate 12
    

    The loop asks the sequence for its next item and stops when the supply runs out. That is why a for loop reads like a sentence about the data rather than a sentence about indices.

    3 / 26
  4. Loop over the items, not over a counter

    One habit is worth building early. Code that modifies a collection while iterating over that same collection is tricky to get right, and the two documented ways out are to iterate over a copy or to build a new collection:

    users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}
    
    # Iterate over a copy
    for user, status in users.copy().items():
        if status == 'inactive':
            del users[user]
    
    # Create a new collection
    active_users = {}
    for user, status in users.items():
        if status == 'active':
            active_users[user] = status
    

    Both keep the thing being walked separate from the thing being changed.

    4 / 26
  5. Quick check

    How does a `for` statement move through a list or a string?

    1. AIt builds an arithmetic progression from the length of the sequence

      Building a progression of numbers is the job of `range()`, which is a separate tool from the `for` statement itself.

    2. BIt takes the items of the sequence in the order in which they appear

      Right. A `for` loop iterates over the items of a sequence in their existing order, with no counter to maintain.

    3. CIt re-tests a condition on each pass and never asks for an item

      Re-testing a condition describes a `while` loop; a `for` loop obtains successive items instead.

    5 / 26

  6. Generate numbers with range()

    When you really do need a sequence of numbers, range() generates an arithmetic progression. The rule that governs every one of them: the given end point is never part of the generated sequence.

    >>> for i in range(5):
    ...     print(i)
    ...
    0
    1
    2
    3
    4
    

    range(10) therefore produces ten values — exactly the legal indices of a ten-item sequence. It also accepts an alternative start and a step, and the step may be negative:

    Call Values produced
    range(5, 10) 5, 6, 7, 8, 9
    range(0, 10, 3) 0, 3, 6, 9
    range(-10, -100, -30) -10, -40, -70
    6 / 26
  7. Generate numbers with `range()`

    Printing a range shows something surprising: range(10) displays as range(0, 10), not as a list of numbers. In many ways the object behaves like a list, but it is not one. It returns the successive values when you iterate over it without ever building the corresponding list, which saves space.

    For walking the positions of a sequence, range() can be combined with len():

    >>> a = ['Mary', 'had', 'a', 'little', 'lamb']
    >>> for i in range(len(a)):
    ...     print(i, a[i])
    ...
    0 Mary
    1 had
    2 a
    3 little
    4 lamb
    

    In most such cases, though, enumerate() is the more convenient choice, because it hands you the position and the item together instead of making you index back into the sequence.

    7 / 26
  8. Quick check

    Which values does `range(0, 10, 3)` produce?

    1. A`0, 3, 6, 9, 10`, since the end point closes the progression

      The end point is never part of the generated sequence, so ten cannot appear among the values.

    2. B`3, 6, 9, 12`, since the step also sets the first value

      The progression starts at the start value, which is zero here; the step only sets the increment.

    3. C`0, 3, 6, 9`, since the end point is never generated

      Right. It begins at zero, advances by three, and stops before the excluded end point of ten.

    8 / 26

  9. Keep your progress in the app

    That’s 3 of 10 quick checks. In the app they stay answered, and every lesson remembers where you left off.

  10. Leave a loop early with break

    Some searches are finished before the data is. The break statement breaks out of the innermost enclosing for or while loop — one loop, the nearest one, and nothing beyond it.

    >>> for n in range(2, 10):
    ...     for x in range(2, n):
    ...         if n % x == 0:
    ...             print(f"{n} equals {x} * {n//x}")
    ...             break
    ...
    4 equals 2 * 2
    6 equals 2 * 3
    8 equals 2 * 4
    9 equals 3 * 3
    

    Read what that break did. It ended the inner loop over candidate divisors as soon as one was found, and the outer loop over n carried on to the next number. Choosing which loop a break sits in is therefore how you decide how much work gets abandoned.

    9 / 26
  11. Quick check

    A nested search must stop testing candidates for the current record once it finds a match, yet keep processing later records. Where should the exit go?

    1. A`continue` in the inner candidate loop, which ends the candidate search

      `continue` abandons only the current pass and lets the candidate loop carry on testing, which is not the requirement.

    2. B`pass` in the inner candidate loop, which moves both loops along at once

      `pass` performs no action at all, so the candidate loop would simply run to its end.

    3. C`break` in the inner candidate loop, so the outer record loop keeps running

      Right. `break` exits the innermost enclosing loop, so placing it in the candidate loop ends only that search and leaves the outer loop free to continue.

    10 / 26

  12. Skip the rest of one pass with continue

    Where break abandons the loop, continue continues with the next iteration of the loop. The remainder of the current pass is skipped; the loop itself survives.

    >>> for num in range(2, 10):
    ...     if num % 2 == 0:
    ...         print(f"Found an even number {num}")
    ...         continue
    ...     print(f"Found an odd number {num}")
    ...
    Found an even number 2
    Found an odd number 3
    Found an even number 4
    Found an odd number 5
    

    The two statements are easy to tell apart once you name what each one discards:

    Statement What it abandons What survives
    break The whole loop Everything outside that loop
    continue The rest of the current pass The loop, which advances to its next iteration
    11 / 26
  13. Quick check

    A loop body executes `continue`. What happens next?

    1. AEvery loop currently running is terminated straight away

      Not even one loop is terminated; `continue` never ends a loop, and no statement ends several at once.

    2. BThe loop moves on to its next iteration

      Right. `continue` skips the remainder of the current pass and proceeds with the next iteration.

    3. CThe loop's `else` clause runs immediately

      A loop `else` runs after the loop finishes without a `break`, not in the middle of an iteration.

    12 / 26

  14. The clause that means "we got to the end"

    A for or while loop may carry an else clause, and it runs when the loop finishes without executing break. What "finishing" means depends on the loop:

    Loop Its else runs when
    for The final iteration has completed and no break occurred
    while The condition has become false and no break occurred

    A break skips the else — and so does any other early exit, such as a return or a raised exception.

    13 / 26
  15. The clause that means "we got to the end"

    That makes the loop's else the natural home for a "we looked everywhere" message. The prime search below prints its verdict only for numbers whose inner loop ran out of divisors:

    >>> for n in range(2, 10):
    ...     for x in range(2, n):
    ...         if n % x == 0:
    ...             print(n, 'equals', x, '*', n//x)
    ...             break
    ...     else:
    ...         # loop fell through without finding a factor
    ...         print(n, 'is a prime number')
    ...
    2 is a prime number
    3 is a prime number
    4 equals 2 * 2
    5 is a prime number
    

    Look closely: that else belongs to the for loop, not to the if. It expresses successful exhaustion, not the false branch of a condition — it has more in common with the else of a try statement, which runs when no exception occurred.

    14 / 26
  16. Quick check

    When does an `else` clause attached to a `while` loop run?

    1. AOnly when a `break` has ended the loop ahead of time

      A `break` is precisely what suppresses the `else` clause, along with a `return` or a raised exception.

    2. BAt the end of every single pass whose condition happened to test true

      The clause runs once, after the loop is over, not after each individual pass.

    3. COnce the condition turns false, provided no `break` ended the loop

      Right. For a `while` loop, finishing normally means the condition became false, and no `break` may have intervened.

    15 / 26

  17. Hold a place with pass

    Sometimes the grammar demands a statement and the program has nothing to do there. The pass statement performs no action, and it exists to satisfy a position where a statement is required syntactically.

    That covers three everyday situations: an empty class, a function whose body is still to be written, and a conditional branch left deliberately blank.

    >>> class MyEmptyClass:
    ...     pass
    ...
    >>> def initlog(*args):
    ...     pass  # Remember to implement this!
    ...
    

    pass is silently ignored when it runs, which is what lets you keep sketching a structure at a high level and fill the bodies in later. It is not a loop control statement and it does not return anything.

    16 / 26
  18. Quick check

    Why put `pass` in a function body that has not been written yet?

    1. AIt fills a position where a statement is required, while doing nothing

      Right. `pass` performs no action and exists to satisfy a place where Python's syntax requires a statement.

    2. BIt hands back `True` so the caller can carry on for the moment

      `pass` produces no value at all; a function left this way simply has an empty body.

    3. CIt jumps to the next iteration of whatever loop encloses the call

      Advancing a loop is what `continue` does, and `pass` has no effect on control flow.

    17 / 26

  19. Match a subject against successive patterns

    A match statement takes a subject value and compares it against successive case patterns. Two rules govern the outcome: only the first pattern that matches is executed, and if nothing matches, no branch runs at all.

    def http_error(status):
        match status:
            case 400:
                return "Bad request"
            case 404:
                return "Not found"
            case 418:
                return "I'm a teapot"
            case _:
                return "Something's wrong with the internet"
    

    Note the last block. The standalone name _ is a wildcard pattern that never fails to match, which is how a match statement is given a catch-all ending.

    18 / 26
  20. Match a subject against successive patterns

    Several literal alternatives can share a single case when they are joined with |, read as "or":

            case 401 | 403 | 404:
                return "Not allowed"
    

    One more comparison rule is worth memorising before the patterns get more elaborate: most literals are compared by equality, but the singletons True, False and None are compared by identity.

    19 / 26
  21. Quick check

    A `match` statement is given a subject that no `case` pattern matches, and there is no wildcard case. What runs?

    1. AThe last `case` block, which acts as a fallback when nothing else fits

      A final case is only a fallback when its pattern is the standalone `_` wildcard, which always matches.

    2. BNo branch at all, because a subject that matches nothing selects nothing

      Right. If no case matches, none of the branches is executed.

    3. CEvery `case` block in turn, since the subject is compared against all of them

      Only the first matching pattern is executed; the statement never runs several branches.

    20 / 26

  22. Take the subject apart

    Patterns do more than recognise a value: they can decompose data. A sequence-like pattern can bind selected components to standalone variable names, so recognising and extracting happen in the same line:

    match point:
        case (0, 0):
            print("Origin")
        case (0, y):
            print(f"Y={y}")
        case (x, 0):
            print(f"X={x}")
        case (x, y):
            print(f"X={x}, Y={y}")
        case _:
            raise ValueError("Not a point")
    

    Only standalone names are assigned by a match statement. That is the reason for one small rule with big consequences: a named constant used in a pattern must be written as a dotted name, such as Color.RED, or Python reads the bare name as a capture variable and the case matches everything.

    21 / 26
  23. Take the subject apart

    Mapping patterns capture named values out of a dictionary, and — unlike sequence patterns — extra keys are ignored:

    match config:
        case {"bandwidth": b, "latency": l}:
            print(b, l)
    

    A dictionary carrying additional keys still matches, which is exactly what you want for data whose shape may grow.

    22 / 26
  24. Take the subject apart

    A pattern can also carry an if clause, known as a guard. If the guard is false, matching goes on to try the next case, and the captured values are bound before the guard is evaluated — which is what lets the guard talk about them:

    match point:
        case Point(x, y) if x == y:
            print(f"Y=X at {x}")
        case Point(x, y):
            print(f"Not on the diagonal")
    

    A false guard is not an error and it does not end the statement: it simply declines this case and hands the subject to the next one.

    23 / 26
  25. Quick check

    A routing rule receives a dictionary that may hold extra keys. It must capture `bandwidth` and `latency`, accept the case only when latency is under a threshold, and otherwise fall through to the next case. Which design fits?

    1. AA sequence pattern for the two keys, using `_` as a guard that rejects extra keys

      Sequence patterns do not match dictionaries, and `_` is a wildcard pattern rather than a guard.

    2. BA mapping pattern for the two keys, with an `if` guard on the captured latency

      Right. A mapping pattern captures the requested values and ignores extra keys, and a false guard moves matching on to the next case.

    3. CTwo dotted constant patterns, so the values are bound before any case is chosen

      Dotted names identify named constants to compare against; they capture nothing and cannot express the threshold test.

    24 / 26

  26. Key takeaways

    • if chooses one ordered path: zero or more elif clauses, an optional else, and the first true condition wins.
    • for consumes the items of a sequence in their existing order; to change a collection safely, loop over a copy or build a new one.
    • range() supplies integer progressions with an excluded end point, an optional start and step, and no list is built along the way.
    • break exits the innermost enclosing loop, while continue only advances that loop to its next iteration.
    • A loop's else means exhaustion without break — and a return or a raised exception skips it too.
    • pass supplies a statement without taking action, keeping an empty class, function or branch valid.
    • match tests cases in order, runs at most one, and combines wildcards, alternatives joined with |, captures, guards and mapping patterns.
    25 / 26
  27. Quick check

    A search must report `not found` only after examining every item, and stay silent when a match ends the search early. Which structure does that?

    1. APut the report in the loop's `else` clause and `break` when a match is found

      Right. The loop's `else` runs only when the loop finished without a `break`, so a matching `break` cleanly separates found from not-found.

    2. BPut the report just after the matching `if` and `continue` when a match is found

      `continue` never ends the loop, so the report placed there would run on every pass that found nothing.

    3. CPut the report before the loop starts and `break` once the first item is examined

      A report written before the loop runs before anything has been examined, so it can say nothing about the outcome.

    26 / 26

  28. 10 quick checks · then the test

    In the app, finishing the quick checks opens this lesson’s 10-question test, and the ones you miss come back exactly when you’re about to forget them.

The whole course, on your phone

Lessons you can read, audio you can listen to on the way to work, and practice that remembers what you got wrong.