Files
python-patterns/smells/common-mistakes.md
T
2026-06-01 21:42:05 +00:00

5.4 KiB

Common Mistakes in Python

This file captures recurring non-idiomatic patterns, especially code that reads like Java or TypeScript in a Python costume.

1. Boolean-flag methods that hide two behaviors

Smell

def fetch_user(user_id: str, include_deleted: bool = False) -> User | None:
    ...

Why it is a smell

Boolean mode flags often mean one function is doing two jobs. They create call sites like fetch_user(id, True) that say almost nothing.

Better shape

  • split into separate functions when the behaviors are meaningfully different
  • or promote the difference to a real enum or config object when there are several modes

2. Broad except Exception: without a boundary reason

Smell

try:
    do_work()
except Exception:
    return None

Why it is a smell

This erases recovery meaning. Mature libraries like HTTPX and Click use exception hierarchies so callers can catch broadly or narrowly depending on what they can recover from.

Better shape

  • catch the specific failures you can handle
  • use broad catch points only at real process, API, or CLI boundaries
  • translate internal failures into a clear outer error contract

Source signals

  • httpx/_exceptions.py:74-90 defines HTTPError as a broad catch point.
  • httpx/_exceptions.py:107-160 then narrows request, transport, and timeout failures for recovery.
  • src/click/exceptions.py:35-111 ties exception subtypes to different exit codes and help output.

3. Hidden I/O in constructors or properties

Smell

class User:
    def __init__(self, user_id: str):
        self.profile = requests.get(...).json()

Why it is a smell

Object creation now performs surprising network I/O, which makes testing, lifetime, and failure handling muddy.

Better shape

  • keep I/O in explicit factory or boundary methods
  • make resource acquisition visible
  • separate data containers from loading logic

4. Import-time side effects

Smell

client = connect_to_db()
load_config_from_network()

Why it is a smell

Importing a module should usually define names, not secretly talk to the outside world. Import-time side effects make startup order brittle and tests unpredictable.

Better shape

  • move initialization into explicit startup paths
  • create resources in app setup, dependency wiring, or main entrypoints

5. Mixing validation, transport, and business logic in one class

Smell

class OrderModel(BaseModel):
    def save(self): ...
    def send_webhook(self): ...
    def render_html(self): ...

Why it is a smell

Boundary schema, persistence logic, and business behavior cannot evolve independently anymore.

Better shape

  • let Pydantic, attrs, or dataclasses own data-shape concerns
  • keep boundary translation explicit
  • split long-lived behavior into services or domain objects when needed

Source signals

  • pydantic/main.py:253-264 treats model construction as input validation.
  • pydantic/main.py:455-519 makes dumping an explicit serialization step.
  • src/_pytest/timing.py:24-64 uses frozen dataclasses for small internal value objects.

6. Fake sync wrappers around async code

Smell

def get(url: str) -> Response:
    return asyncio.run(async_get(url))

Why it is a smell

This breaks in existing event loops and hides real async lifetime and cancellation semantics.

Better shape

  • provide separate sync and async entrypoints when semantics differ
  • keep them parallel, not secretly interchangeable

Source signals

  • httpx/_client.py:594-661 and httpx/_client.py:1307-1375 define separate Client and AsyncClient types.
  • httpx/_client.py:639-660 vs. httpx/_client.py:1353-1374 keep the constructor shapes parallel while changing transport types.
  • examples/asyncio/async_orm.py:61-67 vs. examples/inheritance/joined.py:93-120 show the same split in SQLAlchemy session usage.

7. Overuse of Any

Smell

def send(data: Any, options: Any) -> Any:
    ...

Why it is a smell

The API contract disappeared.

Better shape

  • use concrete types when the contract is specific
  • use Protocol when callers care about capability
  • use explicit unions or aliases for ergonomic flexibility

Source signals

  • Lib/typing.py:2132-2157 frames Protocol around structural subtyping.
  • Lib/typing.py:2155-2157 warns that runtime protocol checks ignore signatures.
  • httpx/_client.py:639-660 shows rich public typing instead of Any-shaped parameters.

8. Accidental public APIs through file layout

Smell

from mypkg.utils import helper_a
from mypkg.impl_v2 import thing

Why it is a smell

Internal module layout becomes the public contract by accident, which makes refactors painful.

Better shape

  • publish a deliberate import surface
  • re-export supported names
  • use __all__ when the boundary should be explicit

Source signals

  • src/pytest/__init__.py:6-80 builds a top-level facade over _pytest.* internals.
  • src/pytest/__init__.py:98-186 pins that facade with __all__.
  • httpx/__init__.py:1-12 and httpx/__init__.py:29-106 do the same while also rewriting exported objects to appear under httpx.

Heuristic

If the code:

  • hides resource lifetime
  • hides write boundaries
  • hides which failures matter
  • hides what the public API really is

…it is usually fighting Python instead of using it.