Pythonic Patterns
The idioms that make Python code feel like Python. Most of these are small. Together they're the difference between code that works and code that reads.
The Zen of Python
import this
Key principles:
- Beautiful is better than ugly
- Explicit is better than implicit
- Simple is better than complex
- Readability counts
- There should be one obvious way to do it
EAFP vs LBYL
LBYL: Look Before You Leap
# Check first, then act
if key in dictionary:
value = dictionary[key]
else:
value = default
EAFP: Easier to Ask Forgiveness than Permission (Pythonic)
# Try it, handle failure
try:
value = dictionary[key]
except KeyError:
value = default
# Or better yet
value = dictionary.get(key, default)
Truthy and Falsy Values
# Instead of
if len(items) > 0:
if len(items) == 0:
if value == None:
if value == True:
# Write
if items: # Non-empty
if not items: # Empty
if value is None: # Is None
if value: # Is truthy
Comparisons
# Instead of
if not (a < b):
if type(x) == list:
# Write
if a >= b:
if isinstance(x, list):
# Chained comparisons
if 0 <= x < 10: # Instead of: if x >= 0 and x < 10
if a == b == c: # All equal
Default Values
# Instead of
if x is None:
x = default
# Write
x = x or default # If x might be falsy
x = default if x is None else x # Only if None
# In function signatures
def func(items=None):
items = items or [] # Safe mutable default
Unpacking
# Swap values
a, b = b, a
# Multiple assignment
x, y, z = 1, 2, 3
# Unpack with rest
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
first, *middle, last = [1, 2, 3, 4] # first=1, middle=[2,3], last=4
# Ignore values
_, important, _ = get_data()
# Unpack in loops
for key, value in dictionary.items():
print(key, value)
for i, item in enumerate(items):
print(i, item)
String Formatting
name = "Alice"
age = 30
# f-strings (preferred)
f"Name: {name}, Age: {age}"
f"Next year: {age + 1}"
f"{name.upper()}"
f"{value:.2f}" # Format specifiers
# Debug printing (3.8+)
f"{name=}, {age=}" # "name='Alice', age=30"
Comprehensions
# List comprehension (instead of map + filter)
squares = [x**2 for x in range(10) if x % 2 == 0]
# Dict comprehension
d = {k: v for k, v in pairs if v > 0}
# Set comprehension
unique = {x.lower() for x in words}
# Generator expression (memory efficient)
total = sum(x**2 for x in range(1000000))
Iteration Patterns
# Don't use indices when you don't need them
# Instead of
for i in range(len(items)):
print(items[i])
# Write
for item in items:
print(item)
# When you need the index
for i, item in enumerate(items):
print(i, item)
# Iterate multiple sequences
for a, b in zip(list_a, list_b):
print(a, b)
# Iterate in reverse
for item in reversed(items):
print(item)
# Iterate sorted
for item in sorted(items):
print(item)
Dictionary Patterns
# Get with default
value = d.get(key, default)
# Set default and return
value = d.setdefault(key, []).append(item)
# Merge dictionaries (3.9+)
merged = d1 | d2
# Check membership
if key in d: # Not: if key in d.keys()
# Iterate items
for k, v in d.items(): # Not: for k in d: v = d[k]
Context Managers
# Always use with for resources
with open("file.txt") as f:
content = f.read()
# Multiple contexts
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
# Custom context manager
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield
print(f"Elapsed: {time.time() - start:.2f}s")
with timer():
slow_operation()
Function Best Practices
Return Early
# Instead of nested ifs
def process(data):
if data:
if is_valid(data):
if has_permission():
return do_process(data)
return None
# Write
def process(data):
if not data:
return None
if not is_valid(data):
return None
if not has_permission():
return None
return do_process(data)
Use *args and **kwargs
def wrapper(func):
def inner(*args, **kwargs):
# Works with any function signature
return func(*args, **kwargs)
return inner
Type Hints
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
def process(items: list[int]) -> dict[str, int]:
return {"sum": sum(items), "count": len(items)}
Class Patterns
Use @dataclass for Data Containers
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Instead of writing __init__, __repr__, __eq__ yourself
Use @property for Computed Attributes
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
Use slots for Memory Efficiency
class Point:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
Common Idioms
Check for None
# Use 'is' for None, not '=='
if x is None:
...
if x is not None:
...
Boolean Expressions
# Return boolean directly
# Instead of
if condition:
return True
else:
return False
# Write
return condition
# Or
return bool(value)
Conditional Expressions
# Ternary
result = value_if_true if condition else value_if_false
# Default value
name = provided_name or "Anonymous"
String Building
# Instead of concatenation in loops
result = ""
for item in items:
result += str(item)
# Use join
result = "".join(str(item) for item in items)
File Reading
# Read all lines
lines = path.read_text().splitlines()
# Process line by line (memory efficient)
with open(path) as f:
for line in f:
process(line.strip())
Anti-Patterns to Avoid
Don't
# Don't use mutable default arguments
def bad(items=[]): # Shared between calls!
items.append(1)
return items
# Don't use bare except
try:
...
except: # Catches everything including KeyboardInterrupt
pass
# Don't use type() for type checking
if type(x) == list: # Fails for subclasses
# Don't iterate over dict keys unnecessarily
for key in dict.keys(): # Just use: for key in dict
# Don't use + for string concatenation in loops
result = ""
for s in strings:
result += s # O(n²)
# Don't use len() to check for emptiness
if len(items) == 0: # Just use: if not items
Do
# Use None sentinel for mutable defaults
def good(items=None):
items = items if items is not None else []
items.append(1)
return items
# Catch specific exceptions
try:
...
except ValueError as e:
handle_error(e)
# Use isinstance for type checking
if isinstance(x, list): # Works with subclasses
# Iterate directly
for key in dict:
# Use join for string concatenation
result = "".join(strings) # O(n)
# Check truthiness directly
if not items:
Performance Tips
# Use generators for large data
sum(x**2 for x in range(1000000)) # Generator - O(1) memory
# Use sets for membership testing
valid = {"a", "b", "c"}
if item in valid: # O(1) vs O(n) for list
# Use dict.get() instead of checking then accessing
value = d.get(key, default) # One lookup
# Use local variables in loops
# Instead of
for item in items:
result.append(some_module.function(item))
# Write
func = some_module.function
append = result.append
for item in items:
append(func(item))
# Use functools.lru_cache for expensive computations
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive(n):
...
Code Organization
# Imports at top, grouped
import os # Standard library
import sys
import requests # Third-party
from mypackage import utils # Local
# Constants after imports
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
# Classes and functions
class MyClass:
...
def my_function():
...
# Main block at bottom
if __name__ == "__main__":
main()
Summary Checklist
- [ ] Use f-strings for formatting
- [ ] Use comprehensions over map/filter
- [ ] Use context managers for resources
- [ ] Check truthiness, not length
- [ ] Use
isfor None comparisons - [ ] Use
isinstance()for type checking - [ ] Use unpacking and enumerate
- [ ] Use generators for large data
- [ ] Return early, avoid deep nesting
- [ ] Use dataclasses for data containers
- [ ] Handle exceptions specifically
- [ ] Use type hints for documentation
Where to Go From Here
You've covered the core language. The next steps depend on what you want to build:
- Web backends: pick up FastAPI, Flask, or Django.
- Data and ML:
pandas,numpy,polars, then scikit-learn and the wider PyData stack. - CLIs:
clickortyperwrapargparsewith less ceremony. - Async: read the
asynciodocs and tryhttpxfor concurrent HTTP. - Tooling:
rufffor linting and formatting,mypyfor type checking,pytestfor tests. - The official tutorial: still worth reading end to end, even if you skim. docs.python.org/3/tutorial.
The fastest path to fluency is shipping something. Pick one small project, finish it, and the rest of the language reveals itself.