Pythonic iteration and resource handling
Python's iteration tools separate what values are produced from how they are consumed. Prefer clear, lazy pipelines for large inputs and explicit loops when logic has side effects or several branches.
Comprehensions
Use comprehensions for a single transformation with an optional simple filter.
squares = [number**2 for number in range(10)]
even_squares = {number: number**2 for number in range(10) if number % 2 == 0}
letters = {word[0] for word in ["apple", "apricot", "banana"]}
Do not force complex branching, exception handling, or side effects into a comprehension. A normal loop is then easier to read.
Iterables, iterators, and generators
An iterable can produce an iterator. An iterator tracks progress and returns its next item through next(). A generator is an iterator created by a function containing yield or by a generator expression.
def read_nonempty(lines):
for line in lines:
text = line.strip()
if text:
yield text
length_total = sum(len(line) for line in read_nonempty(source))
Generators are lazy: they produce one item at a time and are normally consumed once. This reduces memory use, but it does not automatically make computation faster.
Useful built-ins
for index, value in enumerate(values, start=1):
print(index, value)
pairs = zip(names, scores, strict=True)
ordered = sorted(records, key=lambda record: record["date"], reverse=True)
has_error = any(result.failed for result in results)
all_ready = all(worker.ready for worker in workers)
Other common tools include min, max, sum, reversed, map, and filter. Comprehensions are often clearer than map and filter when a lambda would be required.
Standard-library iteration tools
collections.Countercounts hashable values.collections.defaultdictsupplies missing values from a factory.collections.dequesupports efficient operations at both ends.itertools.chainjoins iterables lazily.itertools.islicetakes a lazy slice.itertools.pairwiseyields adjacent pairs.functools.reducecombines values cumulatively, thoughsumor a loop is often clearer.
from collections import Counter, deque
from itertools import chain, islice
counts = Counter(words)
queue = deque([start])
first_ten = list(islice(chain(source_a, source_b), 10))
Decorators
A decorator receives a callable and returns a replacement callable. Preserve metadata with functools.wraps.
from functools import wraps
def trace(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
return function(*args, **kwargs)
return wrapper
Decorators are useful for cross-cutting behavior such as caching, registration, authorization, and instrumentation. Keep business logic visible rather than stacking opaque decorators.
Context managers
A context manager pairs acquisition with guaranteed cleanup.
from pathlib import Path
with Path("data.txt").open(encoding="utf-8") as stream:
data = stream.read()
Create one from a generator when needed:
from contextlib import contextmanager
@contextmanager
def transaction(connection):
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
Experiment: lazy iteration
Change the filter, remove islice, or consume the generator twice and compare the results.
from itertools import islice
def matching_squares(limit, divisor):
for number in range(limit):
square = number ** 2
if square % divisor == 0:
yield square
values = matching_squares(1_000_000, 3)
print("first five:", list(islice(values, 5)))
print("next three: ", list(islice(values, 3)))
print("remaining generator:", values)
Checkpoint
- Use comprehensions only when they remain readable.
- Explain iterable, iterator, and generator.
- Build lazy pipelines without accidentally consuming them twice.
- Reach for standard-library tools before custom loops.
- Use decorators and context managers for focused, reusable behavior.