List Comprehensions
Imagine you have a list of numbers and want to get only the even ones, doubled. With traditional code, you write a loop, an if statement, and an append. With list comprehensions, you do it in one readable line! It is Python's way of saying "transform this collection elegantly."
List Comprehension Anatomy
A list comprehension follows this pattern: [expression for item in iterable if condition]. Think of it as a compact factory: the iterable feeds items, the condition filters them, and the expression transforms them into the output.
Breaking it down:
- expression: What to do with each item (the output) - like
x * 2orx.upper() - for item in iterable: The source data to loop through - like a list, range, or string
- if condition: (Optional) Filter to include only items that pass this test
Visual: List Comprehension Anatomy
x * 2
"Double each value"for x in numbers
"Loop through list"if x % 2 == 0
"Keep only evens"for x in numbers
3 if x % 2 == 0
1 x * 2
Basic List Comprehension
# Traditional approach: 4 lines
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens_doubled = []
for x in numbers:
if x % 2 == 0:
evens_doubled.append(x * 2)
print(evens_doubled) # Output: [4, 8, 12, 16, 20]
# List comprehension: 1 line!
evens_doubled = [x * 2 for x in numbers if x % 2 == 0]
print(evens_doubled) # Output: [4, 8, 12, 16, 20]
Step-by-step comparison:
Traditional approach (4 lines):
- Create an empty list
evens_doubled = [] - Loop through each number with
for x in numbers - Check if even with
if x % 2 == 0 - Transform and add with
evens_doubled.append(x * 2)
Comprehension approach (1 line):
- x * 2: The expression that transforms each item (doubles it)
- for x in numbers: Iterates through the source list
- if x % 2 == 0: Filters to keep only even numbers
- Result: A new list with transformed, filtered values
for x in numbers (what we are looping through), then if x % 2 == 0 (what we are filtering), then x * 2 (what we are outputting).
Nested Comprehensions
# Flatten a 2D list (matrix) into 1D
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Create multiplication table
mult_table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(mult_table) # Output: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
Reading nested comprehensions (they can be confusing!):
Flattening a 2D list: [num for row in matrix for num in row]
- Read left-to-right like nested loops: "for each row in matrix, then for each num in that row"
- The outer
for row in matrixruns first, giving us [1,2,3], then [4,5,6], etc. - The inner
for num in rowunpacks each row into individual numbers num(the expression at the start) is what gets added to our output list
Creating a 2D list: [[i*j for j in range(1,4)] for i in range(1,4)]
- The outer comprehension creates rows (controlled by
i) - Each inner comprehension creates a row with values (controlled by
j) - Result: A list of lists - our multiplication table!
Dictionary and Set Comprehensions
# Dictionary comprehension: {key: value for item in iterable}
words = ['apple', 'banana', 'cherry']
word_lengths = {word: len(word) for word in words}
print(word_lengths) # Output: {'apple': 5, 'banana': 6, 'cherry': 6}
# Set comprehension: {expression for item in iterable}
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique_squares = {x ** 2 for x in numbers}
print(unique_squares) # Output: {1, 4, 9, 16}
Same pattern, different brackets - here is how to remember:
| Type | Syntax | Result | Example |
|---|---|---|---|
| List | [ ] | Ordered, allows duplicates | [x*2 for x in nums] |
| Dict | { key: value } | Key-value pairs | {x: x*2 for x in nums} |
| Set | { } | Unordered, no duplicates | {x*2 for x in nums} |
| Generator | ( ) | Lazy iterator | (x*2 for x in nums) |
Dictionary comprehension breakdown:
{word: len(word) for word in words} means "for each word, create a key (the word) and a value (its length)"
Set comprehension breakdown:
{x ** 2 for x in numbers} means "square each number, and automatically remove duplicates" (notice 2, 3, 4 appear multiple times but their squares only appear once)
Common Mistakes to Avoid
Side effects in comprehensions
# BAD: Appending inside comprehension
result = []
[result.append(x) for x in data] # Creates list of None!
# GOOD: Just use a regular loop
for x in data:
result.append(x)
Comprehensions are for creating new lists, not for side effects.
Too complex comprehensions
# BAD: Hard to read!
[[x*y for y in range(1,5) if y%2==0] for x in range(1,10) if x%3==0]
# GOOD: Break it down
result = []
for x in range(1, 10):
if x % 3 == 0:
row = [x*y for y in range(1,5) if y%2==0]
result.append(row)
If it needs a comment to explain, use regular loops.
Forgetting generator exhaustion
# BAD: Generator already consumed!
gen = (x*2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] - Empty! Already used!
# GOOD: Recreate or use list
nums = [x*2 for x in range(5)] # Use list if need multiple iterations
Generators can only be iterated once.
Walrus in wrong context
# BAD: Assignment where expression expected
x := 5 # SyntaxError!
# GOOD: Use in context that expects expression
if (x := 5) > 3: # OK - in if condition
print(x)
y = (x := 10) # OK - parenthesized
Walrus needs context: if, while, comprehensions, etc.
Practice Makes Perfect
These exercises progress from simple to challenging. Here is how to get the most out of them:
- Try first without looking at hints - struggle is where learning happens
- Compare your solution to the expected output - make sure they match
- If stuck, reveal the hint - then try again before looking at the answer
- After solving, try variations - what if the input was different?
Tip: Write your code in VS Code or Python IDLE, not just in your head. Running the code yourself is essential!
Practice: Comprehensions
Task: Given a list of words, create a new list containing only words longer than 3 characters, converted to uppercase.
Show Solution
# Input list
words = ['hi', 'hello', 'hey', 'python', 'go', 'javascript']
# Filter words > 3 chars, convert to uppercase
result = [word.upper() for word in words if len(word) > 3]
# result iterates each word
# if len(word) > 3 filters short words
# word.upper() transforms to uppercase
print(result) # Output: ['HELLO', 'PYTHON', 'JAVASCRIPT']
Task: Combine a list of names and a list of ages into a dictionary using dict comprehension.
Show Solution
# Two parallel lists
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
# Combine using zip() and dict comprehension
person_ages = {name: age for name, age in zip(names, ages)}
# zip() pairs names and ages together
# name: age creates key-value pairs
print(person_ages)
# Output: {'Alice': 25, 'Bob': 30, 'Charlie': 35}
Task: Convert a list of [key, value] pairs into a dictionary, but only include pairs where the value is positive.
Show Solution
# List of [key, value] pairs
data = [['a', 10], ['b', -5], ['c', 20], ['d', -3], ['e', 15]]
# Dict comprehension with filter
result = {k: v for k, v in data if v > 0}
# k, v unpacks each [key, value] pair
# if v > 0 filters out negative values
# k: v creates the dict entry
print(result) # Output: {'a': 10, 'c': 20, 'e': 15}
Task: Transpose a matrix (swap rows and columns) using nested list comprehension.
Show Solution
# Original matrix (3 rows x 4 columns)
matrix = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
# Transpose using nested comprehension
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
# Outer: for i in range(4) - iterate column indices
# Inner: for row in matrix - collect that column from each row
# row[i] - get element at column i
for row in transposed:
print(row)
# Output: [1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]
Generator Expressions
What if you have a billion numbers and want to process them one at a time without loading all of them into memory? Generators are lazy iterators that produce values on-demand. Instead of creating the entire list upfront, they generate each item only when you ask for it. Perfect for big data!
What is a Generator?
A generator is a special kind of iterator that computes values on-the-fly instead of storing them all in memory. Think of it like a vending machine - it only dispenses one item at a time when you request it, rather than dumping everything out at once.
Two ways to create generators:
- Generator expressions: Like list comprehensions but with
()instead of[] - Generator functions: Functions that use
yieldinstead ofreturn
Visual: List vs Generator Memory Usage
List Comprehension
[x for x in range(1000000)]
Memory: ~8 MB (all items stored)
Generator Expression
(x for x in range(1000000))
Memory: ~120 bytes (one item at a time)
Generator Expressions
# List comprehension: Creates entire list in memory
squares_list = [x ** 2 for x in range(1000000)]
print(type(squares_list)) #
# Generator expression: Creates generator object (lazy)
squares_gen = (x ** 2 for x in range(1000000))
print(type(squares_gen)) #
# Generators are iterable - use in loops or convert to list
for i, sq in enumerate(squares_gen):
if i >= 5: break
print(sq) # Output: 0, 1, 4, 9, 16
Understanding lazy vs eager evaluation:
| Aspect | List Comprehension [ ] | Generator Expression ( ) |
|---|---|---|
| Memory | Stores ALL values at once | Stores ONE value at a time |
| Speed | Faster for small data (already in memory) | Faster for huge data (no memory pressure) |
| Reusability | Can iterate multiple times | Can only iterate ONCE (then exhausted) |
| Type | Returns a list object | Returns a generator object |
When to use generators:
- Processing large files line by line (do not load entire file into memory)
- Streaming data from APIs or databases
- Creating infinite sequences (like Fibonacci numbers)
- Any time you only need each value once
Generator Functions with yield
# Generator function using yield
def countdown(n):
print("Starting countdown!")
while n > 0:
yield n # Pauses here, returns n
n -= 1 # Resumes here on next iteration
print("Blastoff!")
# Create generator object
counter = countdown(5)
print(next(counter)) # Output: Starting countdown! 5
print(next(counter)) # Output: 4
print(next(counter)) # Output: 3
How yield works (step by step):
counter = countdown(5)- Creates a generator object, but the function does NOT run yet!next(counter)- NOW the function runs until it hitsyield n- At
yield n, the function PAUSES and returns the value (5) - The function's state (variables, position) is saved
- Next
next(counter)call RESUMES from exactly where it paused - It continues from
n -= 1, loops back, and yields the next value (4) - This continues until the function ends (reaches Blastoff!)
yield vs return:
- return: Ends the function completely. All local variables are lost.
- yield: Pauses the function, saves everything, and can resume later.
Practice: Generators
Task: Use a generator expression to sum the squares of numbers from 1 to 100.
Show Solution
# Generator expression inside sum()
total = sum(x ** 2 for x in range(1, 101))
# No need for extra parentheses when generator is only argument
# Computes each square on-demand, never stores 100 values
print(f"Sum of squares 1-100: {total}")
# Output: Sum of squares 1-100: 338350
Task: Create a generator function that yields Fibonacci numbers indefinitely.
Show Solution
def fibonacci():
a, b = 0, 1 # Initialize first two numbers
while True: # Infinite generator
yield a # Yield current number
a, b = b, a + b # Compute next pair
# Get first 10 Fibonacci numbers
fib = fibonacci()
first_ten = [next(fib) for _ in range(10)]
# next() gets one value at a time
# Generator never runs out (infinite loop)
print(first_ten) # Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Task: Create a generator that reads a file and yields non-empty lines (stripped of whitespace).
Show Solution
def read_lines(filepath):
with open(filepath, 'r') as file:
for line in file: # Files are iterators too!
stripped = line.strip()
if stripped: # Skip empty lines
yield stripped # Yield cleaned line
# Usage example (memory-efficient for huge files)
# for line in read_lines('huge_file.txt'):
# process(line) # Only one line in memory at a time
Task: Create a pipeline of generators: generate numbers, filter evens, square them, take first 5.
Show Solution
def numbers(n):
for i in range(1, n + 1):
yield i
def filter_evens(gen):
for x in gen:
if x % 2 == 0:
yield x
def square(gen):
for x in gen:
yield x ** 2
# Chain generators together (lazy pipeline)
pipeline = square(filter_evens(numbers(100)))
# Nothing computed yet - all lazy!
result = [next(pipeline) for _ in range(5)]
# Only computes as many values as needed
print(result) # Output: [4, 16, 36, 64, 100]
Real-World Use Cases: When Generators Shine
Reading Large Log Files
def read_errors(logfile):
"""Read only ERROR lines from huge log file"""
with open(logfile) as f:
for line in f: # File is iterator!
if 'ERROR' in line:
yield line.strip()
# Process millions of lines without loading all into memory
for error in read_errors('server.log'):
print(error)
The file is never fully loaded - each line is processed then discarded.
Streaming API Data
def fetch_pages(api_url):
"""Fetch paginated API results lazily"""
page = 1
while True:
response = requests.get(f"{api_url}?page={page}")
data = response.json()
if not data['results']:
break
yield from data['results'] # yield each item
page += 1
# Stop early if we find what we need
for user in fetch_pages('/api/users'):
if user['name'] == 'Alice':
break # No more pages fetched!
Pages are fetched only as needed - early exit stops fetching.
Infinite Sequences
def prime_numbers():
"""Generate primes forever"""
n = 2
while True:
if all(n % i != 0 for i in range(2, int(n**0.5)+1)):
yield n
n += 1
# Get first 10 primes
primes = prime_numbers()
first_10 = [next(primes) for _ in range(10)]
print(first_10) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Cannot create a "list of all primes" - but a generator works perfectly!
Data Transformation Pipelines
def clean(data):
for item in data:
yield item.strip().lower()
def filter_valid(data):
for item in data:
if item and not item.startswith('#'):
yield item
# Chain transformations - each is lazy!
with open('data.txt') as f:
pipeline = filter_valid(clean(f))
for line in pipeline:
process(line)
Each stage processes one item at a time - memory efficient!
Walrus Operator (:=)
Ever wanted to assign a value and use it in the same expression? The walrus operator (:=) lets you do exactly that! Introduced in Python 3.8, it is called "walrus" because := looks like a walrus face turned sideways. It helps avoid redundant calculations and makes certain patterns more elegant.
The Walrus Operator (:=)
The walrus operator := (officially called the "assignment expression") lets you assign a value to a variable AND use that value in the same expression. The pattern is: (variable := expression)
Why it is useful:
- Avoid repeated calculations: Compute once, use multiple times in the same statement
- Cleaner while loops: Assign and check in one line
- Better comprehensions: Store intermediate results for both filtering and output
Visual: Walrus Operator Patterns
Without Walrus
n = len(data)
if n > 10:
print(f"Got {n} items")
With Walrus
if (n := len(data)) > 10:
print(f"Got {n} items")
Basic Usage
# Without walrus: compute twice or use extra line
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Bad: computes len() twice
if len(data) > 5:
print(f"List has {len(data)} items") # Redundant call
# Better with walrus: assign and use in one expression
if (n := len(data)) > 5:
print(f"List has {n} items") # n is available here
The walrus pattern explained: (variable := expression)
How it works:
- The expression on the right (
len(data)) is evaluated - The result is assigned to the variable on the left (
n) - The SAME result is also returned, so it can be used in the
ifcondition - The variable
nis now available in the if block (and after!)
Why parentheses are needed:
Without parentheses, Python might parse it incorrectly:
if n := len(data) > 5would be parsed asif n := (len(data) > 5)- n would be True/False!if (n := len(data)) > 5correctly assigns the length to n, THEN compares
:= looks like a walrus with tusks turned sideways! ðŸ¦
Common Use Cases
# Use case 1: While loops with assignment
while (line := input("Enter text (q to quit): ")) != "q":
print(f"You entered: {line}")
# Use case 2: List comprehensions with expensive operations
import re
text = "Email: test@example.com and info@site.org"
# Only include matches that exist
emails = [m.group() for s in [text] if (m := re.search(r'\S+@\S+', s))]
# Use case 3: Avoid repeated function calls
data = [5, 12, 3, 18, 7, 25]
filtered = [y for x in data if (y := x * 2) > 10]
print(filtered) # Output: [24, 36, 14, 50]
Understanding Each Use Case
While Loops
Without walrus:
line = input()
while line != "q":
...
line = input()
Duplicate input call!
With walrus:
while (line := input()) != "q":
Assign AND check in one expression
Expensive Operations
When you need to compute, filter, AND include the result:
[y for x in data if (y := expensive(x)) > threshold]
expensive(x) twice!
Intermediate Results
[y for x in data if (y := x * 2) > 10]
- Compute
x * 2 - Store in
y - Filter by
y > 10 - Output
y
Practice: Walrus Operator
Task: Rewrite this code using the walrus operator to avoid calling len() twice.
# Original code
items = [1, 2, 3, 4, 5]
if len(items) > 3:
print(f"Too many: {len(items)} items")
Show Solution
# Using walrus operator
items = [1, 2, 3, 4, 5]
if (n := len(items)) > 3:
print(f"Too many: {n} items")
# len() is computed once, stored in n
# n is used both in condition and print
# Output: Too many: 5 items
Task: Create a list of squared values from numbers 1-10, but only include squares greater than 20.
Show Solution
# Using walrus to avoid computing square twice
result = [sq for x in range(1, 11) if (sq := x ** 2) > 20]
# sq := x ** 2 computes and stores the square
# if sq > 20 filters using the stored value
# sq is used as the output (no recomputation)
print(result) # Output: [25, 36, 49, 64, 81, 100]
Task: Extract all numbers from a string, but only include those greater than 50.
Show Solution
import re
text = "Scores: 45, 82, 33, 91, 67, 12, 55"
# Find all numbers, filter those > 50
numbers = [
num for match in re.finditer(r'\d+', text)
if (num := int(match.group())) > 50
]
# match.group() gets the string match
# int() converts to number and stores in num
# Filter and output use the same num value
print(numbers) # Output: [82, 91, 67, 55]
Loop Control Statements
Sometimes you need to break out of a loop early, skip certain iterations, or know if a loop completed without interruption. Python provides break, continue, pass, and the unique else clause on loops for fine-grained control over iteration.
Loop Control Statements
Python gives you three keywords to control loop execution, plus a unique else clause:
break - Exit immediately
Completely exits the loop. No more iterations. Code continues after the loop.
continue - Skip to next
Skips the rest of this iteration. Loop continues with the next item.
pass - Do nothing
A placeholder that does nothing. Used when syntax requires a statement.
else - No break occurred
Runs only if the loop completed all iterations without hitting break.
Break, Continue, and Pass
# break: Exit the loop immediately
for i in range(10):
if i == 5:
break # Exit when i is 5
print(i, end=" ") # Output: 0 1 2 3 4
# continue: Skip to next iteration
for i in range(10):
if i % 2 == 0:
continue # Skip even numbers
print(i, end=" ") # Output: 1 3 5 7 9
# pass: Do nothing (placeholder)
for i in range(5):
if i == 2:
pass # Placeholder - does nothing
print(i, end=" ") # Output: 0 1 2 3 4
Understanding each control statement:
break example trace:
i=0: print 0, i=1: print 1, i=2: print 2, i=3: print 3, i=4: print 4, i=5: BREAK! Loop exits immediately. Numbers 6-9 never run.
continue example trace:
i=0: even, CONTINUE (skip print), i=1: odd, print 1, i=2: even, CONTINUE, i=3: odd, print 3... etc.
The loop runs all 10 times, but print only happens for odd numbers.
pass example trace:
i=0: print 0, i=1: print 1, i=2: pass (nothing happens), print 2, i=3: print 3, i=4: print 4
pass is a placeholder - Python requires something in the if block, so we use pass to say "do nothing here"
pass prevents syntax errors.
The Loop else Clause
# else runs when loop completes WITHOUT break
def find_prime(numbers):
for n in numbers:
if n < 2:
continue
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
break # Not prime, exit inner loop
else:
return n # else runs = no break = prime found
return None
print(find_prime([4, 6, 8, 9, 11, 12])) # Output: 11
The for-else pattern explained (often misunderstood!):
Think of else on a loop as a "no-break" clause. It runs only if the loop finishes naturally without hitting a break.
How the prime finder works:
is_prime(n)checks if a number is prime by testing divisibility- If any divisor is found,
breakexits the loop andelseis SKIPPED - If NO divisor is found, loop finishes naturally and
elseruns → returns True
Trace with is_prime(11):
i=2: 11%2≠0, i=3: 11%3≠0, i=4: 11%4≠0... no break hits → else runs → returns True
Trace with is_prime(12):
i=2: 12%2=0 → break! → else SKIPPED → returns False (after the loop)
Nested Loop Control
# Break only exits the innermost loop
found = False
for i in range(5):
for j in range(5):
if i * j == 12:
print(f"Found at ({i}, {j})")
found = True
break # Only exits inner loop
if found:
break # Need this to exit outer loop
# Alternative: Use a function with return
def find_product(target):
for i in range(10):
for j in range(10):
if i * j == target:
return (i, j) # Exits both loops
return None
print(find_product(12)) # Output: (2, 6)
Breaking out of nested loops - three approaches:
1. Flag variable (shown first):
- Set
found = Truein inner loop, then check it in outer loop - Works but adds clutter - you need to check the flag after every inner loop
2. Function + return (cleanest - shown second):
- Wrap the nested loops in a function
returnimmediately exits the entire function, breaking all loops at once- This is the recommended approach - clean and Pythonic
3. Exception (not shown - not recommended):
- Raise a custom exception in inner loop, catch it outside both loops
- Works but goes against the principle "exceptions for exceptional cases, not control flow"
return. It is cleaner and makes your intent obvious.
Real-World Use Cases: Loop Control in Action
Search with Early Exit
def find_user(users, email):
"""Find user by email - stop when found"""
for user in users:
if user['email'] == email:
return user # Early exit!
return None
# Or with for-else:
for user in users:
if user['email'] == target:
print(f"Found: {user['name']}")
break
else:
print("User not found")
No need to scan entire list once target is found.
Input Validation Loop
def get_positive_number():
"""Keep asking until valid input"""
while True:
try:
n = int(input("Enter positive number: "))
if n <= 0:
print("Must be positive!")
continue # Skip to next iteration
return n # Valid - exit loop
except ValueError:
print("Invalid number!")
continue
continue skips to re-prompt, return exits when valid.
Skip Invalid Items
def process_records(records):
"""Process valid records, skip invalid ones"""
results = []
for record in records:
if not record.get('id'):
continue # Skip records without ID
if record.get('status') == 'deleted':
continue # Skip deleted records
# Only reach here for valid records
results.append(transform(record))
return results
continue acts like a filter - skip unwanted items cleanly.
Rate Limiting / Max Attempts
def fetch_with_retry(url, max_attempts=3):
"""Try API call with retries"""
for attempt in range(1, max_attempts + 1):
try:
response = requests.get(url)
response.raise_for_status()
return response.json() # Success - exit
except RequestException:
if attempt == max_attempts:
raise # Give up after max attempts
time.sleep(2 ** attempt) # Exponential backoff
continue # Try again
Retry logic with break on success, continue on failure.
Practice: Loop Control
Task: Find the first even number in a list using break.
Show Solution
numbers = [1, 3, 5, 7, 8, 9, 10]
first_even = None
for num in numbers:
if num % 2 == 0:
first_even = num
break # Stop searching after finding first
print(f"First even: {first_even}") # Output: First even: 8
Task: Sum only positive numbers from a list using continue.
Show Solution
numbers = [10, -5, 20, -3, 15, -8, 25]
total = 0
for num in numbers:
if num < 0:
continue # Skip negative numbers
total += num
print(f"Sum of positives: {total}") # Output: Sum of positives: 70
Task: Check if all items in a list are positive. Use loop-else to print appropriate message.
Show Solution
def check_all_positive(numbers):
for num in numbers:
if num <= 0:
print(f"Found non-positive: {num}")
break # Exit loop, skip else
else:
# Only runs if no break occurred
print("All numbers are positive!")
check_all_positive([1, 2, 3, 4, 5]) # All positive!
check_all_positive([1, 2, -3, 4, 5]) # Found non-positive: -3
Task: Find the position of a target value in a 2D grid. Return (row, col) or None if not found.
Show Solution
def find_in_grid(grid, target):
for row_idx, row in enumerate(grid):
for col_idx, val in enumerate(row):
if val == target:
return (row_idx, col_idx) # Found! Exit both
return None # Not found after checking all
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(find_in_grid(grid, 5)) # Output: (1, 1)
print(find_in_grid(grid, 10)) # Output: None
Quick Reference Cheat Sheet
Comprehension Syntax
| List: | [expr for x in iter] |
| List + filter: | [expr for x in iter if cond] |
| Dict: | {key: val for x in iter} |
| Set: | {expr for x in iter} |
| Generator: | (expr for x in iter) |
| Nested: | [expr for a in X for b in a] |
Generator Patterns
| Expression: | (x*2 for x in range(10)) |
| Function: | def gen(): yield value |
| Get next: | next(gen_obj) |
| To list: | list(gen_obj) |
| Iterate: | for item in gen_obj: |
Walrus Operator
| In if: | if (n := func()) > 5: |
| In while: | while (x := input()) != "q": |
| In comprehension: | [y for x in L if (y := f(x))] |
Loop Control
| break: | Exit loop immediately |
| continue: | Skip to next iteration |
| pass: | Do nothing (placeholder) |
| for...else: | Runs if no break executed |
| while...else: | Same - runs on normal exit |
Knowledge Check
Quick Quiz
Test what you've learned about advanced control flow
1 What is the main difference between a list comprehension and a generator expression?
2 What does the walrus operator (:=) do?
3 When does the else clause of a for loop execute?
4 What is the output of: [x for x in range(5) if x % 2]?
5 What happens when you try to iterate over a generator twice?
6 Which statement is true about break in nested loops?
Key Takeaways
List Comprehensions are Pythonic
Transform multi-line loops into elegant one-liners. Use [expr for x in iter if cond] for cleaner, faster code.
Generators Save Memory
Use generator expressions (expr for x in iter) when processing large datasets - they compute values on-demand.
Walrus Operator Reduces Repetition
Use := to assign and use a value in one expression - perfect for while loops and conditional checks.
Break Exits, Continue Skips
Use break to exit loops early, continue to skip iterations, and pass as a placeholder.
Loop Else is Powerful
The else clause after for/while runs only if no break occurred - great for search patterns.
Nested Comprehensions
Read nested comprehensions left-to-right like nested loops: [x for row in matrix for x in row].