◀ Course contents Part 2 · Module 2-02

for Loops

Say it once, do it to everything

Every program so far has been as long as the work it did. Three readings meant three lines. A for loop breaks that link: you describe the work once and Python applies it to every item, whether there are three of them or three hundred thousand. This is the single largest jump in what you can write.

Ready?

1

A for Loop Walks Through a Sequence

A for loop takes something with items in it and runs the same block once per item, handing you the current one under a name you choose.

for tag in ["loops", "lists", "functions"]:
    print(tag)

# loops
# lists
# functions

tag is the loop variable. It is not special and it is not declared anywhere — Python creates it, points it at the first item, runs the block, points it at the second, and so on until the sequence is exhausted.

The colon and the indented block work exactly as they did with if. Strings are sequences too, so a loop over a string walks it one character at a time.

for letter in "PY26":
    print(letter)

Name the item, not the loop

for i in tags: tells the reader nothing. for tag in tags: tells them what the block is about, and makes every line inside it read as a sentence. The singular of the collection's name is nearly always the right choice.

Quick check

A loop runs over a string of five characters. How many times does its block run?

2

range() Generates Numbers to Count With

When there is nothing to walk over — you just need to do something five times — range() produces the numbers.

range(5)          # 0 1 2 3 4
range(1, 6)       # 1 2 3 4 5
range(0, 20, 5)   # 0 5 10 15
range(3, 0, -1)   # 3 2 1

Two things to hold on to. It starts at 0 when you do not say otherwise, and it stops before the number you give it. Both are deliberate: range(5) gives five numbers, and range(a, b) followed by range(b, c) covers everything from a to c with nothing repeated and nothing missed.

That exclusive end is also the source of most off-by-one bugs, and there is no trick for it beyond checking: if you want 1 to 10 inclusive, you write range(1, 11).

Works, reads badly

for i in range(len(tags)):
    print(tags[i])

Counting to an index in order to look the item up. Two steps where one will do.

Says what it means

for tag in tags:
    print(tag)

Ask for the items when you want the items. Use range only when you actually want the numbers.

Quick check

Which call produces the numbers 1 through 10 inclusive?

3

The Accumulator: Building an Answer Up

Most loops are not there to print. They are there to build one answer out of many items, and they nearly all have the same three-part shape:

total = 0                 # 1. start it outside the loop
for price in prices:
    total = total + price # 2. update it inside
print(total)              # 3. use it after

Step 1 is where the bugs live. Put total = 0 inside the loop and it resets on every pass, so the answer is always just the last item. That mistake produces a plausible-looking number, which is what makes it worth recognising on sight.

The same shape counts things (count += 1 when a condition holds), finds the largest (if price > biggest:), and builds text. Change what step 2 does and you change what the loop is for.

When you need the position as well as the item, enumerate() gives you both, and starts at 0 unless you say otherwise:

for number, tag in enumerate(tags, start=1):
    print(number, tag)

Total

total += value

Count

count += 1, inside an if

Largest

compare, then replace when bigger

Position too

enumerate(items, start=1)

Quick check

total = 0 is written inside the loop body rather than above it. What does the loop print at the end?

4

Leaving Early, and Skipping a Pass

Two keywords change how a loop runs.

break abandons the loop entirely. The classic use is a search: once you have found what you were looking for, there is no reason to keep going.

for check in checks:
    if check == "FAIL":
        first_failure = check
        break

continue skips the rest of this pass and goes straight to the next item. It is how you filter out the rows you do not care about without wrapping the whole body in an if:

for line in lines:
    if not line.strip():
        continue          # blank line, nothing to do
    process(line)

Both are worth using sparingly and both are worth using. A loop with three breaks in it is hard to follow; a loop that keeps scanning a million rows after it has its answer is just slow.

Nothing here can hang

A for loop over a finite sequence always ends — that is the difference between it and the while loop in the next module. If you do manage to write something that runs forever, the lab stops it after ten seconds and hands you the editor back.

Reach for for when you know what you are walking over, and while when you only know when to stop.

Quick check

What is the difference between break and continue?

0 of 9 completed

Real Python runs right here in your browser — nothing to install, nothing sent to a server. The interpreter downloads once the first time you press Run, then stays cached.

01

To do

A for loop runs its block once for every item in a sequence, handing you the current item under a name you pick.

for letter in "AB":
    print(letter)

A string is a sequence of characters, so this prints A and then B. The colon and the indented block work exactly as they did with if.

Your task: print each character of code on its own line:

P
Y
2
6
your_code.py
Python
Hint

for letter in code: on one line, then print(letter) indented underneath. You never say how many characters there are — the loop works that out.

