Skip to content

Common Python Pitfalls

Status: 🟢 Active  |  Owner: Python Guild

Mutable Default Arguments

Default argument values are evaluated once at function definition time, not on each call.

# ❌ The same list is shared across all calls
def append_item(item: str, items: list[str] = []) -> list[str]:
    items.append(item)
    return items

append_item("a")  # ["a"]
append_item("b")  # ["a", "b"]  ← unexpected

# ✅ Use None and create fresh inside the function
def append_item(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items

Late Binding in Closures

Lambda and nested functions bind variables at call time, not definition time.

# ❌ All lambdas capture the same `i` from the loop's final value
funcs = [lambda: i for i in range(5)]
funcs[0]()  # 4, not 0

# ✅ Capture by default argument
funcs = [lambda i=i: i for i in range(5)]
funcs[0]()  # 0

Catching Broad Exceptions

# ❌ Hides bugs — catches KeyboardInterrupt, SystemExit, and everything else
try:
    process()
except Exception:
    pass

# ✅ Catch only what you can handle
try:
    process()
except (ValueError, OrderNotFoundError) as exc:
    logger.warning("Processing failed: %s", exc)
    raise

Using assert for Runtime Validation

assert is stripped when Python runs with the -O flag (optimise mode), which is often set in production.

# ❌ Not guaranteed to run in production
assert user_id is not None, "user_id required"

# ✅ Use an explicit check
if user_id is None:
    raise ValueError("user_id is required")

Sync I/O Inside Async Functions

Calling blocking I/O directly in an async function blocks the entire event loop.

# ❌ Blocks the event loop during HTTP request
async def fetch_data(url: str) -> dict:
    response = requests.get(url)  # synchronous!
    return response.json()

# ✅ Use an async HTTP client
async def fetch_data(url: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.json()

Importing at Function Scope

Placing imports inside function bodies defers ImportError until runtime and defeats static analysis.

# ❌ Import inside function body
def process():
    import pandas as pd
    return pd.DataFrame()

# ✅ Top-level imports
import pandas as pd

def process():
    return pd.DataFrame()

__init__.py Circular Imports

Avoid importing from sibling modules in __init__.py files. This is the most common source of circular import errors in larger packages.

References


Last reviewed: 2025-Q4  |  Owner: Python Guild