Pythonic Patterns
Idiomatic Python code and best practices.
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