| name | python-anti-patterns |
| description | Common Python anti-patterns and a pre-merge review checklist. |
| when_to_use | Reviewing Python code, finalizing implementations, or debugging issues that might stem from known bad practices. |
Python Anti-Patterns Checklist
A reference checklist of common mistakes and anti-patterns in Python code. Review this before finalizing implementations to catch issues early.
Note: This skill focuses on what to avoid. For guidance on positive patterns and architecture, see the python-patterns skill.
Core Concepts
1. Centralize Cross-Cutting Concerns
Timeouts, retries, and configuration should live in one place, not scattered across every call site.
2. Separate Layers
Keep I/O, business logic, and API concerns in distinct layers. Don't mix SQL into business functions or leak ORM models to API consumers.
3. Handle Failures Explicitly
Catch specific exceptions, preserve partial results in batch operations, and validate inputs at boundaries.
4. Use the Type System
Annotate all public functions, use typed collections, and let static analysis catch bugs before runtime.
Infrastructure Anti-Patterns
Scattered Timeout/Retry Logic
def fetch_user(user_id):
try:
return requests.get(url, timeout=30)
except Timeout:
logger.warning("Timeout fetching user")
return None
def fetch_orders(user_id):
try:
return requests.get(url, timeout=30)
except Timeout:
logger.warning("Timeout fetching orders")
return None
Fix: Centralize in decorators or client wrappers.
@retry(stop=stop_after_attempt(3), wait=wait_exponential())
def http_get(url: str) -> Response:
return requests.get(url, timeout=30)
Double Retry
@retry(max_attempts=3)
def call_service():
return client.request()
Fix: Retry at one layer only. Know your infrastructure's retry behavior.
Hard-Coded Configuration
DB_HOST = "prod-db.example.com"
API_KEY = "sk-12345"
def connect():
return psycopg.connect(f"host={DB_HOST}...")
Fix: Use environment variables with typed settings.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_host: str = Field(alias="DB_HOST")
api_key: str = Field(alias="API_KEY")
settings = Settings()
Architecture Anti-Patterns
Exposed Internal Types
@app.get("/users/{id}")
def get_user(id: str) -> UserModel:
return db.query(UserModel).get(id)
Fix: Use DTOs/response models.
@app.get("/users/{id}")
def get_user(id: str) -> UserResponse:
user = db.query(UserModel).get(id)
return UserResponse.from_orm(user)
Mixed I/O and Business Logic
def calculate_discount(user_id: str) -> float:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id)
if len(orders) > 10:
return 0.15
return 0.0
Fix: Repository pattern. Keep business logic pure.
def calculate_discount(user: User, orders: list[Order]) -> float:
if len(orders) > 10:
return 0.15
return 0.0
Error Handling Anti-Patterns
Bare Exception Handling
try:
process()
except Exception:
pass
Fix: Catch specific exceptions. Log or handle appropriately.
try:
process()
except ConnectionError as e:
logger.warning("Connection failed, will retry", error=str(e))
raise
except ValueError as e:
logger.error("Invalid input", error=str(e))
raise BadRequestError(str(e))
Ignored Partial Failures
def process_batch(items):
results = []
for item in items:
result = process(item)
results.append(result)
return results
Fix: Capture both successes and failures.
def process_batch(items) -> BatchResult:
succeeded = {}
failed = {}
for idx, item in enumerate(items):
try:
succeeded[idx] = process(item)
except Exception as e:
failed[idx] = e
return BatchResult(succeeded, failed)
Missing Input Validation
def create_user(data: dict):
return User(**data)
Fix: Validate early at API boundaries.
def create_user(data: dict) -> User:
validated = CreateUserInput.model_validate(data)
return User.from_input(validated)
Resource Anti-Patterns
Unclosed Resources
def read_file(path):
f = open(path)
return f.read()
Fix: Use context managers.
def read_file(path):
with open(path) as f:
return f.read()
Blocking in Async
async def fetch_data():
time.sleep(1)
response = requests.get(url)
Fix: Use async-native libraries.
async def fetch_data():
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
response = await client.get(url)
Type Safety Anti-Patterns
Missing Type Hints
def process(data):
return data["value"] * 2
Fix: Annotate all public functions.
def process(data: dict[str, int]) -> int:
return data["value"] * 2
Untyped Collections
def get_users() -> list:
...
Fix: Use type parameters.
def get_users() -> list[User]:
...
Testing Anti-Patterns
Only Testing Happy Paths
def test_create_user():
user = service.create_user(valid_data)
assert user.id is not None
Fix: Test error conditions and edge cases.
def test_create_user_success():
user = service.create_user(valid_data)
assert user.id is not None
def test_create_user_invalid_email():
with pytest.raises(ValueError, match="Invalid email"):
service.create_user(invalid_email_data)
def test_create_user_duplicate_email():
service.create_user(valid_data)
with pytest.raises(ConflictError):
service.create_user(valid_data)
Over-Mocking
def test_user_service():
mock_repo = Mock()
mock_cache = Mock()
mock_logger = Mock()
mock_metrics = Mock()
Fix: Use integration tests for critical paths. Mock only external services.
Language-Level Anti-Patterns
Mutable Default Arguments
def append_to(item, items=[]):
items.append(item)
return items
append_to(1)
append_to(2)
Fix: Use None and create a new object inside the function.
def append_to(item, items=None):
if items is None:
items = []
items.append(item)
return items
type() vs isinstance()
if type(obj) == list:
process(obj)
Fix: Use isinstance, which respects inheritance.
if isinstance(obj, list):
process(obj)
== None vs is None
if value == None:
process()
Fix: Use the identity check is None.
if value is None:
process()
Wildcard Imports
from os.path import *
Fix: Import only what you need.
from os.path import join, exists
Quick Review Checklist
Before finalizing code, verify:
Common Fixes Summary
| Anti-Pattern | Fix |
|---|
| Scattered retry logic | Centralized decorators |
| Hard-coded config | Environment variables + pydantic-settings |
| Exposed ORM models | DTO/response schemas |
| Mixed I/O + logic | Repository pattern |
| Bare except | Catch specific exceptions |
| Batch stops on error | Return BatchResult with successes/failures |
| No validation | Validate at boundaries with Pydantic |
| Unclosed resources | Context managers |
| Blocking in async | Async-native libraries |
| Missing types | Type annotations on all public APIs |
| Only happy path tests | Test errors and edge cases |
| Mutable default args | Use None + create inside function |
type() comparison | isinstance() |
== None | is None |
| Wildcard imports | Explicit named imports |