Loops and conditions
A loop does the same work once for every item in a group. A condition decides which work to do. Together they are how every program makes a decision about every row of your data.
- 15 min read
- 3 reading levels
- Published
Read these first
On this page 8
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
A loop repeats the same steps for every item in a group. A condition picks which steps to take.
Think about sorting a basket of washed clothes. You pick up one piece, look at it, and decide: whites in this pile, colours in that pile. Then you pick up the next piece and do exactly the same thing.
That is both ideas at once. Picking up every piece in turn is a loop. Deciding which pile it belongs in is a condition.
You have done this a thousand times without calling it programming. Python only asks you to write down what your hands already know.
Why you should care
You now have a list of a hundred rows. You need to check something about each one and act on it.
That is what training a model is. Look at an example, compare the guess to the answer, adjust, move to the next example. Millions of times. Under all the vocabulary, it is a loop with a condition inside it.
Why they exist
Without a loop, ten students means ten copies of the same lines. A hundred students means a hundred copies.
Worse, you would have to know the number in advance. You cannot write a hundred copies for a file whose length you learn only when you open it.
A loop says "for each of these, do this" without caring how many there are. One block of instructions, any amount of data.
A condition exists because not everything deserves the same treatment. Some rows are missing values. Some payments look suspicious. Some emails are spam. A program with no conditions cannot tell any of them apart.
How they work
pick up the next piece of clothing
|
v
is it white?
|
yes ─────┴───── no
| |
v v
white pile colour pile
| |
└──────┬───────┘
v
any clothes left? ── yes ──→ back to the top
|
no
v
doneRead that loop once more. Nothing in it mentions how many clothes there are. That is the whole point.
Python has two kinds of loop, and the difference is when they stop.
for-each loop → runs once per item, then stops.
You know it will end, because the group is a fixed size.
while loop → keeps running while something stays true.
It stops when that thing becomes false. Not before.Use the first one almost always. The second is for the times when you do not know in advance how many rounds you need.
The one rule about blank space
In most languages, curly brackets show which lines belong inside the loop. Python uses indentation — the blank space at the start of a line.
for each shirt in the basket
look at it ← indented, so this is inside the loop
put it in a pile ← indented, so this is inside the loop
count the piles ← not indented, so this runs once at the endMove a line left or right and you change what the program does. This makes Python pleasant to read and unforgiving to type. Set your editor to insert four spaces when you press Tab and the problem disappears.
A real example you have seen
Your email inbox. Every message that arrives goes through a check: does this look like spam? One condition, applied to each message in turn.
Your bank card. Every transaction is compared against your usual pattern before it is approved. Your phone gallery, when it groups photos by face. A shop bill, adding up items one at a time.
None of those know in advance how many items are coming. All of them are a loop with a condition inside.
An honest word
Two things trip up almost everyone in the first week, and they are worth naming.
The first is indentation. A line that looks right can be one space out, and the program will run and give a wrong answer without complaining. When something behaves oddly, check the blank space before you check anything else.
The second is a loop that never ends, because the thing you were waiting for never became false. The program hangs and the window sits there. Hold Ctrl and press C to stop it. That is not a crash and you have broken nothing.
Remember this
- A loop runs the same block once for each item, however many there are.
- A condition chooses between different actions for different items.
- Indentation decides what is inside the loop, so treat blank space as real code.
What to learn next
- Functions — naming a block of work so you can reuse it.
- Lists and dictionaries — the groups you loop over.
- NumPy — where you learn to delete loops instead of writing them.
Developer — Code and libraries.
Setup
Core Python. Nothing to install, nothing to import.
Two pieces of syntax carry this whole lesson. A line that opens a block ends with a colon :, and the lines inside that block are indented by four spaces.
Sorting a basket
clothes = ["white shirt", "blue jeans", "white socks", "red kurta", "white vest"]
whites = []
colours = []
for item in clothes: # 'for each' - the indented block runs once per item
if "white" in item: # a condition: this is either True or False
whites.append(item)
else:
colours.append(item)
print("whites :", whites)
print("colours:", colours)
print(f"{len(whites)} white, {len(colours)} coloured")
for n in range(1, 4): # range gives you numbers when you need counting
print("wash cycle", n)
water_litres = 30 # a while loop: repeat until the condition fails
cycles = 0
while water_litres >= 10:
water_litres = water_litres - 10
cycles = cycles + 1
print("cycles possible:", cycles, "| water left:", water_litres)whites : ['white shirt', 'white socks', 'white vest'] colours: ['blue jeans', 'red kurta'] 3 white, 2 coloured wash cycle 1 wash cycle 2 wash cycle 3 cycles possible: 3 | water left: 0
Line-by-line walkthrough
for item in clothes: creates a new name, item, and points it at each value in turn. You never write the positions yourself. If you catch yourself writing for i in range(len(clothes)) and then only using clothes[i], drop the index and loop over the values.
if "white" in item: is a test that produces True or False. For text, in asks "does this appear inside that". For a list, it asks "is this one of the values".
else: catches everything the if did not. Its indentation lines up with the if, which is how Python knows they are a pair.
range(1, 4) produces 1, 2 and 3. The stop number is not included, exactly like a list slice. range(4) on its own starts at zero and gives 0, 1, 2, 3. This is the same rule everywhere in Python, and internal consistency is why it stops being annoying.
while water_litres >= 10: checks the condition before every round, including the first. If the condition is false at the start, the body never runs at all.
water_litres = water_litres - 10 is the line that eventually ends the loop. A while loop with nothing changing inside it runs forever. Whenever you write while, find that line before you run anything.
Choosing between many outcomes
scores = {"Asha": 88, "Ravi": 45, "Meera": 95, "Iqbal": 61}
for name, score in scores.items(): # .items() hands you the label and value together
if score >= 90:
band = "distinction"
elif score >= 60: # elif is checked only if the ones above failed
band = "pass"
else:
band = "needs help"
print(f"{name:<6}{score:>4} {band}")
print()
for position, name in enumerate(scores, start=1): # enumerate counts as it walks
print(position, name)
print()
for name, score in scores.items():
if score < 50:
continue # skip the rest of this round, go to the next
if score == 95:
print("top scorer found:", name)
break # leave the loop entirely
print("checked", name)Asha 88 pass Ravi 45 needs help Meera 95 distinction Iqbal 61 pass 1 Asha 2 Ravi 3 Meera 4 Iqbal checked Asha top scorer found: Meera
elif matters more than it looks. The order is top to bottom and the first match wins. Reverse the two tests here and every distinction becomes a pass, with no error to warn you.
.items() gives you both halves of each dictionary entry. Looping over a dictionary directly gives you only the labels, which is why enumerate(scores, ...) prints names and not scores.
continue and break are the two escape hatches. continue abandons this round; break abandons the loop. Both work in for and while.
Common mistakes
1. A line indented one level too far, or not far enough.
total = 0
for n in [10, 20, 30]:
total = total + n
print("running total:", total)running total: 10 running total: 30 running total: 60
Pull that print back to the left margin and it runs once, after the loop, printing running total: 60. Neither version is an error. Only one is what you meant.
2. Changing a list while looping over it.
nums = [1, 2, 2, 3, 4, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)[1, 2, 3, 4]
Two even numbers survived. The loop walks by position while remove shifts everything left, so each removal makes the loop skip the next item. Fix: loop over a copy with for n in nums[:], or build a new list with [n for n in nums if n % 2 != 0].
3. A while loop whose condition never changes.
count = 5
while count > 0:
print("waiting")This prints forever. Press Ctrl+C to stop it. The body forgot count = count - 1. Before running any while loop, point at the line that moves the condition toward false.
4. Testing a value for truth when you meant to test for presence.
score = 0
if score:
print("has a score")
else:
print("looks empty")looks empty
Zero counts as false in Python, along with the empty string, the empty list and None. A student who genuinely scored zero is treated as having no score at all. Fix: if score is not None:. This bug is quiet, it is common in data cleaning, and it changes your results without changing your output format.
5. Comparing with a single =.
if score = 90: is a SyntaxError. One equals sign points a name at a value; two ask whether they match. Python refusing to run it is a kindness — languages that allow it produce silent wrong answers.
Try it yourself
Take the scores dictionary and count how many students passed, using a loop and a counter variable. Print the count and the names.
Then rewrite the pass count as a one-line list comprehension, which builds a list from a loop in a single expression: passed = [n for n, s in scores.items() if s >= 60]. Compare the two and decide which reads better to you at this stage. Both are correct Python.
Last, write a while loop that starts at 100 and halves the value until it drops below 1, printing each step. Count how many rounds it takes before you run it.
What to learn next
- Functions — turning a loop you keep rewriting into one name.
- NumPy — replacing loops over numbers with one operation.
- Python for machine learning — where loop-shaped and array-shaped code meet.
Researcher — Mathematics and papers.
for is sugar over the iterator protocol
A for statement is defined in terms of two dunder methods, not over indices. iter(obj) calls obj.__iter__() and must return an iterator; the iterator's __next__() yields values and raises StopIteration when exhausted. The loop catches that exception and exits.
nums = [10, 20, 30]
it = iter(nums) # a for loop does this for you
print(next(it), next(it), next(it))
try:
next(it)
except StopIteration:
print("iterator exhausted")
def squares(n): # a generator: values produced on demand
for i in range(n):
yield i * i
g = squares(4)
print("lazy:", list(g))
print("second pass:", list(g)) # generators are single-use
for x in nums: # for/else: else runs only if no break happened
if x > 100:
break
else:
print("no value above 100 was found")
print("and/or return operands:", 0 or "fallback", "|", 5 and 7)10 20 30 iterator exhausted lazy: [0, 1, 4, 9] second pass: [] no value above 100 was found and/or return operands: fallback | 7
Three consequences worth internalising. An iterator is single-use, so a generator passed to two consumers silently gives the second one nothing — a real and painful bug in data-loading code. for/else binds the else to the absence of break, which reads badly and is why most style guides discourage it. And and/or return one of their operands rather than a bool, which is what makes x = value or default idiomatic and also what makes it wrong when value is a legitimate 0.
Laziness and memory
range is not a list; it is a lazy sequence with O(1) memory and O(1) indexing. map, filter, zip, enumerate and dict.items() all return lazy iterators or views. Generator expressions (f(x) for x in xs) are lazy; list comprehensions [f(x) for x in xs] are not.
The rule for pipelines over data too large for memory: keep every stage lazy and materialise once at the end. sum(x * x for x in stream) allocates nothing. sum([x * x for x in stream]) allocates the whole list first.
itertools covers the common composed cases — islice, chain, groupby, accumulate, product — and all of them stay lazy.
Cost model
The interpreter cost of a Python-level loop is dominated by dispatch, not by the arithmetic. Each iteration pays for a FOR_ITER, the __next__ call, boxing of the produced object, and reference-count traffic on every intermediate. Rough ordering, measured on the same work:
- Vectorised NumPy — one dispatch for the whole array.
- List comprehension — one specialised opcode path, no bound-method lookup per item.
forloop withlist.append— a method lookup and call per item.forloop calling a Python function per item — a frame construction per item on top of that.
Do not quote ratios from a page like this one. Measure with timeit on your own machine, because the gaps depend on CPython version, build flags and what else is running. Since 3.11, PEP 659's specialising adaptive interpreter narrows the gap between 2 and 3 considerably by quickening hot opcodes.
Two algorithmic notes that dwarf all of the above. A membership test against a list inside a loop is the accidental quadratic, and swapping the inner container to a set fixes it. And loop-invariant work — an attribute lookup, a len(), a re.compile — belongs outside the loop, because CPython will not hoist it for you.
Name resolution matters at the margin: locals are an array index (LOAD_FAST), globals and builtins are dictionary lookups (LOAD_GLOBAL). Binding local_append = out.append before a hot loop is an old and still-real micro-optimisation.
Conditions
if/elif/else is evaluated top to bottom with short-circuit semantics; and and or do not evaluate their right operand unless required, which is what makes if xs and xs[0] > 0 safe on an empty list.
Truthiness is delegated to __bool__, falling back to __len__. Anything with __len__ returning zero is falsy. This is the source of the if array: failure on NumPy: an array with more than one element raises ValueError: The truth value of an array with more than one element is ambiguous, because there is no single sensible answer. The pandas lesson shows the same problem with and on Series.
PEP 634 added structural pattern matching in 3.10. match/case binds and destructures rather than only testing, which makes it genuinely better than an elif chain for tagged unions — parsing model configs, dispatching on message type — and no better at all for numeric range checks.
Why this matters for ML code
Almost every performance problem in a beginner's training script is a Python-level loop over elements that should have been one array operation. The interpreter overhead per element is fixed and unavoidable; the only remedy is to have fewer Python-level steps.
The healthy shape is: Python loops over batches, compiled code loops over elements. A loop whose body triggers milliseconds of C or CUDA work costs nothing. A loop whose body does one multiply costs everything. Python basics covers the interpreter side of why.
References
- PEP 234 — Iterators, Yee and van Rossum, 2001.
- PEP 255 — Simple Generators, Schemenauer, Peters and Hetland, 2001.
- PEP 289 — Generator Expressions, Hettinger, 2003.
- PEP 634 — Structural Pattern Matching: Specification, Bucher, van Rossum et al., 2020.
- PEP 659 — Specializing Adaptive Interpreter, Shannon, 2021.
- CPython source,
Python/ceval.c— theFOR_ITERimplementation and the eval loop.
What to learn next
- Functions — frames, closures and call overhead.
- NumPy — vectorisation as loop elimination.
- Python for machine learning — the pipeline these constructs assemble.