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

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: click or typer wrap argparse with less ceremony.
  • Async: read the asyncio docs and try httpx for concurrent HTTP.
  • Tooling: ruff for linting and formatting, mypy for type checking, pytest for 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.