Python Iteration: Loops and Iterables Cheatsheet

Python offers powerful and flexible ways to iterate over data. This guide will help you understand the different iteration methods available, when to use each one, and how to write efficient, Pythonic code.

Contents

Understanding Basic Loops

Let’s start with the fundamental building blocks of iteration in Python.

The for Loop

The for loop is Python’s primary iteration tool. It works with any iterable object:

# Basic for loop with a list
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
    # The loop variable 'fruit' takes on each value in sequence
    print(f"Processing {fruit}")

# Iterating over a string (strings are iterable!)
word = "Python"
for character in word:
    # Each character is processed one at a time
    print(character.upper())

# Using range for numeric iteration
for number in range(5):
    # range(5) generates numbers 0 through 4
    print(f"Count: {number}")

Understanding range()

The range() function is a special iterator for numeric sequences:

# Different ways to use range
for i in range(5):      # Start from 0, go up to (but not including) 5
    print(i)            # Prints: 0, 1, 2, 3, 4

for i in range(2, 6):   # Start from 2, go up to (but not including) 6
    print(i)            # Prints: 2, 3, 4, 5

for i in range(1, 10, 2):  # Start from 1, go up to 10, step by 2
    print(i)               # Prints: 1, 3, 5, 7, 9

# Counting backwards
for i in range(5, 0, -1):  # Start from 5, go down to (but not including) 0
    print(i)               # Prints: 5, 4, 3, 2, 1

Advanced Iteration Techniques

Enumerate: When You Need Index and Value

The enumerate function is perfect when you need both the index and value:

colors = ['red', 'green', 'blue']

# Basic enumeration
for index, color in enumerate(colors):
    # index starts at 0 by default
    print(f"Color #{index}: {color}")

# Start enumeration at a different number
for index, color in enumerate(colors, start=1):
    # Now index starts at 1
    print(f"Color #{index}: {color}")

# Practical example: finding indices of specific elements
vowels = 'aeiou'
text = "hello world"
vowel_positions = {char: idx for idx, char in enumerate(text) if char in vowels}

Zip: Parallel Iteration

Zip allows you to iterate over multiple sequences simultaneously:

names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
cities = ['New York', 'London', 'Paris']

# Basic zip usage
for name, age in zip(names, ages):
    print(f"{name} is {age} years old")

# Zip with three or more sequences
for name, age, city in zip(names, ages, cities):
    print(f"{name} is {age} years old and lives in {city}")

# Creating dictionaries from zipped sequences
person_dict = dict(zip(names, ages))

# Using zip_longest from itertools when sequences have different lengths
from itertools import zip_longest
numbers = [1, 2]
letters = ['a', 'b', 'c']
for num, let in zip_longest(numbers, letters, fillvalue=None):
    print(f"Number: {num}, Letter: {let}")

Comprehensions: Elegant Iteration

List comprehensions provide a concise way to create lists based on existing sequences:

# Basic list comprehension
numbers = [1, 2, 3, 4, 5]
squares = [n * n for n in numbers]  # Creates: [1, 4, 9, 16, 25]

# List comprehension with conditional
even_squares = [n * n for n in numbers if n % 2 == 0]  # Creates: [4, 16]

# Dictionary comprehension
square_dict = {n: n * n for n in numbers}  # Creates: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Set comprehension
unique_letters = {char.lower() for char in "Hello World"}

# Generator expression (memory efficient)
sum_of_squares = sum(n * n for n in numbers)  # Note: no square brackets!

Working with Iterators and Generators

Creating Custom Iterators

Understanding how to create custom iterators gives you powerful control over iteration:

class CountByTwo:
    def __init__(self, start, stop):
        self.current = start
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        result = self.current
        self.current += 2
        return result

# Using the custom iterator
for num in CountByTwo(1, 10):
    print(num)  # Prints: 1, 3, 5, 7, 9

Generator Functions

Generators provide a memory-efficient way to create iterators:

def fibonacci(n):
    """Generate first n Fibonacci numbers"""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# Using the generator
for num in fibonacci(5):
    print(num)  # Prints: 0, 1, 1, 2, 3

# Generator with infinite sequence (use with caution!)
def count_forever(start=0):
    while True:
        yield start
        start += 1

# Using itertools.islice to work with infinite generators safely
from itertools import islice
first_five = list(islice(count_forever(), 5))

