Lists and Loops

🔁 Lists & Loops

Lists are Python's workhorse collection — flexible, powerful, and everywhere in real code. Python's loop syntax is simpler than Java's but more expressive, especially with list comprehensions, which replace most map() / filter() patterns.

⚡ Coming from Java / JavaScript Python has no C-style for loop. There is no for (int i = 0; i < n; i++). Python's for is always a for-each. Want an index? Use enumerate(). Want a numeric range? Use range(). List comprehensions replace most stream().map().filter().collect() chains from Java and array.map().filter() from JavaScript — but they're more readable.
🚀 FastAPI relevance — HIGH FastAPI endpoints routinely return lists. List comprehensions are the standard way to transform database results into API response models. Understanding how to iterate, filter, and reshape lists is essential for writing clean FastAPI route handlers.

📋 Lists — Creation, Indexing & Slicing

Creating Lists

List Literals
[ ] syntax
Square brackets, comma-separated. Can hold mixed types (but keep them consistent in practice). The empty list is just [].
# Typed consistently ingredients = ["eggs", "milk", "flour"] quantities = [6, 2, 500] empty = [] # Mixed types (avoid in practice) mixed = ["eggs", 6, True]
Constructing Lists
list() & range()
list() converts any iterable to a list. range() generates a sequence of integers — combine them to make numeric lists.
# Convert other iterables chars = list("hello") # → ['h', 'e', 'l', 'l', 'o'] # range(start, stop, step) days = list(range(1, 8)) # → [1, 2, 3, 4, 5, 6, 7] weeks = list(range(0, 28, 7)) # → [0, 7, 14, 21]

Indexing & Slicing

Python supports negative indexing and slicing — two features that have no direct Java equivalent and are genuinely useful.

0-5"eggs"
1-4"milk"
2-3"flour"
3-2"butter"
4-1"sugar"
Indexing — Positive & Negative
Positive indices count from the start; negative indices count from the end. No more list.get(list.size() - 1) to get the last element!
items = ["eggs", "milk", "flour", "butter", "sugar"] # Positive indexing print(items[0]) # "eggs" — first print(items[2]) # "flour" # Negative indexing — counts from the end print(items[-1]) # "sugar" — last (no .size()-1 needed!) print(items[-2]) # "butter" — second to last
Slicing — list[start : stop : step]
Returns a new list — the original is unchanged. stop is exclusive. Any part can be omitted to mean "from the beginning" or "to the end".
meals = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] meals[0:3] # → ["Mon", "Tue", "Wed"] (stop is exclusive) meals[5:] # → ["Sat", "Sun"] (from index 5 to end) meals[:2] # → ["Mon", "Tue"] (first 2) meals[-2:] # → ["Sat", "Sun"] (last 2) meals[::] # → full copy of the list meals[::-1] # → reversed list (very Pythonic!) meals[1:6:2] # → ["Tue", "Thu", "Sat"] (every 2nd from 1 to 5)

🛠️ Essential List Methods

Python lists are mutable — methods modify them in place (they return None, not a new list). This is different from string methods, which always return a new value.

Adding, Removing & Reordering
pantry = ["flour", "sugar", "salt"] # Adding pantry.append("butter") # add to end → ["flour","sugar","salt","butter"] pantry.insert(1, "eggs") # insert at index 1 pantry.extend(["milk", "cream"]) # add multiple items # Removing pantry.remove("salt") # remove by value (first match) popped = pantry.pop() # remove & return last item popped = pantry.pop(0) # remove & return item at index 0 pantry.clear() # remove all items # Reordering pantry.sort() # sort in place (ascending) pantry.sort(reverse=True) # sort descending pantry.reverse() # reverse in place # Non-mutating alternatives (return new list/value) new_list = sorted(pantry) # sorted() → new sorted list n = len(pantry) # length idx = pantry.index("eggs") # index of first match count = pantry.count("flour") # how many times it appears exists = "milk" in pantry # True / False

🔄 Loop Patterns

Basic for-each
for item in collection:
for ingredient in pantry: print(ingredient) # Works on any iterable: # lists, tuples, strings, # dicts, files, ranges...
The standard loop. Always prefer this over index-based loops when you just need the values.
With index — enumerate()
for i, item in enumerate(collection):
for i, meal in enumerate(meals): print(f"Day {i+1}: {meal}") # Start at 1 instead of 0: for i, meal in enumerate(meals, 1): print(f"Day {i}: {meal}")
Replaces the for(int i=0...) pattern. Never use range(len(x)) when enumerate will do.
Parallel iteration — zip()
for a, b in zip(list1, list2):
items = ["eggs", "milk"] qtys = [6, 2] for item, qty in zip(items, qtys): print(f"{item}: {qty}") # Stops at the shortest list
Iterate two (or more) lists in parallel. Much cleaner than indexing manually.
Numeric range
for i in range(start, stop, step):
for day in range(7): # 0–6 print(f"Day {day}") for week in range(1, 5): # 1–4 print(f"Week {week}") for i in range(10, 0, -1): # countdown print(i)
When you genuinely need a number, not an item. stop is always exclusive.
While loop
while condition:
days_left = 7 while days_left > 0: print(f"{days_left} days left") days_left -= 1 # Useful for: polling, retries, # user input, unknown length
Use when you don't know how many iterations upfront. Prefer for when iterating a collection.
break / continue / else
Control flow inside loops
for item in pantry: if item == "expired": break # exit loop entirely if item == "skip": continue # skip to next iteration print(item) else: # Runs if loop wasn't broken print("All items checked")
The for/else block is Python-unique — the else runs only if the loop completed without a break.

