for Loops, range() and Iterating Collections

Python’s for loop is different from JavaScript’s for loop in a fundamental way โ€” it is a for-each loop that iterates over any iterable object directly, rather than incrementing an index variable. This makes Python loops more readable and less error-prone: there is no off-by-one error, no need to track an index variable, and no need to call .length. Python’s for loop works seamlessly with lists, tuples, strings, dictionaries, sets, generators, and any object that implements the iterator protocol. You will use for loops constantly in FastAPI to process database query results, transform API response data, and validate collections of values.

Basic for Loop

# Iterate directly over items โ€” no index needed
fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit)
# apple
# banana
# cherry

# Iterate over a string (character by character)
for char in "hello":
    print(char)   # h, e, l, l, o

# Iterate over a range of numbers
for i in range(5):         # 0, 1, 2, 3, 4
    print(i)

for i in range(1, 6):      # 1, 2, 3, 4, 5
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (step=2)
    print(i)

for i in range(10, 0, -1): # 10, 9, 8, ... 1 (countdown)
    print(i)
Note: Python’s range() does not create a list in memory โ€” it is a lazy sequence that generates numbers on demand. This means range(1_000_000) uses almost no memory, while [0, 1, 2, ..., 999999] uses about 8MB. This matters in FastAPI when processing large datasets: iterating over a range or a database cursor is memory-efficient, while converting them to a list first wastes memory.
Tip: Use enumerate() when you need both the index and the value โ€” it is more Pythonic than using range(len(iterable)) to get an index. The syntax is for i, value in enumerate(items):. You can also start the counter at a different number: enumerate(items, start=1) starts counting from 1 instead of 0. This is cleaner than manually tracking a counter variable.
Warning: Never modify a list while iterating over it โ€” this produces unpredictable results and may skip items or raise a RuntimeError. If you need to remove items from a list, iterate over a copy: for item in items[:]: or collect items to remove in a separate list and remove them after the loop. This is a common source of subtle bugs when filtering FastAPI query results.

enumerate() โ€” Index and Value Together

posts = ["First Post", "Second Post", "Third Post"]

# Old way โ€” verbose and error-prone
for i in range(len(posts)):
    print(f"{i}: {posts[i]}")

# Pythonic way โ€” use enumerate()
for i, post in enumerate(posts):
    print(f"{i}: {post}")
# 0: First Post
# 1: Second Post
# 2: Third Post

# Start index at 1 (for human-readable numbering)
for i, post in enumerate(posts, start=1):
    print(f"Post {i}: {post}")
# Post 1: First Post
# Post 2: Second Post
# Post 3: Third Post

# Practical FastAPI use: add position to response items
results = db.get_posts()
numbered = [{"rank": i, "post": p} for i, p in enumerate(results, start=1)]

zip() โ€” Looping Over Multiple Sequences

names  = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
grades = ["A", "B+", "A-"]

# Loop over multiple lists in parallel
for name, score, grade in zip(names, scores, grades):
    print(f"{name}: {score} ({grade})")
# Alice: 95 (A)
# Bob: 87 (B+)
# Charlie: 92 (A-)

# zip() stops at the shortest sequence
a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]
list(zip(a, b))   # [(1, 'a'), (2, 'b'), (3, 'c')] โ€” stops at 3

# Create a dict from two lists
keys   = ["name", "age", "role"]
values = ["Alice", 30, "admin"]
user   = dict(zip(keys, values))
# {"name": "Alice", "age": 30, "role": "admin"}

Iterating Over Dictionaries

user = {"name": "Alice", "age": 30, "role": "admin"}

# Iterate over keys (default)
for key in user:
    print(key)           # name, age, role

# Iterate over values
for value in user.values():
    print(value)         # Alice, 30, admin

# Iterate over key-value pairs
for key, value in user.items():
    print(f"{key}: {value}")
# name: Alice
# age: 30
# role: admin

# Practical use: filter a response dict
def sanitise_user(user_dict: dict) -> dict:
    excluded = {"password", "password_hash", "reset_token"}
    return {k: v for k, v in user_dict.items() if k not in excluded}
# Removes sensitive fields before sending to the client

Common Mistakes

Mistake 1 โ€” Modifying a list while iterating over it

โŒ Wrong โ€” removes items during iteration, skips some:

items = [1, 2, 3, 4, 5]
for item in items:
    if item % 2 == 0:
        items.remove(item)   # Bug! Skips items as list shrinks
print(items)   # [1, 3, 5] โ€” looks correct but skipped item 4 in some cases

โœ… Correct โ€” use a list comprehension to filter:

items = [1, 2, 3, 4, 5]
items = [item for item in items if item % 2 != 0]
print(items)   # [1, 3, 5] โœ“

Mistake 2 โ€” Using range(len()) when enumerate() is cleaner

โŒ Wrong โ€” JavaScript-style index loop:

for i in range(len(posts)):
    print(f"{i}: {posts[i]}")   # verbose and error-prone

โœ… Correct โ€” enumerate():

for i, post in enumerate(posts):
    print(f"{i}: {post}")   # โœ“ cleaner

Mistake 3 โ€” Forgetting range() is exclusive of the end

โŒ Wrong โ€” expecting range(1, 5) to include 5:

for i in range(1, 5):
    print(i)   # 1, 2, 3, 4 โ€” NOT 5!

โœ… Correct โ€” range end is exclusive, use range(1, 6) to include 5:

for i in range(1, 6):
    print(i)   # 1, 2, 3, 4, 5 โœ“

Quick Reference

Pattern Code
Iterate list for item in items:
Count from 0 to n-1 for i in range(n):
Count from a to b-1 for i in range(a, b):
Count with step for i in range(0, 10, 2):
Index + value for i, v in enumerate(items):
Two lists together for a, b in zip(list1, list2):
Dict keys for key in d:
Dict values for val in d.values():
Dict pairs for k, v in d.items():

🧠 Test Yourself

You have two lists: user_ids = [1, 2, 3] and usernames = ["alice", "bob", "charlie"]. How do you produce a dictionary {1: "alice", 2: "bob", 3: "charlie"} in one line?