In the previous lesson, Papyrus learned to decide: if there's stock, sell; if the customer is a member, apply the discount. But we closed with a problem still open: Ana has a whole catalog, and writing an if for every book doesn't scale. The solution is loops, structures that repeat a block of code as many times as needed. In this lesson you'll master Python's two loops: for, to walk through collections and sequences of numbers, and while, to repeat as long as a condition holds. With them we'll go through the entire Papyrus catalog and add up the day's takings in just a few lines.
Contents
- The
forloop and therange()function range()with start, stop and step- Looping over strings and simple lists with
for - The
whileloop: repeat while a condition holds - Counters and accumulators: the daily takings
- Nested loops
enumerate(): index and value at the same timefororwhile? When to use each one
The for loop and the range() function
A for loop repeats a block of code once for each element of a sequence. Its most basic form uses range(), a function that generates a sequence of integers:
Output:
Let's break the syntax down piece by piece:
forandinare reserved words: the line reads "for eachnumberinrange(5)".numberis the loop variable: on each pass (each iteration) it takes the next value in the sequence. You can call it whatever you like, following snake_case.range(5)generates the numbers0, 1, 2, 3, 4. Yes: it starts at 0 and ends just before 5. It generates exactly 5 numbers, but 5 itself isn't included. This convention (start included, stop excluded) is universal in Python and worth internalizing as soon as possible.- The colon and the indented block work just like in an
if: everything indented is repeated; whatever comes after, unindented, runs once when the loop finishes.
An immediate use at Papyrus: printing the receipt's separator line without copy-pasting.
STORE_NAME = "Papyrus"
print(f"=== {STORE_NAME} ===")
for _ in range(3):
print("- Thank you for your purchase -")When the loop variable's value isn't used (we just want to repeat 3 times), the Python convention is to call it _ (underscore). It's a signal to the reader: "this value doesn't matter".
range() with start, stop and step
range() accepts up to three arguments, just like the sequence slicing you'll see later on:
| Call | Generates | Meaning |
|---|---|---|
range(5) |
0, 1, 2, 3, 4 | Only stop: from 0 to stop−1 |
range(1, 6) |
1, 2, 3, 4, 5 | start and stop: from start to stop−1 |
range(0, 10, 2) |
0, 2, 4, 6, 8 | start, stop and step: jumping step by step |
range(10, 0, -1) |
10, 9, ..., 1 | Negative step: counts backwards |
An example with all three forms, numbering things around the bookshop:
# Number Papyrus's shelves, from 1 to 6
for shelf in range(1, 7):
print(f"Checking shelf {shelf}")
# The even days of the month, for orders to the distributor
for day in range(2, 31, 2):
print(f"Order scheduled for day {day}")
# Countdown to opening
for seconds in range(3, 0, -1):
print(f"Opening in {seconds}...")
print("Papyrus opens its doors!")Details to watch:
range(1, 7)goes up to 6, not 7 (stop excluded, always).- With a negative step, start must be greater than stop, or the range will be empty and the loop won't run a single pass (no error: simply nothing happens).
range(0)orrange(5, 5)are also valid empty ranges.
Looping over strings and simple lists with for
The real power of for in Python is that it doesn't just iterate over numbers: it iterates over any sequence of elements (what we call an iterable). Two cases we can use right away:
Looping over a string
A string is a sequence of characters, so it can be traversed letter by letter:
Output:
On each iteration, letter holds one character of the string, in order. (Remember print()'s end parameter from module 1: here it replaces the newline with a space.)
Looping over a simple list
To walk through the Papyrus catalog we need to group the titles into a single variable. For that, Python has lists: an ordered collection of values, written between square brackets and separated by commas. We'll study them in depth in module 4; for now it's enough to know how to create and traverse them:
Output:
Read that loop out loud: "for each title in catalog, print it". Python's for is designed to sound like natural language. On each pass, title points to the next element of the list, from first to last.
And of course, inside the loop we can use everything from the previous lesson. Let's combine two parallel lists (same position = same book) with a conditional:
catalog = ["The Odyssey", "Hamlet", "Don Quixote"]
stocks = [4, 6, 8]
for i in range(3):
title = catalog[i]
stock = stocks[i]
availability = "available" if stock > 0 else "OUT OF STOCK"
print(f"{title:<12} | {availability}")Here catalog[i] accesses the element at position i of the list (positions also start at 0, just like range). This "manual index" pattern works, but we'll soon see a more elegant way with enumerate().
The while loop: repeat while a condition holds
The for loop is ideal when you know what to traverse or how many times to repeat. But sometimes you don't know in advance: "repeat while there's stock left", "keep asking until the user types something valid". That's what while is for:
The flow is: evaluate the condition → if it's True, run the block and evaluate again → if it's False, exit the loop.
flowchart TD
A[Start] --> B{condition?}
B -- True --> C[Run block]
C --> B
B -- False --> D[Continue after the loop]
An example at Papyrus: simulating sales of The Odyssey until the stock runs out.
title = "The Odyssey"
stock = 4
while stock > 0:
print(f"Sold a copy of '{title}'. {stock - 1} left.")
stock = stock - 1
print(f"'{title}' out of stock. Ana needs to restock.")Output:
Sold a copy of 'The Odyssey'. 3 left. Sold a copy of 'The Odyssey'. 2 left. Sold a copy of 'The Odyssey'. 1 left. Sold a copy of 'The Odyssey'. 0 left. 'The Odyssey' out of stock. Ana needs to restock.
The line stock = stock - 1 is crucial: something inside the loop must push the condition towards False. If you forget it, the stock never drops, the condition is always true and you have an infinite loop: the program never ends (if it happens to you, kill the run with Ctrl+C in the terminal). It's the classic while mistake.
By the way, stock = stock - 1 is so common that Python offers a shorthand: stock -= 1 ("subtract 1 from stock"). The equivalents +=, *=, /=, etc. also exist. We'll use them from now on.
Another typical use of while: repeating a question until you get a valid answer, one of the pending improvements to book_card.py:
answer = ""
while answer != "y" and answer != "n":
answer = input("Is the customer a member? (y/n): ")
is_member = answer == "y"
print(f"Member: {is_member}")As long as the user doesn't type exactly y or n, the question repeats. Note the trick of initializing answer = "" before the loop so that the first evaluation of the condition is possible (and gives True).
Counters and accumulators: the daily takings
Two patterns show up again and again with loops, and they deserve names of their own:
- Counter: a variable that starts at
0and adds1every time something happens. It answers "how many times...?". - Accumulator: a variable that starts at
0(or at""for text) and keeps adding amounts. It answers "how much in total...?".
The perfect example is Papyrus's daily takings: at closing time, Ana wants to know how much came in and how many sales she made.
STORE_NAME = "Papyrus"
daily_sales = [12.50, 9.95, 15.90, 12.50, 9.95] # amounts taken today
till_total = 0.0 # accumulator: sum of amounts
num_sales = 0 # counter: number of transactions
for amount in daily_sales:
till_total += amount
num_sales += 1
print(f"Sale {num_sales}: {amount:.2f} EUR (running total: {till_total:.2f} EUR)")
print("-" * 40)
print(f"{STORE_NAME} — till closing")
print(f"Sales made: {num_sales}")
print(f"Day's total: {till_total:.2f} EUR")
print(f"Average receipt: {till_total / num_sales:.2f} EUR")Output:
Sale 1: 12.50 EUR (running total: 12.50 EUR) Sale 2: 9.95 EUR (running total: 22.45 EUR) Sale 3: 15.90 EUR (running total: 38.35 EUR) Sale 4: 12.50 EUR (running total: 50.85 EUR) Sale 5: 9.95 EUR (running total: 60.80 EUR) ---------------------------------------- Papyrus — till closing Sales made: 5 Day's total: 60.80 EUR Average receipt: 60.80 EUR...
(The real average receipt is 12.16 EUR; check it when you run the code.)
The anatomy of the pattern, which you'll repeat hundreds of times:
- Initialize the counter/accumulator before the loop (if you initialize it inside, it would reset to zero on every pass and never accumulate anything).
- Update inside the loop (
+=). - Use the result after the loop.
Counters also combine with conditionals: counting only what meets some criterion.
stocks = [4, 6, 8, 0, 2]
titles_below_minimum = 0
for stock in stocks:
if stock <= 3:
titles_below_minimum += 1
print(f"Titles that need restocking: {titles_below_minimum}") # 2Nested loops
Just like ifs, loops can be nested: one loop inside another. The inner loop runs in full for each pass of the outer one.
Ana wants labels for every physical copy: one label per unit in stock of each title.
catalog = ["The Odyssey", "Hamlet", "Don Quixote"]
stocks = [3, 2, 2] # reduced stock to keep the output short
for i in range(3): # outer loop: each title
title = catalog[i]
for copy in range(1, stocks[i] + 1): # inner loop: each copy
print(f"Label: {title} — copy {copy}/{stocks[i]}")Output:
Label: The Odyssey — copy 1/3 Label: The Odyssey — copy 2/3 Label: The Odyssey — copy 3/3 Label: Hamlet — copy 1/2 Label: Hamlet — copy 2/2 Label: Don Quixote — copy 1/2 Label: Don Quixote — copy 2/2
Count the iterations: the outer loop makes 3 passes and the inner one 3+2+2 = 7 in total. With nested loops the number of executions multiplies quickly: two loops of 1000 passes each mean a million executions of the inner block. Keep that in mind when you work with large data.
A visual classic to cement the idea, the multiplication table:
for row in range(1, 4):
for column in range(1, 4):
print(f"{row * column:>3}", end="")
print() # newline at the end of each rowOutput:
Notice the empty print(): it's indented at the level of the outer loop, so it runs once per row, not once per cell. With nested loops, each line's indentation level determines "which pass it belongs to" — always double-check it.
enumerate(): index and value at the same time
Let's go back to the manual index pattern we used with the parallel lists. When you traverse a list and also need each element's position, Python offers something better than range() + square brackets: the enumerate() function, which on each pass hands you the index-value pair.
catalog = ["The Odyssey", "Hamlet", "Don Quixote"]
for position, title in enumerate(catalog, start=1):
print(f"{position}. {title}")Output:
How it works:
enumerate(catalog)generates pairs:(0, "The Odyssey"),(1, "Hamlet"),(2, "Don Quixote").- In the
forwe write two variables separated by a comma (position, title), and Python unpacks each pair into them — it's the same multiple assignment you saw in module 1. - The optional argument
start=1makes the numbering begin at 1, perfect for customer-facing listings (by default it starts at 0).
Compare the two versions of the same numbered listing:
# With a manual index: works, but more noise
for i in range(len(catalog)):
print(f"{i + 1}. {catalog[i]}")
# With enumerate: clearer and more Pythonic
for position, title in enumerate(catalog, start=1):
print(f"{position}. {title}")(len() returns the length of a sequence: len(catalog) is 3. We'll use it constantly.) Practical rule: if you only need the values, for x in some_list; if you also need the position, enumerate(); save range(len(...)) for very specific cases, such as traversing two parallel lists by index.
for or while? When to use each one
| Question you're asking yourself | Right loop | Example at Papyrus |
|---|---|---|
| "For each element of this collection..." | for |
Walk through the catalog |
| "Exactly N times" | for with range(N) |
Print 3 receipt lines |
| "While this condition holds..." | while |
Sell while there's stock left |
| "Until the user does X" | while |
Repeat a question until the answer is valid |
| "I don't know how many passes it'll take" | while |
Wait for correct input |
Golden rule: if you know the collection or the number of repetitions in advance, use for; if stopping depends on something that happens during execution, use while. In practice, for is far more common in Python.
Common Mistakes and Tips
- Expecting
range(5)to include 5: it generates 0 to 4. If you want 1 to 5, writerange(1, 6). The classic off-by-one error is almost always born here. - The infinite
whileloop: forgetting to update the condition's variable (stock -= 1, reading a newinput()...). If your program "hangs", this is it; interrupt withCtrl+Cand check what should be changing on each pass. - Initializing the accumulator inside the loop:
total = 0must go before thefor; inside, it resets on every pass and the final total would just be the last amount. - Modifying a
forloop's variable expecting to alter the traversal: doingnumber += 10insidefor number in range(5)doesn't skip iterations; on the next passnumbertakes the next value of the range regardless. To control the jumps userange()'s step or awhile. - Indentation in nested loops: a line indented one level too many or too few changes how many times it runs. If a
printcomes out repeated more times than it should, check its level. - Tip: name the loop variable in the singular and the collection in the plural (
for title in titles). The code reads itself. - Tip: before writing a loop, say out loud what you want: if the sentence starts with "for each..." it's a
for; if it starts with "while..." it's awhile. It works surprisingly well.
Exercises
Exercise 1: aligned catalog with numbering
Take the aligned table from initial_catalog.py (module 1) and generate it now with a loop. With the lists titles = ["The Odyssey", "Hamlet", "Don Quixote"] and prices = [12.50, 9.95, 15.90], use enumerate() to print:
(Title left-aligned in 12 characters, price right-aligned in 6 with two decimals.)
Exercise 2: daily takings with statistics
Given sales = [12.50, 9.95, 15.90, 15.90, 12.50, 9.95, 15.90], write daily_till.py which, with a single loop (without using sum(), max() or min(), which you'll see later), calculates and prints: the till total, the number of sales, the highest sale and how many sales exceeded 10 EUR.
Exercise 3: restocking piggy bank
Ana is setting money aside to restock Don Quixote (15.90 EUR per copy). Each day she can save 4 EUR. Write a while loop that simulates the days: it accumulates 4 EUR per day and prints Day N: M.MM EUR saved until she has enough to buy 2 copies. At the end, say how many days it took.
Solutions
Solution 1:
titles = ["The Odyssey", "Hamlet", "Don Quixote"]
prices = [12.50, 9.95, 15.90]
for i, title in enumerate(titles, start=1):
price = prices[i - 1]
print(f"{i}. {title:<12}{price:>6.2f} EUR")Since enumerate starts at 1 but list positions start at 0, we access the price with prices[i - 1]. An equally valid alternative: enumerate(titles) without start, use prices[i] and print {i + 1}.
Solution 2:
sales = [12.50, 9.95, 15.90, 15.90, 12.50, 9.95, 15.90]
total = 0.0
num_sales = 0
highest_sale = 0.0
big_sales = 0
for amount in sales:
total += amount
num_sales += 1
if amount > highest_sale:
highest_sale = amount
if amount > 10:
big_sales += 1
print(f"Till total: {total:.2f} EUR")
print(f"Number of sales: {num_sales}")
print(f"Highest sale: {highest_sale:.2f} EUR")
print(f"Sales > 10 EUR: {big_sales}")Four variables, four patterns: two unconditional accumulators/counters (total, num_sales), a maximum (updated only if the amount beats the best seen so far) and a conditional counter. The total should come to 92.60 EUR and the maximum 15.90 EUR.
Solution 3:
QUIXOTE_PRICE = 15.90
DAILY_SAVINGS = 4.0
goal = QUIXOTE_PRICE * 2 # 31.80 EUR
saved = 0.0
days = 0
while saved < goal:
days += 1
saved += DAILY_SAVINGS
print(f"Day {days}: {saved:.2f} EUR saved")
print(f"Goal reached in {days} days! ({saved:.2f} EUR towards {goal:.2f} EUR)")The condition saved < goal guarantees the loop stops as soon as the target is reached or exceeded: at 4 EUR a day you get to 32 EUR on day 8. Notice that we increment days at the start of each pass so the message shows the right day.
Conclusion
With this lesson, Papyrus has made a leap in scale: we're no longer working book by book, but over the whole catalog. You've learned the for loop with range() (start included, stop excluded, optional step), how to traverse strings and lists, the while loop to repeat as long as a condition holds, the counter and accumulator patterns with which we closed the daily takings, nested loops, and the elegant enumerate() to have position and value at once.
Even so, our loops are still somewhat rigid: they always traverse the whole sequence, start to finish. What if we find the book we were looking for halfway through the catalog and want to stop there? What if we want to skip out-of-stock titles and carry on with the rest? What if we need a menu that repeats indefinitely until Ana chooses "exit"? For all of that there are the control flow tools — break, continue, else on loops and the modern match statement — which are exactly the topic of the next lesson, where we'll build Papyrus's first interactive menu.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
