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 is for 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