Useful Iterator Tools

The itertools module provides powerful iteration tools:

from itertools import (
    chain, cycle, repeat, count,
    combinations, permutations, product,
    takewhile, dropwhile
)

# Combining multiple iterables
numbers = [1, 2, 3]
letters = ['a', 'b', 'c']
for item in chain(numbers, letters):
    print(item)  # Prints: 1, 2, 3, a, b, c

# Cycling through elements
for item in islice(cycle(['A', 'B']), 5):
    print(item)  # Prints: A, B, A, B, A

# Generating combinations
items = ['x', 'y', 'z']
for combo in combinations(items, 2):
    print(combo)  # Prints all 2-item combinations

# Cartesian product
for pair in product(range(2), repeat=2):
    print(pair)  # Prints: (0,0), (0,1), (1,0), (1,1)

Performance and Best Practices

Memory Efficiency

Understanding how to write memory-efficient iteration code:

# Bad: Loading entire file into memory
with open('large_file.txt') as f:
    lines = f.readlines()  # Reads all lines into memory
for line in lines:
    process(line)

# Good: Iterator-based approach
with open('large_file.txt') as f:
    for line in f:  # Reads one line at a time
        process(line)

# Bad: Creating intermediate lists
numbers = range(1000000)
squares = [n * n for n in numbers]  # Creates large list in memory
for square in squares:
    process(square)

# Good: Generator expression
numbers = range(1000000)
for square in (n * n for n in numbers):  # Generates values on-the-fly
    process(square)

Common Iteration Patterns

Here are some patterns you’ll often encounter:

# Filtering while iterating
def is_valid(x):
    return x > 0

numbers = [1, -2, 3, -4, 5]
valid_nums = filter(is_valid, numbers)  # Built-in filter function
# Or using comprehension:
valid_nums = [x for x in numbers if is_valid(x)]

# Transforming while iterating
squares = map(lambda x: x * x, numbers)  # Built-in map function
# Or using comprehension:
squares = [x * x for x in numbers]

# Breaking out of loops
for item in items:
    if condition(item):
        break  # Exit loop completely
else:
    # This runs if no break occurred
    print("Completed without breaking")

# Skipping iterations
for item in items:
    if condition(item):
        continue  # Skip to next iteration
    process(item)

Debugging and Common Pitfalls

Common Mistakes to Avoid

# Mistake: Modifying a list while iterating
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Don't do this!

# Correct approach
numbers = [num for num in numbers if num % 2 != 0]
# Or
numbers = list(filter(lambda x: x % 2 != 0, numbers))

# Mistake: Creating unnecessary lists
result = list(map(str, range(1000000)))  # Don't do this!

# Better approach (generator expression)
for num_str in map(str, range(1000000)):
    process(num_str)

# Mistake: Not using enumerate when index is needed
items = ['a', 'b', 'c']
index = 0
for item in items:  # Don't do this!
    print(f"{index}: {item}")
    index += 1

# Correct approach
for index, item in enumerate(items):
    print(f"{index}: {item}")

Practice Exercises

Here are some exercises to reinforce your understanding:

  1. Create a generator that yields prime numbers:
def primes():
    """Generate an infinite sequence of prime numbers"""
    def is_prime(n):
        if n < 2:
            return False
        for i in range(2, int(n ** 0.5) + 1):
            if n % i == 0:
                return False
        return True

    n = 2
    while True:
        if is_prime(n):
            yield n
        n += 1

# Use it with islice
first_primes = list(islice(primes(), 5))
  1. Implement a custom iterator for date ranges:
from datetime import date, timedelta

class DateRange:
    def __init__(self, start_date, end_date):
        self.current = start_date
        self.end_date = end_date

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.end_date:
            raise StopIteration
        result = self.current
        self.current += timedelta(days=1)
        return result

# Usage
start = date(2024, 1, 1)
end = date(2024, 1, 5)
for d in DateRange(start, end):
    print(d)

Remember that Python’s iteration tools are designed to be both powerful and readable. When writing iteration code, always consider:

  1. Memory efficiency (use generators when possible)
  2. Code readability (choose the most clear and explicit approach)
  3. Performance implications (avoid unnecessary operations)
  4. Whether you need indices (use enumerate) or multiple sequences (use zip)

Tags: Python, Programming, Iteration, Loops, Generators, Comprehensions