QA work is full of repetition: run the same test against five browsers, retry an API call until it succeeds, classify a thousand log lines. Python's loops are designed to make those jobs short. Unlike Java's three-clause for (int i = 0; i < n; i++), Python's for iterates directly over a sequence — list, string, range, dict, set — and lets you forget about indices most of the time. This lesson covers for, while, range(), enumerate(), break, continue, and Python's lesser-known for/else clause.
The basic for loop — iterate over a sequence
browsers = ["Chrome", "Firefox", "Safari"]
for browser in browsers:
print(f"Testing on {browser}")Output:
Testing on Chrome
Testing on Firefox
Testing on Safari
Read it as "for each browser in browsers, do this." There's no counter, no length, no i++. Python pulls each element out for you and binds it to the loop variable.
You can iterate over almost any sequence — strings (gets characters), lists (gets items), tuples, dicts (gets keys), sets, even file objects (gets lines):
for ch in "QA":
print(ch) # Q, then A on separate lines
config = {"env": "staging", "retries": 3}
for key in config:
print(key) # "env", then "retries"This is genuinely simpler than Java's enhanced-for, and miles ahead of the C-style for (int i = 0; …) form for plain iteration.
range() — when you need numbers
When you want to repeat N times or iterate over indices, use range(). It generates a sequence of integers lazily:
for i in range(5):
print(f"Test case {i + 1}")Output:
Test case 1
Test case 2
Test case 3
Test case 4
Test case 5
range(5) produces 0, 1, 2, 3, 4 — five numbers, starting at 0 by default, stop value is exclusive. The same end-exclusive convention as Java's for (int i = 0; i < 5; i++).
range() accepts up to three arguments — range(start, stop, step):
for i in range(1, 6): # 1, 2, 3, 4, 5
print(f"Attempt {i}")
for i in range(0, 10, 2): # 0, 2, 4, 6, 8
print(i)
for i in range(10, 0, -1): # 10, 9, 8, …, 1
print(i)range() doesn't actually build a list — it's a generator that yields one number at a time. You can range(1_000_000_000) without using gigabytes of memory. (List comprehensions, in the next lesson, are how you'd materialise a range into a list when you need one.)
enumerate() — index and value together
If you find yourself reaching for range(len(things)) to get an index alongside each item, stop. Python's idiomatic answer is enumerate():
browsers = ["Chrome", "Firefox", "Safari"]
for i, browser in enumerate(browsers):
print(f"{i + 1}. {browser}")Output:
1. Chrome
2. Firefox
3. Safari
enumerate() yields (index, value) pairs that Python unpacks into the two loop variables. Pass start=1 if you want one-based numbering directly: for i, browser in enumerate(browsers, start=1):.
The non-Pythonic version — for i in range(len(browsers)): browser = browsers[i] — works but every reviewer will rewrite it to enumerate.
Looping over a dictionary
Iterating a dict gives you the keys by default. To get key and value, call .items():
config = {"env": "staging", "retries": 3, "timeout": 30}
for key, value in config.items():
print(f"{key:<10} = {value}")Output:
env = staging
retries = 3
timeout = 30
We'll cover .items(), .keys(), and .values() properly in chapter 3.
while loops — repeat until a condition fails
A while loop runs as long as a condition is true. Use it when you don't know the number of iterations in advance — typically retry logic, polling, or "keep trying until the timeout":
retry_count = 0
max_retries = 3
success = False
while retry_count < max_retries and not success:
print(f"Attempt {retry_count + 1}")
success = call_api() # imagine this returns True or False
retry_count += 1
if success:
print("API call succeeded")
else:
print(f"Gave up after {max_retries} attempts")Two things to notice:
- No do-while loop in Python. If you need "run at least once, then check," start with a
while True:andbreakout manually. +=is the increment. Python doesn't have++— writeretry_count += 1.
The classic infinite-loop bug — forgetting to advance the counter — bites in Python the same way it does in Java. Always check the loop variable changes inside the body.
break and continue
Same idea as in JS and Java:
break— exit the loop entirely.continue— skip the rest of this iteration and start the next one.
test_results = ["pass", "pass", "fail", "pass", "fail"]
# Stop at the first failure
for status in test_results:
if status == "fail":
print("First failure found — stopping")
break
print(f"OK: {status}")
# Skip already-passed tests, only act on failures
for status in test_results:
if status == "pass":
continue
print(f"Investigating failure: {status}")break jumps out of the nearest enclosing loop. If you have nested loops, it only breaks the inner one — Python doesn't have labelled break (the workaround is to refactor into a function and return).
for/else — a Python-specific quirk
A loop can have an else clause. The else runs only if the loop completes without hitting break:
users = [
{"name": "Alice", "role": "tester"},
{"name": "Bob", "role": "viewer"},
{"name": "Carol", "role": "tester"},
]
for user in users:
if user["role"] == "admin":
print(f"Found admin: {user['name']}")
break
else:
print("No admin found")If any user has role == "admin", the loop prints the name and breaks — the else does not run. If the loop finishes naturally without finding one, the else runs.
This reads as "do this; otherwise (if we never broke), do that." Many Python developers find it confusing and avoid it; you'll see it occasionally in real code, mostly in search-and-fail-fast patterns. Recognise it; reach for a flag variable or a function with return if it makes the intent clearer.
A QA example — retrying an API call
A loop that retries a flaky call up to N times, with a short delay between attempts:
import time
def fetch_user(user_id):
"""Pretend to call an API. Returns None on failure."""
# ... real call here ...
return None
max_retries = 3
delay_seconds = 0.5
for attempt in range(1, max_retries + 1):
print(f"Attempt {attempt}/{max_retries}…")
user = fetch_user(42)
if user is not None:
print(f"Got user: {user}")
break
time.sleep(delay_seconds)
else:
print(f"Failed after {max_retries} attempts")The for/else here cleanly expresses "loop with breaks; if we never broke, give up." The same logic with while plus a success flag works too — pick the form that reads better in context.
A loop, drawn step by step
Step 1 of 6
Get the iterable
browsers = ['Chrome', 'Firefox', 'Safari'] — Python lines up the three values to iterate over.
⚠️ Common mistakes
- Using
range(len(items))to iterate. Pythonic code iterates the items directly (for item in items:) or usesenumerate(items)if you also need the index. Therange(len(...))form is a Java/C habit and triggers a "not Pythonic" comment in every code review. - Modifying a list while iterating it.
for item in items: items.remove(item)skips elements unpredictably. Build a new list with a comprehension, or iterate over a copy:for item in items[:]:. - Infinite
whileloops. Forgetting to update the loop variable (retry_count += 1) means the condition stays true forever. Stop the script with Ctrl+C and re-read the loop carefully — usually the increment is missing or the wrong variable is being checked.
🎯 Practice task
Loop over QA data three different ways. 25-30 minutes.
- Create
loop_practice.py. - Define a list
tests = ["login", "checkout", "search", "logout", "profile"]near the top. - for loop — print each test name with its 1-based number using
enumerate(tests, start=1). Output should look like1. login,2. checkout, …. - range — print "Retrying… (n)" for n in 1 to 5 inclusive. (Hint:
range(1, 6).) - while loop — define a variable
attempts = 0and a (fake) variablesucceeded = False. Loop whileattempts < 5 and not succeeded, incrementattemptseach time. After two iterations, setsucceeded = Trueto break out cleanly. Print the final attempt count. - break / continue — given
results = ["pass", "pass", "fail", "pass", "fail", "pass"], print only the indices of failures usingenumerateandcontinue. Then, in a separate loop, stop at the first failure withbreakand print "first failure at index N". - Run with
python loop_practice.py. - Stretch: write a
for/elseloop that searchestestsfor the string"admin". If found, print "Found"; if the loop completes without finding it, print "No admin test in suite". Confirm both branches by removing/adding the value.
You can now repeat work over any sequence Python understands. The next lesson formalises functions — packaging a piece of logic with a name so you can reuse it.