Python Control Flow: Conditionals, Loops, and Comprehensions
Master Python control flow with if/elif/else, while and for loops, range(), break/continue/pass, nested loops, list/dict comprehensions, generator expressions, and classic algorithm examples.
Python Control Flow: Conditionals, Loops, and Comprehensions
Control flow determines the order in which code executes. Instead of running top to bottom, programs can make decisions, repeat operations, and skip code. Python's control flow is clean and expressive ā once you master it, you can write algorithms that read almost like English.
This guide covers every control flow construct in Python with real-world examples, including three complete algorithm implementations at the end.
1. if / elif / else
The if statement is the fundamental decision-making tool. It evaluates a condition and executes code only when that condition is True.
Basic if
temperature = 35
if temperature > 30:
print("It's hot outside!")
print("Stay hydrated.")
print("Program continues here regardless.")if / else
score = 72
if score >= 60:
print("You passed!")
else:
print("You failed. Please review the material.")if / elif / else
Use elif (else-if) to check multiple conditions in sequence. Python checks each condition in order and executes only the first matching block.
score = 84
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"Your grade is: {grade}") # Your grade is: BWhy the Order Matters
# WRONG: the first condition is too broad
score = 95
if score >= 60:
grade = "D" # This matches 95!
elif score >= 90:
grade = "A" # Never reached for 95
# grade is "D" for a score of 95 ā bug!
# CORRECT: check from most specific to least specific
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 60:
grade = "D"
# grade is "A" for score 95 ā correctNested if Statements
is_logged_in = True
is_admin = False
account_suspended = False
if is_logged_in:
if account_suspended:
print("Your account is suspended.")
elif is_admin:
print("Welcome, administrator!")
else:
print("Welcome back!")
else:
print("Please log in.")One-Line if (Ternary Expression)
# Syntax: value_if_true if condition else value_if_false
age = 20
status = "adult" if age >= 18 else "minor"
print(status) # adult
# Can be nested ā but don't overdo it
score = 75
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "F"
print(grade) # C2. Comparison Operators (Review)
x = 10
# All comparison operators
print(x == 10) # True ā equal
print(x != 5) # True ā not equal
print(x > 5) # True ā greater than
print(x < 20) # True ā less than
print(x >= 10) # True ā greater than or equal
print(x <= 10) # True ā less than or equal
# Chained comparisons
print(1 < x < 100) # True
print(10 <= x <= 10) # True
# String comparison (lexicographic)
print("apple" < "banana") # True
print("Z" < "a") # True (uppercase before lowercase in ASCII)
print("cat" == "cat") # True3. Truthy and Falsy Values
Python's if statement does not require a strict boolean ā it evaluates any expression's truthiness.
What Is Falsy?
# ALL of these are falsy ā if condition will be False
falsy_values = [
False,
0, # integer zero
0.0, # float zero
0j, # complex zero
"", # empty string
[], # empty list
(), # empty tuple
{}, # empty dict
set(), # empty set
None, # None
]
for val in falsy_values:
if not val:
print(f"{repr(val):15} is falsy")What Is Truthy?
# These are all truthy
print(bool(1)) # True ā any non-zero number
print(bool(-1)) # True
print(bool("hello")) # True ā any non-empty string
print(bool(" ")) # True ā whitespace is truthy!
print(bool([0])) # True ā list with one item (even if item is falsy)
print(bool({"key": 0})) # True ā non-empty dictPythonic Use of Truthiness
# Instead of: if len(my_list) > 0:
my_list = [1, 2, 3]
if my_list:
print("List has items")
# Instead of: if name != "":
name = "Alice"
if name:
print(f"Hello, {name}")
# Instead of: if result is not None:
result = some_function_that_might_return_none = lambda: 42
result = result()
if result is not None: # Explicit ā use when None has meaning distinct from 0
print(result)
# Guard clause pattern ā fail fast
def process_order(order):
if not order:
return "No order provided"
if not order.get("items"):
return "Order has no items"
if order.get("total", 0) <= 0:
return "Invalid total"
# Main logic here ā we know order is valid
return f"Processing {len(order['items'])} items"4. while Loops
A while loop executes a block of code repeatedly as long as its condition is True.
Basic while Loop
# Count down
count = 5
while count > 0:
print(count)
count -= 1
print("Blast off!")
# 5 4 3 2 1
# Blast off!Infinite Loop with break
# Input validation loop ā common real-world pattern
while True:
answer = input("Enter 'yes' or 'no': ").strip().lower()
if answer in ("yes", "no"):
break
print("Invalid input. Please try again.")
print(f"You entered: {answer}")while/else
Python's while/else is unusual ā the else block runs when the condition becomes False normally (not via break).
# Search for a user
users = ["alice", "bob", "charlie"]
target = "dave"
index = 0
while index < len(users):
if users[index] == target:
print(f"Found {target} at index {index}")
break
index += 1
else:
print(f"{target} not found in users list")
# Output: dave not found in users listCommon while Loop Patterns
# Reading until a sentinel value
print("Enter numbers (0 to stop):")
total = 0
count = 0
while True:
num = float(input("Number: "))
if num == 0:
break
total += num
count += 1
if count > 0:
print(f"Average: {total / count:.2f}")
# Retry with max attempts
import random
MAX_ATTEMPTS = 3
attempts = 0
while attempts < MAX_ATTEMPTS:
# Simulate a flaky operation
success = random.random() > 0.5
attempts += 1
if success:
print(f"Success on attempt {attempts}")
break
print(f"Attempt {attempts} failed, retrying...")
else:
print(f"Failed after {MAX_ATTEMPTS} attempts")5. for Loops
A for loop iterates over any iterable object ā lists, strings, ranges, tuples, dicts, files, etc.
Iterating Over Common Types
# List
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# String ā iterates over characters
for char in "Python":
print(char, end=" ") # P y t h o n
# Tuple
coordinates = (10, 20)
for coord in coordinates:
print(coord)
# Dict ā iterates over KEYS by default
scores = {"Alice": 95, "Bob": 87, "Charlie": 92}
for name in scores:
print(f"{name}: {scores[name]}")
# Dict items ā key-value pairs
for name, score in scores.items():
print(f"{name}: {score}")
# Dict keys and values explicitly
for key in scores.keys():
print(key)
for value in scores.values():
print(value)enumerate() ā Index and Value Together
fruits = ["apple", "banana", "cherry"]
# Without enumerate ā ugly
for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")
# With enumerate ā Pythonic
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# Start from 1
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherryzip() ā Iterate Two Lists in Parallel
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip stops at the shortest iterable
a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]
for x, y in zip(a, b):
print(x, y)
# 1 a
# 2 b
# 3 c
# (4 and 5 are ignored)
# Unzip ā transpose a list of pairs
pairs = [(1, "a"), (2, "b"), (3, "c")]
numbers, letters = zip(*pairs)
print(list(numbers)) # [1, 2, 3]
print(list(letters)) # ['a', 'b', 'c']for/else
Like while/else, the else block runs if the loop completes without a break.
# Find a prime factor
def smallest_factor(n):
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
print(f"{n} is divisible by {i}")
break
else:
print(f"{n} is prime")
smallest_factor(17) # 17 is prime
smallest_factor(15) # 15 is divisible by 36. range()
range() generates a sequence of integers. It is lazy ā it does not create a list in memory.
# range(stop)
for i in range(5):
print(i) # 0 1 2 3 4
# range(start, stop)
for i in range(2, 7):
print(i) # 2 3 4 5 6
# range(start, stop, step)
for i in range(0, 20, 4):
print(i) # 0 4 8 12 16
# Counting backwards
for i in range(10, 0, -1):
print(i, end=" ") # 10 9 8 7 6 5 4 3 2 1
# Converting to list
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(1, 10, 2))) # [1, 3, 5, 7, 9]
# Range is memory-efficient
import sys
r = range(1_000_000)
lst = list(range(1_000_000))
print(sys.getsizeof(r)) # 48 bytes
print(sys.getsizeof(lst)) # ~8,448,728 bytes
# range supports len(), in, indexing
r = range(10)
print(len(r)) # 10
print(5 in r) # True
print(r[3]) # 3
print(r[-1]) # 9
print(r[2:7]) # range(2, 7)7. break, continue, and pass
break ā Exit the Loop Immediately
# Find first even number
numbers = [3, 7, 2, 9, 4, 6]
for n in numbers:
if n % 2 == 0:
print(f"First even number: {n}")
break
# First even number: 2
# break in nested loops only exits the innermost loop
for i in range(3):
for j in range(3):
if j == 1:
break # only breaks inner loop
print(f"({i},{j})", end=" ")
# (0,0) (1,0) (2,0)continue ā Skip to Next Iteration
# Skip even numbers
for n in range(10):
if n % 2 == 0:
continue # skip even numbers
print(n, end=" ")
# 1 3 5 7 9
# Process only valid data
data = [1, None, 3, None, 5, 6]
total = 0
for item in data:
if item is None:
continue # skip None values
total += item
print(f"Sum: {total}") # Sum: 15pass ā Do Nothing (Placeholder)
pass is a no-op statement. It is used when syntax requires a block but you have nothing to write yet.
# Placeholder for future code
def process_payment():
pass # TODO: implement payment processing
class DatabaseConnection:
pass # TODO: implement class
# In a try/except ā silently ignore an error
try:
risky_operation = int("not a number")
except ValueError:
pass # intentionally ignore
# In a loop ā skip with no side effects
for i in range(10):
if i % 2 == 0:
pass # could add handling later
else:
print(i, end=" ")
# 1 3 5 7 98. Nested Loops
Nested loops are loops inside loops. They are essential for working with 2D data like grids and matrices.
# Multiplication table
print("Multiplication Table (1-5):")
print(" ", end="")
for j in range(1, 6):
print(f"{j:4}", end="")
print()
for i in range(1, 6):
print(f"{i:4}", end="")
for j in range(1, 6):
print(f"{i*j:4}", end="")
print()
# Output:
# 1 2 3 4 5
# 1 1 2 3 4 5
# 2 2 4 6 8 10
# 3 3 6 9 12 15
# 4 4 8 12 16 20
# 5 5 10 15 20 25# 2D grid traversal
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Sum all elements
total = 0
for row in grid:
for cell in row:
total += cell
print(f"Total: {total}") # 45
# Find a value
target = 6
found = False
for i, row in enumerate(grid):
for j, cell in enumerate(row):
if cell == target:
print(f"Found {target} at row {i}, col {j}")
found = True
break
if found:
break9. List Comprehensions
List comprehensions are a concise, Pythonic way to create lists. They are often faster than equivalent for loops because they are optimized in CPython.
Basic Syntax
# Syntax: [expression for item in iterable if condition]
# Traditional loop
squares = []
for n in range(10):
squares.append(n ** 2)
# List comprehension
squares = [n ** 2 for n in range(10)]
print(squares)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]With Filtering
# Only even squares
even_squares = [n ** 2 for n in range(10) if n % 2 == 0]
print(even_squares)
# [0, 4, 16, 36, 64]
# Strings: filter and transform
words = ["hello", "world", "python", "is", "great"]
long_words_upper = [w.upper() for w in words if len(w) > 4]
print(long_words_upper)
# ['HELLO', 'WORLD', 'PYTHON', 'GREAT']Nested List Comprehensions
# Flatten a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
print(flat)
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Cartesian product
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
variants = [(color, size) for color in colors for size in sizes]
print(variants)
# [('red', 'S'), ('red', 'M'), ('red', 'L'), ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]
# Matrix transpose
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]When NOT to Use List Comprehensions
# If the logic is complex, use a regular loop for readability
# Hard to read:
result = [do_complex_transform(x) for x in data if predicate(x) and another_predicate(x)]
# Better as a loop:
result = []
for x in data:
if predicate(x) and another_predicate(x):
transformed = do_complex_transform(x)
result.append(transformed)
# Never use a comprehension just for side effects!
# BAD:
[print(x) for x in range(5)] # uses comprehension for side effect
# GOOD:
for x in range(5):
print(x)10. Dictionary Comprehensions
Same idea as list comprehensions, but creates dictionaries.
# Basic dict comprehension
# Syntax: {key_expr: value_expr for item in iterable if condition}
# Squares as a dict
squares = {n: n**2 for n in range(6)}
print(squares)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Invert a dictionary
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted)
# {1: 'a', 2: 'b', 3: 'c'}
# Transform values
prices = {"apple": 1.20, "banana": 0.50, "cherry": 3.00}
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted)
# {'apple': 1.08, 'banana': 0.45, 'cherry': 2.7}
# Filter and transform
students = {"Alice": 92, "Bob": 45, "Charlie": 78, "Dave": 55}
passing = {name: score for name, score in students.items() if score >= 60}
print(passing)
# {'Alice': 92, 'Charlie': 78}
# Build a lookup from a list
words = ["hello", "world", "python"]
word_lengths = {word: len(word) for word in words}
print(word_lengths)
# {'hello': 5, 'world': 5, 'python': 6}11. Generator Expressions
Generator expressions are like list comprehensions but they are lazy ā they compute values one at a time and do not store everything in memory.
# List comprehension ā creates the full list in memory
squares_list = [n**2 for n in range(1_000_000)] # 8+ MB
# Generator expression ā computes on demand
squares_gen = (n**2 for n in range(1_000_000)) # tiny footprint
import sys
print(sys.getsizeof([n**2 for n in range(1000)])) # ~8056 bytes
print(sys.getsizeof(n**2 for n in range(1000))) # 104 bytes
# Use generators when you only need to iterate once
total = sum(n**2 for n in range(1000))
print(total) # 332833500
# Find first match in a large sequence
first_big = next((n for n in range(1_000_000) if n**2 > 1000), None)
print(first_big) # 32
# Generators are exhausted after one pass
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] ā exhausted!12. Real Example: FizzBuzz
FizzBuzz is a classic programming interview exercise. Print numbers 1-100, but:
- Print "Fizz" if divisible by 3
- Print "Buzz" if divisible by 5
- Print "FizzBuzz" if divisible by both
def fizzbuzz(n):
"""Classic FizzBuzz up to n."""
for i in range(1, n + 1):
# Check 15 (3*5) FIRST, then 3, then 5
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
fizzbuzz(20)
# FizzBuzz using string concatenation (elegant variant)
def fizzbuzz_v2(n):
for i in range(1, n + 1):
result = ""
if i % 3 == 0:
result += "Fizz"
if i % 5 == 0:
result += "Buzz"
print(result or i)
# FizzBuzz as a list comprehension (concise)
def fizzbuzz_v3(n):
return [
"FizzBuzz" if i % 15 == 0
else "Fizz" if i % 3 == 0
else "Buzz" if i % 5 == 0
else i
for i in range(1, n + 1)
]
result = fizzbuzz_v3(20)
print(result)
# [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19, 'Buzz']13. Real Example: Number Guessing Game
import random
def number_guessing_game():
"""
A complete number guessing game demonstrating:
- while loops
- break/continue
- if/elif/else
- type conversion and input validation
"""
SECRET = random.randint(1, 100)
MAX_GUESSES = 7
guesses_taken = 0
print("=" * 40)
print(" NUMBER GUESSING GAME")
print(f" I'm thinking of a number 1-100.")
print(f" You have {MAX_GUESSES} guesses.")
print("=" * 40)
while guesses_taken < MAX_GUESSES:
remaining = MAX_GUESSES - guesses_taken
# Input validation loop
while True:
raw = input(f"\nGuess ({remaining} remaining): ").strip()
try:
guess = int(raw)
if 1 <= guess <= 100:
break
else:
print("Please enter a number between 1 and 100.")
except ValueError:
print(f"'{raw}' is not a valid number.")
guesses_taken += 1
if guess == SECRET:
print(f"\nā Correct! The number was {SECRET}.")
print(f" You got it in {guesses_taken} guess{'es' if guesses_taken > 1 else ''}!")
break
elif guess < SECRET:
diff = SECRET - guess
if diff > 20:
print("Too low ā way off!")
elif diff > 5:
print("Too low ā getting warmer.")
else:
print("Too low ā very close!")
else:
diff = guess - SECRET
if diff > 20:
print("Too high ā way off!")
elif diff > 5:
print("Too high ā getting warmer.")
else:
print("Too high ā very close!")
else:
print(f"\nā Out of guesses! The number was {SECRET}.")
return guesses_taken
# Simulate a game (no real input)
# number_guessing_game()
# Demonstrate the logic with a mock
def simulate_game(secret, guesses):
"""Simulate a guessing game without interactive input."""
print(f"Secret: {secret}")
for guess in guesses:
if guess == secret:
print(f"Guess {guess}: Correct!")
return True
elif guess < secret:
print(f"Guess {guess}: Too low")
else:
print(f"Guess {guess}: Too high")
print("Out of guesses!")
return False
simulate_game(42, [50, 25, 37, 44, 41, 43, 42])14. Real Example: Prime Sieve (Sieve of Eratosthenes)
Finding all primes up to n using the classic algorithm:
def sieve_of_eratosthenes(limit):
"""
Find all prime numbers up to 'limit' using the Sieve of Eratosthenes.
Algorithm:
1. Start with all numbers 2..limit marked as prime
2. For each prime p, mark all multiples of p (starting from p*p) as not prime
3. Repeat until p*p > limit
"""
if limit < 2:
return []
# Boolean array: is_prime[i] = True means i is prime
is_prime = [True] * (limit + 1)
is_prime[0] = False
is_prime[1] = False
# Only need to check up to sqrt(limit)
p = 2
while p * p <= limit:
if is_prime[p]:
# Mark all multiples of p as not prime
# Start from p*p (smaller multiples already marked)
for multiple in range(p * p, limit + 1, p):
is_prime[multiple] = False
p += 1
# Collect primes
primes = [n for n in range(2, limit + 1) if is_prime[n]]
return primes
# Test it
primes_100 = sieve_of_eratosthenes(100)
print(f"Primes up to 100: {primes_100}")
print(f"Count: {len(primes_100)}")
# 25 primes up to 100
primes_1000 = sieve_of_eratosthenes(1000)
print(f"Primes up to 1000: {len(primes_1000)} primes")
# Verify: check if specific numbers are prime
def is_prime_check(n, primes_list):
return n in primes_list
test_numbers = [2, 3, 4, 17, 25, 97, 100]
primes = set(sieve_of_eratosthenes(100))
for n in test_numbers:
result = n in primes
print(f"{n:3d} is {'prime' if result else 'not prime'}")Alternative: Simple Primality Test
def is_prime(n):
"""
Check if n is prime using trial division.
Time complexity: O(sqrt(n))
"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# Only check odd divisors up to sqrt(n)
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2 # skip even numbers
return True
# Test
for n in range(2, 30):
if is_prime(n):
print(n, end=" ")
# 2 3 5 7 11 13 17 19 23 29
# List comprehension with is_prime
primes = [n for n in range(2, 100) if is_prime(n)]
print(primes)15. Performance Comparison
Understanding when to use each construct:
import timeit
# Comparing sum approaches
n = 10000
# for loop with append
def sum_loop():
result = []
for i in range(n):
result.append(i * i)
return sum(result)
# list comprehension
def sum_comprehension():
return sum([i * i for i in range(n)])
# generator expression
def sum_generator():
return sum(i * i for i in range(n))
# Time each
loop_time = timeit.timeit(sum_loop, number=1000)
comp_time = timeit.timeit(sum_comprehension, number=1000)
gen_time = timeit.timeit(sum_generator, number=1000)
print(f"Loop: {loop_time:.3f}s")
print(f"Comprehension: {comp_time:.3f}s")
print(f"Generator: {gen_time:.3f}s")
# Typical results: comprehension ~2x faster than loop,
# generator slightly slower than comprehension but uses less memoryKey Takeaways
- elif, not else if ā Python uses
elif, notelse if. Usingelse ifcreates a nestedelseblock, which is a bug. - Truthy/falsy simplifies conditions ā
if my_list:is cleaner thanif len(my_list) > 0:. - for/else and while/else are unique to Python ā the
elseblock runs only when nobreakwas hit. - break vs continue ā
breakexits the loop entirely;continueskips to the next iteration. - pass is a placeholder ā use it when syntax requires a block but you have no code yet.
- range() is lazy ā it does not create a list; it generates numbers on demand.
- List comprehensions are Pythonic ā but use them only when the logic is simple and readable.
- Generator expressions save memory ā use
(expr for x in iter)when you only need to iterate once. - Dict comprehensions ā
{k: v for k, v in ...}is the clean way to build dictionaries. - Nested loops are O(n²) ā be aware of performance with large datasets.
Common Mistakes
# Mistake 1: Modifying a list while iterating over it
numbers = [1, 2, 3, 4, 5, 6]
for n in numbers:
if n % 2 == 0:
numbers.remove(n) # BUG: skips elements!
print(numbers) # [1, 3, 5] ā BUT only by accident
# Correct: iterate over a copy or use comprehension
numbers = [1, 2, 3, 4, 5, 6]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers) # [1, 3, 5]
# Mistake 2: Off-by-one in range
# Want 1 to 10 inclusive
for i in range(1, 10): # BUG: goes to 9
pass
for i in range(1, 11): # Correct: goes to 10
pass
# Mistake 3: Using == instead of 'is' for None
result = None
if result == None: # Works but not Pythonic
pass
if result is None: # Correct
pass
# Mistake 4: Forgetting that dict iteration is over keys
scores = {"Alice": 95, "Bob": 87}
for item in scores:
print(item) # prints keys: Alice, Bob (not values!)
# Correct:
for name, score in scores.items():
print(f"{name}: {score}")
# Mistake 5: Generator used twice
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] ā already exhausted!
# Use list if you need to iterate multiple timesEnjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.