Python for AI

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.

On this page 8
  1. Why you should care
  2. Why they exist
  3. How they work
  4. The one rule about blank space
  5. A real example you have seen
  6. An honest word
  7. Remember this
  8. What to learn next

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
               done

Read 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 end

Move 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

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

laundry.py
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)
Output
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

marks.py
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)
Output
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.

python
total = 0
for n in [10, 20, 30]:
    total = total + n
    print("running total:", total)
Output
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.

python
nums = [1, 2, 2, 3, 4, 4]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(nums)
Output
[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.

python
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.

python
score = 0
if score:
    print("has a score")
else:
    print("looks empty")
Output
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

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.

iteration.py
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)
Output
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:

  1. Vectorised NumPy — one dispatch for the whole array.
  2. List comprehension — one specialised opcode path, no bound-method lookup per item.
  3. for loop with list.append — a method lookup and call per item.
  4. for loop 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 — the FOR_ITER implementation and the eval loop.

What to learn next

What to learn next

These follow on from what you just read.

  • Python for AI

    Functions

    A function is a block of work you give a name, so you can run it again with different inputs. Every AI library you will ever use is a pile of functions someone else already wrote.

  • Python for AI

    NumPy

    NumPy lets you do one operation to millions of numbers at once instead of one at a time. It is the foundation every AI library in Python is built on.

  • Python for AI

    Pandas

    Pandas is a table with named columns that you can filter, group and summarise in one line. It is where almost every AI project starts, because real data arrives as a table.