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 failureDo 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 eBare 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)})
raiseUsing 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 0Do 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 tagsDo 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 tagsNaive 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) # spamDo 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 timeoutDo 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 importDo 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 orderMagic 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
- PEP 8: https://peps.python.org/pep-0008/
- Python Exceptions (docs): https://docs.python.org/3/tutorial/errors.html
- OWASP Logging Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html