Output

      
    02

    To do

    split() hands back a list, and a list is a sequence, so a loop walks it the same way it walks a string.

    The work inside the block is whatever you would have done to one item — the loop is what makes it happen to all of them.

    Your task: the tags below arrive with inconsistent spacing. Split them on the comma, then print each one stripped and title-cased, one per line:

    Loops
    Lists
    Functions
    your_code.py
    Python
    Hint

    for tag in tags: then print(tag.strip().title()) inside. Both methods chain onto the item, exactly as they would if there were only one.

    Output
    
          
      03

      To do

      range() produces numbers. It starts at 0 unless told otherwise, and it always stops before the number you give it.

      range(5)        # 0 1 2 3 4
      range(1, 6) # 1 2 3 4 5
      range(3, 0, -1) # 3 2 1

      The exclusive end is deliberate — range(5) gives you five numbers — and it is also where nearly every off-by-one bug comes from.

      Your task: two loops. The first prints 1 to 5 inclusive, one per line. The second counts back down from 3 to 1. Eight lines altogether:

      1
      2
      3
      4
      5
      3
      2
      1
      your_code.py
      Python
      Hint

      range(1, 6) for the first — the end is exclusive, so 6 gets you 5. For the countdown, range(3, 0, -1): start at 3, stop before 0, step backwards by one.

      Output
      
            
        04

        To do

        The most useful loop does not print. It builds an answer, and it always has the same three parts:

        total = 0              # start it OUTSIDE the loop
        for price in prices:
            total += price # update it inside
        print(total) # use it after

        Your task: four line items arrive as one comma-separated string. Total them into total, then print the total and the average to two decimal places:

        Total: 3040
        Average: 760.00

        Remember that split() hands back text. Each piece needs converting before it can be added to anything.

        your_code.py
        Python
        Hint

        for amount in amounts: then total += int(amount) inside. The int() matters — adding the text "1250" to a number raises a TypeError.

        Output
        
              
          05

          To do

          Change what the accumulator does and the same shape counts instead of totalling: start at zero, add one whenever a condition holds, read it afterwards.

          count = 0
          for line in lines:
              if "ERROR" in line:
                  count += 1

          splitlines() cuts a multi-line string into a list of lines, which is how a block of log text becomes something a loop can walk.

          Your task: count how many of the five log lines are errors, and print:

          Errors: 2 of 5
          your_code.py
          Python
          Hint

          for line in lines: then an if inside it — "ERROR" in line is True when the text appears anywhere in that line. Increment with errors += 1.

          Output
          
                
            06

            To do

            Sometimes the block needs to know where it is, not just what it has. enumerate() hands back both, as two names on the for line:

            for number, tag in enumerate(tags, start=1):
                print(number, tag)

            Without start=1 the numbering begins at 0, which is right for indexes and wrong for anything a person reads.

            This is what replaces for i in range(len(tags)) — that version counts to a number in order to look the item up, which is two steps where one will do.

            Your task: print a numbered menu:

            1. loops
            2. lists
            3. functions
            your_code.py
            Python
            Hint

            for number, tag in enumerate(tags, start=1): then print(f"{number}. {tag}") inside — note the full stop and the space in the output.

            Output
            
                  
              07

              To do

              break abandons the loop immediately. The classic use is a search: once the thing is found, every remaining pass is wasted work.

              for check in checks:
                  if check == "FAIL":
                      break

              Your task: find the position of the first failing check, counting from 1, and stop looking as soon as you have it. There are two failures in the list; only the first one matters.

              First failure at check 3
              your_code.py
              Python
              Hint

              Loop with enumerate(checks, start=1) so you have the position, set it when the check is "FAIL", then break out of the loop.

              Output
              
                    
                08

                To do

                continue abandons the current pass and moves to the next item. It is how you drop the rows that are not your problem without wrapping the whole body in an if:

                for line in lines:
                    if not line.strip():
                        continue
                    print(line)

                With one filter the two shapes are equivalent. With three, the continue version stays flat while the nested version marches off the right of the screen.

                Your task: print only the real settings from this config, skipping blank lines and anything starting with #. Strip each line before printing it:

                port=8080
                debug=false
                timeout=30
                your_code.py
                Python
                Hint

                Strip the line into a variable first. Then: if not line: continue, and if line.startswith("#"): continue. Whatever survives both is a setting.

                Output
                
                      
                  09

                  To do

                  This loop reports a total of 79 for three scores that add up to 259. It raises no error, and the number it prints is a perfectly believable score.

                  The accumulator is being created inside the loop, so every pass throws away everything before it. What survives to the print is the last item alone.

                  Your task: move one line, and print the real total:

                  259
                  your_code.py
                  Python
                  Hint

                  total = 0 belongs above the loop, not inside it. Start it once, add to it many times.

                  Output
                  
                        

                    The log summariser

                    To do

                    A service wrote this log overnight and somebody has to say what happened in it before standup. Walk it once and produce the summary.

                    Each real line is a date, a level and a message, padded so the levels line up on screen. Blank lines and lines starting with # are not log entries and must be skipped entirely — they count towards nothing.

                    Work these out:

                    • total_lines — how many real entries there are
                    • infos, warnings, errors — how many of each level
                    • first_error — the message of the first ERROR entry, and nothing after it should change this
                    • error_rate — errors as a percentage of the real entries

                    Then print exactly six lines:

                    Lines: 6
                    INFO: 3
                    WARN: 1
                    ERROR: 2
                    First error: db timeout after 30s
                    Error rate: 33.3%
                    your_code.py
                    Python
                    Hint

                    Loop over log.splitlines(). Strip each line, then continue past the blanks and the # lines. What is left splits with maxsplit=2 into date, level and message. Count with += 1 inside an if/elif chain, and take the first error only when first_error is still empty — or set it and break out of a second, separate search. The rate is errors / total_lines * 100, shown with {error_rate:.1f}.

                    Output
                    
                          

                      Notification