"Pythonic" code leans on the language's own tools to say things briefly and clearly. This chapter covers the machinery behind that style: iterators, generators, comprehensions and decorators, followed by the habits that make code feel natural to experienced Python developers.
Iterators
A for loop works on anything iterable: lists, strings, dicts, files, ranges and more. Under the hood, the loop calls iter() to get an iterator, then calls next() repeatedly until the iterator raises StopIteration.
numbers = [10, 20, 30]
it = iter(numbers)
next(it) # 10
next(it) # 20
next(it) # 30
# one more next(it) raises StopIteration
# zip and enumerate return iterators as well
names = ["mina", "joon"]
scores = [90, 85]
for rank, (name, score) in enumerate(zip(names, scores), start=1):
print(rank, name, score)
Because iterators produce values on demand, they can walk through huge data sets without loading everything into memory. The flip side is that an iterator is single-use: once exhausted, it stays exhausted.
Generators
Any function containing yield is a generator function. Calling it does not run the body; it returns a generator object that executes up to the next yield each time a value is requested, then pauses. It is by far the easiest way to write a custom iterator.
def countdown(n: int):
while n > 0:
yield n
n -= 1
list(countdown(3)) # [3, 2, 1]
def read_lines(path: str):
with open(path, encoding="utf-8") as f:
for line in f:
if line.strip():
yield line.rstrip("\n")
def numbered(lines):
for i, line in enumerate(lines, start=1):
yield f"{i}: {line}"
# stream a large file through a pipeline, one line at a time
for row in numbered(read_lines("log.txt")):
print(row)
Use yield from to delegate to another iterable. The standard itertools module adds building blocks such as islice, chain and groupby for composing iterators.
Comprehensions
Comprehensions build a list, dict or set from a loop and an optional condition in a single expression. Swap the brackets for parentheses and you get a generator expression, which produces values lazily instead of building a collection up front.
nums = range(10)
squares = [n * n for n in nums] # list
evens = [n for n in nums if n % 2 == 0] # with a filter
lengths = {w: len(w) for w in ["py", "rust"]} # dict
initials = {w[0] for w in ["apple", "avocado"]} # set {'a'}
total = sum(n * n for n in nums) # generator expression
pairs = [(x, y) for x in range(2) for y in range(2)]
Comprehensions shine when they are short. Once you are stacking several loops and conditions and the line no longer reads at a glance, an ordinary for loop is the better choice.
Decorators
Functions in Python are values: you can store them in variables and pass them around. A decorator is a function that takes a function and returns a new one with extra behavior, applied with the @name syntax. Decorators are ideal for cross-cutting concerns such as logging, timing, access checks and caching.
import functools
import time
def timed(func):
@functools.wraps(func) # keep the original name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return wrapper
@timed
def slow_add(a, b):
time.sleep(0.1)
return a + b
@functools.lru_cache(maxsize=None) # memoization from the standard library
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
slow_add(1, 2)
fib(80)
You have already met several decorators: @property, @staticmethod, @classmethod and @dataclass all work this way.
Writing Pythonic code
Type import this in the REPL to print "The Zen of Python", a short statement of the language's design values. In practice, these habits get you most of the way:
- Loop over values directly rather than over indexes; use
enumerate when you need a counter and zip to walk sequences in parallel. - Open resources with
with and let context managers handle cleanup. - Prefer "easier to ask forgiveness than permission" (EAFP): try the operation and handle the exception, rather than checking every precondition first.
- Lean on built-ins such as
sum, any, all, sorted and max(key=...). - Choose readable over clever. Readability counts.
users = [{"name": "mina", "active": True}, {"name": "joon", "active": False}]
# not very Pythonic
active = []
for i in range(len(users)):
if users[i]["active"] == True:
active.append(users[i]["name"])
# Pythonic
active = [u["name"] for u in users if u["active"]]
has_inactive = any(not u["active"] for u in users)
Summary
for loops run on the iterator protocol (iter and next), and iterators produce values lazily.- Generators written with
yield let you process large data step by step with little memory. - Comprehensions build collections concisely; fall back to a loop when they get complicated.
- Decorators move shared behavior out of function bodies; always use
functools.wraps in your wrappers.
To keep going, the official documentation is the best next stop:
0 comments
Sign in · Sign in to leave a comment.