Anti Patterns

Anti Patterns

Why this page exists

Anti patterns are not “style issues”. They are repeatable ways to ship incidents.

This document lists the most common bad coding habits. Fix them at PR review.

This is a living document. We will keep adding items as we find new failure modes.

Common Anti Patterns (Python examples)

Swallowing exceptions

Anti pattern: you hide the failure and destroy debuggability.

def load_user(user_id: str):
    try:
        return repo.get_user(user_id)
    except Exception:
        return None  # silent failure

Do this instead: handle, wrap, or rethrow with context.

class UserLoadError(RuntimeError):
    pass


def load_user(user_id: str):
    try:
        return repo.get_user(user_id)
    except RepoError as e:
        raise UserLoadError(f"failed to load user_id={user_id}") from e

Bare except:

Anti pattern: you catch things you must not catch (KeyboardInterrupt, SystemExit).

try:
    do_work()
except:
    logger.exception("failed")

Do this instead: catch the specific exception types you can recover from.

try:
    do_work()
except (TimeoutError, ConnectionError) as e:
    logger.warning("dependency failure", extra={"error": str(e)})
    raise

Using exceptions as control flow

Anti pattern: it’s slow, it’s noisy, and it hides intent.

def get_age(data: dict) -> int:
    try:
        return int(data["age"])
    except Exception:
        return 0

Do this instead: validate inputs explicitly.

def get_age(data: dict) -> int:
    age = data.get("age")
    if age is None:
        return 0
    return int(age)

Mutable default arguments

Anti pattern: state leaks across calls.

def add_tag(tag: str, tags: list[str] = []):
    tags.append(tag)
    return tags

Do this instead: default to None.

def add_tag(tag: str, tags: list[str] | None = None):
    tags = [] if tags is None else tags
    tags.append(tag)
    return tags

Naive logging (PII/secrets + log spam)

Anti pattern: you leak sensitive data and explode log volume.

logger.info("request=%s", request.json)  # may contain tokens/PII
for item in items:
    logger.info("processing item=%s", item)  # spam

Do this instead: log outcomes + identifiers, redact payloads.

logger.info(
    "request accepted",
    extra={"request_id": request_id, "user_id": user_id},
)

logger.info(
    "batch processed",
    extra={"request_id": request_id, "count": len(items)},
)

Building SQL with string concatenation

Anti pattern: SQL injection and broken escaping.

sql = f"SELECT * FROM users WHERE email = '{email}'"  # injection risk
cursor.execute(sql)

Do this instead: parameterize.

cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

Missing timeouts for network calls

Anti pattern: hangs become cascading failures.

requests.get(url)  # no timeout

Do this instead: set timeouts and handle retries deliberately.

requests.get(url, timeout=3)

Doing side effects at import time

Anti pattern: importing a module changes the world (hard to test, hard to reason).

# app.py
db.connect()  # happens on import

Do this instead: explicit startup.

def main() -> None:
    db.connect()
    run_server()


if __name__ == "__main__":
    main()

Giant files / “God classes”

Anti pattern: one module/class owns everything (routing, validation, DB, business logic, logging).

class OrderService:
    def create_order(self, request):
        # validation
        # auth
        # pricing
        # DB writes
        # external calls
        # logging
        # metrics
        # email notification
        ...

Do this instead: split responsibilities and keep boundaries explicit.

class OrderService:
    def __init__(self, repo, pricing, notifier):
        self.repo = repo
        self.pricing = pricing
        self.notifier = notifier

    def create_order(self, cmd: "CreateOrder") -> "Order":
        order = self.pricing.price(cmd)
        self.repo.save(order)
        self.notifier.order_created(order.id)
        return order

Magic numbers

Anti pattern: numbers with hidden meaning spread everywhere.

if retries > 3:
    raise RuntimeError("give up")

time.sleep(0.2)

Do this instead: name constants and centralize policy.

MAX_RETRIES = 3
BACKOFF_SECONDS = 0.2

if retries > MAX_RETRIES:
    raise RuntimeError("give up")

time.sleep(BACKOFF_SECONDS)

Repeat code (copy/paste logic)

Anti pattern: the same logic appears in multiple places and diverges.

def create_user(payload: dict) -> None:
    if "email" not in payload:
        raise ValueError("missing email")
    if "name" not in payload:
        raise ValueError("missing name")


def update_user(payload: dict) -> None:
    if "email" not in payload:
        raise ValueError("missing email")
    if "name" not in payload:
        raise ValueError("missing name")

Do this instead: factor shared logic into a single function.

REQUIRED_USER_FIELDS = ("email", "name")


def validate_required_fields(payload: dict, fields: tuple[str, ...]) -> None:
    missing = [f for f in fields if f not in payload]
    if missing:
        raise ValueError(f"missing fields: {missing}")


def create_user(payload: dict) -> None:
    validate_required_fields(payload, REQUIRED_USER_FIELDS)


def update_user(payload: dict) -> None:
    validate_required_fields(payload, REQUIRED_USER_FIELDS)

References