⚡ List Comprehensions

List comprehensions are one of Python's most loved features — a concise way to build a new list by transforming or filtering an existing one. They replace most map() + filter() patterns and are more readable once you're used to them.

The Pattern: [ expression for item in iterable if condition ]
Read left to right: "give me expression for each item in iterable, but only if condition is true."
# Traditional loop approach expensive = [] for item in shopping_list: if item["price"] > 5.0: expensive.append(item["name"]) # List comprehension — same result, one line expensive = [item["name"] for item in shopping_list if item["price"] > 5.0]
Meal Planner Examples
Real-world comprehension patterns themed around the meal planner project.
ingredients = [ {"name": "milk", "days_left": 2, "qty": 1}, {"name": "eggs", "days_left": 10, "qty": 6}, {"name": "yogurt", "days_left": 1, "qty": 2}, {"name": "cheese", "days_left": 5, "qty": 1}, ] # 1. Transform — get just the names names = [i["name"] for i in ingredients] # → ["milk", "eggs", "yogurt", "cheese"] # 2. Filter — items expiring within 3 days expiring = [i["name"] for i in ingredients if i["days_left"] <= 3] # → ["milk", "yogurt"] # 3. Transform with expression alerts = [ f"{i['name']} expires in {i['days_left']} day(s)!" for i in ingredients if i["days_left"] <= 3 ] # → ["milk expires in 2 day(s)!", "yogurt expires in 1 day(s)!"] # 4. Uppercase all names upper_names = [i["name"].upper() for i in ingredients] # → ["MILK", "EGGS", "YOGURT", "CHEESE"]
Nested Loops & Comprehensions — Weekly Meal Plan
Nested comprehensions work but should stay readable — if it needs more than two levels, use a regular loop instead.
days = ["Mon", "Tue", "Wed", "Thu", "Fri"] meals = ["Breakfast", "Lunch", "Dinner"] # Nested loop — all day/meal combinations schedule = [] for day in days: for meal in meals: schedule.append(f"{day} {meal}") # Same thing as a comprehension schedule = [f"{day} {meal}" for day in days for meal in meals] # Practical nested loop — expiry check across categories fridge = { "dairy": ["milk", "yogurt"], "produce": ["spinach", "carrots"], } for category, items in fridge.items(): for item in items: print(f"[{category}] {item}")

📋 Quick Reference

OperationSyntaxNotes
Createitems = ["a", "b"]Empty list: items = []
Get by indexitems[0] / items[-1]Negative counts from end
Sliceitems[1:3] / items[::-1]stop is exclusive; [::-1] reverses
Lengthlen(items)Works on all collections and strings
Membership"a" in itemsReturns True/False
Appenditems.append("c")Adds to end — O(1)
Insertitems.insert(1, "x")Inserts at index — O(n)
Extenditems.extend(other)Add all items from another list
Remove by valueitems.remove("a")Removes first match — raises ValueError if missing
Remove by indexitems.pop(0)Returns the removed item
Sort in placeitems.sort()Returns None — modifies original
Sort (new list)sorted(items)Returns new sorted list — original unchanged
Reverseitems.reverse()In place — or use items[::-1] for new list
for-eachfor x in items:Always prefer over index-based loops
With indexfor i, x in enumerate(items):Replaces for(int i=0...)
Parallelfor a, b in zip(l1, l2):Stops at shortest list
Rangefor i in range(5):range(start, stop, step)
Comprehension[f(x) for x in items if cond]Transform + filter in one expression
Copyitems.copy() / items[:]Shallow copy — see gotcha below!
⚠ Gotchas for Java / JavaScript Programmers:

Assignment copies the reference, not the listb = a makes both variables point to the same list. Changing b changes a too. To copy, use b = a.copy() or b = a[:]. This is the same as Java object references — it just surprises people who expect value semantics.

sort() returns None — unlike Java's Collections.sort() which also returns void, Python beginners often write items = items.sort() and wonder why items is now None. Use items.sort() to sort in place, or items = sorted(items) to get a new sorted list.

Never modify a list while iterating over it — removing items from a list inside a for loop over that same list causes skipped items and hard-to-find bugs. Instead, iterate over a copy: for item in items.copy(): or build a new list with a comprehension.

range() is lazyrange(1000000) doesn't create a million-element list in memory. It generates numbers on demand. This is a good thing — just don't be surprised when print(range(5)) shows range(0, 5) instead of a list.