Why control flow matters for data work
In the previous article you learned how to store data in lists, tuples, sets and dictionaries. Those structures hold values, but on their own they do not do anything. To turn stored data into answers, you need code that can make decisions (this value is too high, skip it) and code that can repeat an action across many items (do this for every row). That is exactly what conditions and loops give you. Every data cleaning script, every aggregation, and even the vectorised operations you will eventually use in pandas or numpy are built on the same two ideas you will learn here: branching with if/elif/else, and repeating with for and while.
This article treats both topics in depth, with examples built around small, realistic data such as prices, transactions and city names, so you can see exactly how these tools get used in practice rather than in abstract puzzles.
Making decisions with if, elif and else
An if statement runs a block of code only when a condition evaluates to True. The condition is any expression that produces a boolean value. The block that follows is marked by indentation, not by brackets, so getting the indentation consistent matters a lot in Python. Four spaces is the common convention, and most editors and notebooks set this up for you automatically.
temp_c = 15
if temp_c < 0:
print('Freezing')
elif temp_c < 15:
print('Cold')
elif temp_c < 25:
print('Mild')
else:
print('Warm')
Python checks each condition in order and runs the first block whose condition is True, then skips the rest entirely. Here temp_c is 15. The first condition, temp_c < 0, is False. The second, temp_c < 15, is also False because 15 is not less than 15. The third, temp_c < 25, is True, so the output is Mild, and the else block is never reached. Only one branch ever runs in an if/elif/else chain, no matter how many elif lines you write.
A second example, closer to data work: turning a numeric score into a letter grade.
score = 78
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'
print(grade)
This prints C. Notice the order of the thresholds matters: because the conditions are checked top to bottom and the first True one wins, you must list them from the highest cutoff to the lowest. If you wrote score >= 60 before score >= 90, every score of 60 or above would stop at the first branch and you would never see an A or a B.
Two small but common mistakes to watch for: using a single equals sign (=) instead of a double one (==) inside a condition, which Python will actually reject with a syntax error for if statements, and forgetting the colon at the end of the if, elif or else line, which raises a syntax error immediately. Both are easy to fix once you know what the error message is pointing at.
What counts as true: truthiness
You do not always need an explicit comparison. Python can treat many values as True or False directly when you put them in a condition. The rule, informally, is that 0, 0.0, an empty string '', an empty list [], an empty dictionary {}, an empty set, and None are all treated as false. Pretty much everything else is treated as true. You can check this yourself with the bool() function.
print(bool(0), bool(1))
print(bool(''), bool('a'))
print(bool([]), bool([1, 2]))
print(bool(None))
This prints False True, then False True, then False True, then False. This behaviour is genuinely useful in data work. CSV files often give you empty strings rather than a proper missing value marker, so a common pattern is to test a field directly rather than comparing it to an empty string explicitly.
row = {'name': 'Nairobi', 'population': ''}
if not row['population']:
print('Missing population value')
Here row['population'] is an empty string, which is falsy, so not row['population'] is True, and the message prints. This is a very common idiom for spotting blank fields before you try to convert them to numbers.
Comparison and boolean operators
The comparison operators you will use constantly are ==, !=, <, >, <= and >=. They work on numbers as you would expect, and they also work on strings, comparing them character by character in alphabetical (technically, character code) order. Comparing values of very different types, such as a number and a list, is not meaningful and Python will usually raise a TypeError if you try it with the ordering operators.
To combine conditions you use the boolean keywords and, or and not, rather than symbols like && or ||. A condition built with and is True only if both sides are True. A condition built with or is True if at least one side is True. Python also short-circuits these: in an and expression, if the first part is False, the second part is never evaluated at all, because the overall result is already decided.
This short-circuiting is not just a performance detail, it is a safety pattern you will use often when working with dictionaries that might be missing a key.
record = {'city': 'Accra'}
if 'price' in record and record['price'] > 100:
print('Expensive')
else:
print('No price info or not expensive')
record has no 'price' key. The first part of the and, 'price' in record, is False, so Python never evaluates record['price'] at all, and you avoid a KeyError that would otherwise crash the program. If you wrote the two checks the other way round, record['price'] > 100 and 'price' in record, Python would try to access record['price'] first and the whole thing would fail before it ever got to check whether the key exists. Order matters when you rely on short-circuiting like this.
Membership and identity: in, not in, and is
The in and not in operators test whether a value appears inside a list, tuple, set, dictionary (where they check the keys) or string (where they check for a substring). From the previous article, remember that membership tests on a set are much faster than on a list once the collection gets large, because a set is built for quick lookups.
allowed_cities = {'Lagos', 'Accra', 'Nairobi', 'Kigali'}
city = 'Cairo'
if city in allowed_cities:
print('Known city')
else:
print('Unknown city')
This prints Unknown city, since 'Cairo' is not one of the four values in the set.
A separate operator, is, checks identity rather than equality: it asks whether two names refer to literally the same object in memory, not just whether they hold equal-looking values. For most everyday comparisons you want ==, but there is one case where is is the correct and idiomatic choice: checking for None. Because None is a single, unique object, you should write x is None rather than x == None.
value = [1, 2, 3]
copy_value = value
other_value = [1, 2, 3]
print(value == other_value)
print(value is other_value)
print(value is copy_value)
a = None
if a is None:
print('a has no value')
value and other_value hold equal contents, so value == other_value is True, but they are two separate list objects, so value is other_value is False. copy_value was assigned directly from value, so it refers to the same object, and value is copy_value is True. The last block prints a has no value, since a is literally the None object.
The for loop: doing something for every item
A for loop runs a block of code once for every item in a collection: a list, tuple, set, dictionary, string, or anything else Python considers iterable. This is the workhorse of everyday data scripting. A common pattern is the accumulator: start a variable at a starting value, then update it on every pass through the loop.
prices = [12.50, 8.75, 20.00, 5.25]
total = 0
for price in prices:
total = total + price
print(total)
This prints 46.5. total starts at 0, and on each pass through the loop it is reassigned to itself plus the next price in the list. By the time the loop finishes, total holds the sum of all four prices. This accumulator pattern, start with a value, update it each time around the loop, shows up constantly: counting matches, building up a running total, collecting results into a new list, and so on.
You can also loop directly over a dictionary. Looping over the dictionary itself gives you the keys; calling .items() gives you key and value pairs together, which is usually what you want.
sales_by_region = {'East': 1200, 'West': 950, 'North': 1600}
for region, amount in sales_by_region.items():
print(region, 'sold', amount)
This prints three lines, one per region, each showing the region name and its sales figure. One detail worth knowing: since Python 3.7, dictionaries keep the order in which keys were inserted, so this loop will print East, West, North in that order, matching how the dictionary was written.
range(): looping a fixed number of times
When you need to repeat something a specific number of times, or need the index positions rather than the values themselves, range() generates a sequence of numbers on demand.
for i in range(5):
print(i)
for i in range(1, 6):
print(i)
for i in range(0, 10, 2):
print(i)
range(5) counts from 0 up to, but not including, 5, so it prints 0, 1, 2, 3, 4. range(1, 6) starts at 1 and stops before 6, printing 1 through 5. range(0, 10, 2) adds a step of 2, printing 0, 2, 4, 6, 8. The end value is always excluded, which trips people up at first, so it is worth remembering: range(n) gives you n values, not n+1.
enumerate(): getting the index and the value together
If you need both the position and the item while looping over a list, enumerate() is cleaner than manually tracking an index with range(len(...)).
columns = ['date', 'category', 'amount']
for index, name in enumerate(columns):
print(index, name)
This prints 0 date, 1 category, 2 amount. enumerate() hands you a running counter alongside each item, which is useful when you need to know a column's position, label rows, or build an index while scanning a list.
zip(): looping over two sequences together
zip() pairs up items from two or more sequences, one from each, so you can loop over them in lockstep.
names = ['Ada', 'Baraka', 'Chidi']
scores = [88, 92, 79]
for name, score in zip(names, scores):
print(name, 'scored', score)
This prints three lines pairing each name with its score. If the two lists are different lengths, zip() stops as soon as the shorter one runs out, without raising an error, which is worth knowing when your two columns are not quite the same length.
The while loop: repeat until a condition changes
A for loop is right when you know you want to visit every item in a collection. A while loop is right when you do not know in advance how many times you will need to repeat something, and instead want to keep going until some condition stops being true. Python checks the condition before every pass through the loop, including the very first one.
expenses = [15, 40, 22, 18, 30, 25]
total = 0
index = 0
budget = 100
while total < budget and index < len(expenses):
total = total + expenses[index]
index = index + 1
print('Stopped after', index, 'expenses, total =', total)
Walking through it: total starts at 0, which is less than the 100 budget, so the loop body runs and adds 15, bringing total to 15 and index to 1. It keeps going, adding 40 (total 55), 22 (total 77), 18 (total 95), and then checks again: 95 is still less than 100, so it adds one more expense, 30, bringing total to 125, before the condition is checked again and finally fails. The output is Stopped after 5 expenses, total = 125. This is worth noticing carefully: because the condition is checked before each step rather than after, the loop can still take one more step that pushes the total past the budget. If you need to guarantee the total never exceeds a limit, you have to check that before adding, not only as the loop condition.
A while loop will run forever if its condition never becomes False, which usually means you forgot to update the variable the condition depends on somewhere inside the loop body. An infinite loop is one of the most common early bugs, and if your notebook or script seems to hang, this is the first thing to check.
Controlling loops: break, continue, and the loop's else
Three extra keywords let you control a loop's flow more precisely. break exits the loop immediately, skipping any remaining items. continue skips the rest of the current pass and moves straight to the next item. Both work in for loops and while loops.
transactions = [45, 60, 120, 30, 15]
threshold = 100
for amount in transactions:
if amount > threshold:
print('Found a large transaction:', amount)
break
else:
print('No transaction above threshold')
This prints Found a large transaction: 120, and then stops looking at the remaining values, because break ends the loop as soon as the first match is found. The else clause attached to the for loop is a feature many people never learn: it runs only if the loop finished naturally, without hitting a break. In this example the else does not run, because break was triggered. If none of the transactions had exceeded the threshold, the loop would have finished on its own and the else block would have printed No transaction above threshold instead. This for/else pattern is a tidy way to express a search that reports whether it found something or not.
continue is useful for skipping invalid or unwanted entries while still processing the rest of a collection.
raw_prices = ['12.50', '', '8.75', '20.00']
total = 0
for value in raw_prices:
if not value:
continue
total = total + float(value)
print(total)
This prints 41.25. When value is the empty string, not value is True, so continue skips straight to the next item without trying to convert it with float(), which would otherwise fail. The three valid entries, 12.50, 8.75 and 20.00, get converted and added up normally.
Nested loops and conditions: working with records
Real data rarely comes as a single flat list; it is more often a list of dictionaries, where each dictionary is one record with several fields, very close to what a row from a CSV file will look like once you start reading files in a later article. Combining loops and conditions lets you summarise this kind of data without needing any external library yet.
transactions = [
{'date': '2024-01-05', 'category': 'groceries', 'amount': 42.10},
{'date': '2024-01-07', 'category': 'transport', 'amount': 15.00},
{'date': '2024-01-10', 'category': 'groceries', 'amount': 30.50},
{'date': '2024-01-12', 'category': 'entertainment', 'amount': -20.00},
{'date': '2024-01-15', 'category': 'transport', 'amount': 12.25},
]
totals_by_category = {}
for record in transactions:
amount = record['amount']
if amount < 0:
continue
category = record['category']
if category in totals_by_category:
totals_by_category[category] = totals_by_category[category] + amount
else:
totals_by_category[category] = amount
print(totals_by_category)
This prints {'groceries': 72.6, 'transport': 27.25}. Here is what happens step by step: the loop visits each transaction dictionary in turn. The entertainment transaction has a negative amount, which this code treats as a refund, so continue skips it before it can be added anywhere. For every other transaction, the code checks whether that category already has a running total in totals_by_category. If it does, it adds the new amount to the existing total; if not, it creates a new entry. By the end, groceries holds 42.10 + 30.50 = 72.60 and transport holds 15.00 + 12.25 = 27.25. This if-inside-a-for pattern, checking whether a key already exists before deciding how to update a dictionary, is extremely common in hand-written data summaries.
A loop inside another loop, a nested loop, lets you compare every item against every other item. This is useful but can get slow on large data, since the number of comparisons grows with the square of the number of items.
cities = ['Lagos', 'Accra', 'Nairobi', 'Accra', 'Kigali']
duplicates = []
for i in range(len(cities)):
for j in range(i + 1, len(cities)):
if cities[i] == cities[j] and cities[i] not in duplicates:
duplicates.append(cities[i])
print(duplicates)
This prints ['Accra']. The outer loop picks a starting position i, and the inner loop checks every position j after it, looking for a repeated value. Starting j at i + 1 avoids comparing a city to itself and avoids checking the same pair twice. The not in duplicates check stops the same duplicate from being added more than once if it appears three or more times. This works correctly, but notice it is doing roughly 5 x 4 / 2 comparisons for a list of only 5 cities; for a list of 10,000 cities, that is tens of millions of comparisons. A set, which you met in the previous article, would solve the same problem far faster, and you will see that approach once comprehensions are introduced in the next part of this path.
Common mistakes to watch for
One mistake deserves a careful example of its own, because it produces wrong answers silently rather than an error: modifying a list while you are looping over it.
numbers = [2, 4, 6, 8]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers)
You might expect this to remove every even number and leave an empty list, but it prints [4, 8]. The reason is that Python's for loop tracks a position internally, and removing an item shifts every later item one slot to the left. After removing 2 (the first item), the list becomes [4, 6, 8], but the loop moves on to the next position, which now holds 6 instead of 4, so 4 gets skipped. The loop effectively leapfrogs over items whenever you remove something while iterating. The fix is to loop over a copy of the list while modifying the original, which you can get with a full slice.
numbers = [2, 4, 6, 8]
for n in numbers[:]:
if n % 2 == 0:
numbers.remove(n)
print(numbers)
numbers[:] creates a new list with the same contents, so the loop iterates over a stable copy while .remove() safely edits the original. This prints [], as expected. The general rule: never add to or remove from a list while a for loop is walking over that exact same list.
- Mixing tabs and spaces, or inconsistent indentation levels, which changes which block a line belongs to without any visible difference on screen
- Writing while True without a break or a condition that is guaranteed to eventually become False, which leaves your script hanging
- Off-by-one errors with range(), forgetting that the stop value is excluded
- Placing conditions in the wrong order inside an and expression, losing the safety that short-circuiting would otherwise give you
- Reaching for a loop with an if and an empty list you keep appending to, when a comprehension (covered in the next part of this path) would say the same thing in one readable line
A note on performance versus clarity
Everything in this article is pure Python, with no external libraries, and that is exactly the point at this stage: understanding how a loop actually walks through data, one item at a time, with a condition deciding what happens to each item, is the foundation for everything that comes later. When you eventually work with pandas or numpy for larger datasets, you will mostly avoid writing explicit for loops over rows, because those libraries offer vectorised operations that apply a condition to an entire column at once and run much faster in compiled code. But vectorised thinking is still built on the same logic you practised here: this is the condition, this is what happens when it is true, this is what happens when it is false. Learning it by hand first makes the vectorised version easier to read and trust later, rather than feeling like unexplained magic.
Summary and what's next
You now have the two tools that turn stored data into decisions and summaries: conditions that branch based on a value, and loops that repeat an action across a collection.
- if, elif and else choose one branch out of several, checked in order, with only the first matching branch running
- Values other than explicit booleans still have truthiness, which is handy for spotting empty strings, empty lists or None
- and, or and not combine conditions, with and and or short-circuiting so the second part is not evaluated unnecessarily
- in and not in test membership in lists, sets, dictionaries and strings; is tests identity and is the right tool specifically for comparing against None
- for loops visit every item in a collection; range(), enumerate() and zip() cover the most common looping patterns over positions, index-value pairs, and parallel sequences
- while loops repeat until a condition changes, checked before every pass, which can let a value overshoot a limit if you are not careful
- break exits a loop early, continue skips to the next item, and a for loop's else clause runs only if the loop was never broken
- Never add to or remove from a list while looping over that same list directly; loop over a copy instead if you need to modify the original
The next article introduces functions, arguments and scope: a way to package the kind of repeated logic you just wrote, like the totals-by-category loop, into a named, reusable block that you can call with different inputs instead of copying and pasting code each time.
Comments (0)
No comments yet. Be the first to share your thoughts.