Last updated: 2026-09-18
Python Idioms and Paradigms
Code that runs correctly and code that's "Pythonic" aren't always the same thing. Pythonic code follows a set of community-agreed idioms — patterns the language was specifically designed to make natural — codified early on in PEP 8 (the style guide) and, half-seriously, in PEP 20, "The Zen of Python"1. Two of its nineteen aphorisms matter more than the rest for what follows here: "There should be one — and preferably only one — obvious way to do it," and "Readability counts."
Comprehensions versus Explicit Loops
A list comprehension builds a new list from an existing iterable in a single expression, and Python's own style guide treats it as the preferred idiom over an equivalent explicit loop when the transformation is simple enough to read in one line2:
# Explicit loop
squares = []
for n in range(10):
if n % 2 == 0:
squares.append(n ** 2)
# Comprehension — same result, one expression
squares = [n ** 2 for n in range(10) if n % 2 == 0]
The same pattern extends to dict and set comprehensions ({k: v for ...}, {x for ...}). The judgement call is readability, not brevity for its own sake: a comprehension nesting two loops and a conditional is usually harder to read than the explicit version, and PEP 8's own guidance is to prefer the explicit loop once a comprehension stops being immediately scannable — "obvious way to do it" cuts both directions.
Generators and Lazy Evaluation
A list comprehension builds the entire result in memory before returning it. A generator — written the same way but with parentheses instead of brackets, or as a function using yield instead of return — produces values one at a time, on demand, and never holds the whole sequence in memory at once:
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
# Only one line is ever in memory at a time, regardless of file size
for line in read_large_file("huge_log.txt"):
process(line)
yield pauses the function's execution at that point and hands control back to the caller, resuming exactly where it left off the next time a value is requested — the function's local state (loop position, variables) is preserved across pauses, which is what makes it possible to represent a stream of values without ever materialising all of them together. This matters most exactly where the pattern above hints: processing files, network streams, or any data source too large — or too slow-arriving — to reasonably hold in memory at once.
First-Class Functions and Functional Style
Functions in Python are ordinary objects — they can be assigned to variables, passed as arguments, and returned from other functions, which is what makes map, filter, and lambda possible at all:
nums = [1, 2, 3, 4, 5]
# map/filter with a lambda
evens_squared = list(map(lambda n: n ** 2, filter(lambda n: n % 2 == 0, nums)))
# The equivalent, and per PEP 8's own guidance the generally preferred, comprehension
evens_squared = [n ** 2 for n in nums if n % 2 == 0]
PEP 8 specifically recommends a comprehension over map/filter with a lambda in cases like this one, on readability grounds — the comprehension reads left-to-right in the order the operations actually happen, where the nested map(lambda ..., filter(lambda ..., ...)) reads inside-out. lambda keeps its place for short, throwaway functions passed directly as an argument where defining a full named function would be needless ceremony — a sort key, an event-handler callback — not as a general substitute for comprehensions.
Duck Typing and Magic Methods
Python doesn't check whether an object formally implements an interface before calling a method on it — it simply calls the method, and lets the call fail at runtime if the object doesn't actually support it. This is duck typing: "if it walks like a duck and quacks like a duck, treat it as a duck," and it's Python's answer to what OOP Fundamentals covers as the interface contract in statically-typed languages — the promise just isn't checked by the compiler, it's checked by whether the method actually exists when it's called.
The mechanism that makes this powerful rather than just permissive is magic (dunder) methods — __len__, __iter__, __getitem__, and dozens more — which let a user-defined class opt into Python's own built-in protocols. Define __len__ and len(my_object) works; define __iter__ and a for loop works directly over it; define __getitem__ and square-bracket indexing works. A class doesn't need to extend or implement anything to gain this behaviour — implementing the right dunder method is the entire contract, which is the same duck-typing philosophy applied specifically to the language's own core syntax rather than to a library the class happens to use.
References
Peters, T. (2004). PEP 20 — The Zen of Python. Python Software Foundation. https://peps.python.org/pep-0020/ ↩
van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 — Style Guide for Python Code. Python Software Foundation. https://peps.python.org/pep-0008/ ↩