Code Review for AI Agents
BAD -- generic name in a 30-line function
GOOD -- says what the data actually is
BAD -- function name is vague
BAD -- "flag" says nothing
BAD -- magic string repeated in multiple places
GOOD -- single source of truth
BAD -- status codes scattered as literals
BAD -- three lines of commented-out code nobody understands
conn = db.connect(host="old-server", port=5432)
cursor = conn.cursor()
cursor.execute("SELECT * FROM legacy_table")
BAD -- crutch comment for a bad name
GOOD -- self-documenting name, no comment needed
GOOD -- comment explains the non-obvious
GOOD -- comment explains why, not what
We sort by created_at descending because the UI shows newest first,
and the database index on created_at makes this O(log n) not O(n log n).
GOOD -- comment documents a workaround
Workaround for Chrome bug #123456: requestAnimationFrame fires twice
BAD -- vowel-dropped abbreviation
BAD -- single-letter name for module-level variable used across 200 lines
GOOD -- descriptive for its scope
BAD -- class name is one character
BAD -- mixed conventions
BAD -- unused import and unused variable
GOOD -- only what's needed
BAD -- feature-flagged dead code without ticket reference
BAD -- "process" says nothing about what happens
GOOD -- specific verb
BAD -- negated boolean name + negated check
BAD -- `result` reused for three different things
BAD -- does clip() modify the text or return a new string?
BAD -- bare FIXME, impossible to prioritize or assign
FIXME: this is slow
GOOD -- owner attached, can be searched and assigned
BAD -- hardcodes a value that lives in settings.py
Max retries is 5 (configured in settings.py)
BAD -- mixed casing in the same module
BAD -- orphaned TODOs that will never be addressed
TODO fix this later
FIXME not sure why this works
HACK temporary
GOOD -- every TODO has accountability
TODO(CND-423): handle edge case where user has no email
FIXME(@bob, 2025-06-01): replace with batch API once available
HACK(GH-112): workaround for sqlalchemy bug #4521, remove after upgrade
BAD -- long function doing validation + transformation + persistence
BAD -- 5 positional args
BAD -- flag toggles behavior
BAD -- "validate" suggests pure, but it modifies the order
BAD -- deeply nested
BAD -- fetch + parse + filter in one function
BAD -- deep nesting from lack of early returns
BAD -- business logic mixed with HTTP details
BAD -- repeated date formatting in two functions
BAD -- name says "calculate" but it also modifies the order
BAD -- set_and_return pattern violates CQS
Later:
GOOD -- separated
BAD -- mutates the input list (output argument)
Caller:
GOOD -- returns a new dict with defaults applied
BAD -- complex boolean expression repeated in two places
BAD -- temporal coupling with no enforcement
Caller must "just know" the order:
GOOD -- each step returns what the next step needs
BAD -- enum selects completely different notification strategies
GOOD -- separate functions, dispatchable if needed
BAD -- loop variable modified inside body
BAD — user cancel checks ownership, admin cancel skips all checks
BAD -- caller gets None, forgets to check, AttributeError in production
GOOD -- use Optional with explicit type hint
BAD -- business logic buried inside try/except
BAD -- nested callbacks via callback-passing style
BAD -- mutable default is shared across all calls
GOOD -- use None sentinel and create inside the function
BAD — handles both user authentication AND email sending
BAD — 8 fields, low cohesion
BAD — function reaches into another object's internals
BAD — refused bequest: overrides parent to disable
BAD — 4 levels of navigation
BAD — shallow: wrapper adds no value, just passes through
BAD — circular dependency
file: order.py
file: customer.py
GOOD — break cycle with interface or restructure
file: types.py
BAD — middle man adds nothing
BAD — constructor does I/O and can fail
BAD — temporal decomposition leaks protocol details
BAD — data class with no behavior
Logic lives elsewhere
BAD — abstract class with one subclass, unused parameter
BAD — no-arg constructor, required setters to reach valid state
GOOD — valid at construction
BAD — returning internal list reference
Caller bypasses validation:
BAD — protocol forces implementors to stub unused methods
BAD -- shallow: 6 public methods, each 2-3 lines
BAD -- both the sender and receiver know the message envelope format
sender.py
receiver.py
GOOD -- single module owns the envelope format
BAD -- pass-through that adds nothing
BAD -- hybrid: public attributes + business methods
BAD — deeply nested
BAD — near-duplicate: same structure, different field names
BAD — money as float, unit confusion
BAD — date range as separate params everywhere
BAD — repeated if-else on shape type
Shotgun Surgery — one change, many files
BAD: adding a new "premium" user tier requires changes in:
- models/user.py (add tier enum value)
- services/billing.py (add pricing logic)
- templates/dashboard.html (add premium badge)
- api/serializers.py (add tier field)
- tests/test_billing.py (add premium test)
GOOD: consolidate tier logic into one module
models/user_tier.py — single source of truth for tier behavior
BAD — module-level global modified from multiple functions
Bug: in tests, state leaks between test cases; in production, thread safety issues
BAD — field set only during parsing, None otherwise
BAD — imperative accumulation
BAD — trivial wrappers that add navigation cost, not clarity
BAD -- money as float; mixing currencies is invisible
Bug: charge_customer(price_in_yen, "USD") -- no type error
GOOD -- value object with validation
BAD -- function in shipping.py but mostly touches order fields
shipping.py
BAD -- bidirectional: auth imports user, user imports auth
auth.py
user.py
GOOD -- extract interface or pass data
BAD — no type annotation, anything goes
BAD
BAD — returns mutable internal list
BAD — trusts external JSON
BAD
BAD — cast without validation
BAD — equality for None check
BAD — raw strings
BAD — raw types, duplicated validation
BAD — list with repeated membership checks
BAD — list of tuples used as key-value store
GOOD — set for deduplication and membership
BAD — raw primitives, no protection against invalid values
BAD — stored derived field
line.length is still 10 — stale!
BAD -- missing variant, no exhaustiveness check
BAD -- unchecked cast silences the type checker
GOOD -- validate with Pydantic at the boundary
BAD — swallows all errors
BAD — bare except catches SystemExit, KeyboardInterrupt
BAD — fails with AttributeError deep in processing
BAD — file handle leaks on exception
BAD — EAFP overused for expected case
BAD
BAD — fixed delay, no limit
BAD — cleanup exception replaces original
BAD — implicit assumption with no assertion
GOOD — assertion for internal invariant (NOT user input)
BAD — constraints in docstring only, no enforcement
BAD — user input in error response
BAD — raw input in raised exception
GOOD — generic client error, details server-side
BAD — raises when the desired state already holds
Caller must:
if is_subscribed(user_id, topic): # race-prone!
unsubscribe(user_id, topic)
BAD — duplicated error handling in every endpoint
BAD — abstraction leak
BAD — returns default on error, caller can't tell the difference
BAD — no default, new role silently ignored
BAD -- FastAPI route with no error handling; unhandled exception returns 500 with stack trace
BAD -- bare Exception with no structure
GOOD -- typed error hierarchy with context
Caller can handle each case distinctly
BAD — N+1: one query per post
BAD — loads all rows into memory
BAD — returns all records
BAD — blocking HTTP call in async handler
BAD — overlapping alternation with quantifier
BAD — O(n^2) string concatenation
BAD — recomputes on every call
BAD — set grows unbounded
BAD — no timeout, blocks indefinitely
BAD — debug log in tight loop, string formatting always runs
BAD — new date object created on every iteration
BAD — JSON encode/decode in high-throughput message handler
BAD — premature optimization sacrificing readability
"Using bitwise shift for division by 2 — it's faster"
BAD — nested conditionals for multi-dimensional lookup
BAD — parallelizes (level 5) without considering "do less" (level 3)
BAD -- imports entire module for one function
BAD -- wildcard import pollutes namespace and obscures dependencies
GOOD -- import only what you need
BAD -- observer started without cleanup
GOOD -- cleanup via context manager or explicit stop
Usage: cleanup guaranteed
BAD — no assertion
BAD — asserts on internal call order
BAD — shared list mutated across tests
BAD — over-mocked, more mocks than assertions
BAD — depends on network and external service
BAD — tests only the normal case
BAD — method-name tests
BAD — mirrors the implementation
BAD — asserts on internal call order and arguments
BAD — tests a dataclass constructor
BAD — class-level mutable fixture
BAD — mock hides real query behavior
BAD — only 2 hardcoded cases for a serializer
BAD — tests internal call sequence
BAD — mocking the repository (managed dependency)
BAD — SQLite for tests, PostgreSQL in production
BAD — datetime.now() buried in domain logic
GOOD — time as explicit parameter
BAD — conditional assertion
BAD -- snapshot of entire serialized response (100+ lines)
BAD -- only happy path
BAD -- interleaved actions and assertions
GOOD -- one behavior per test, clear AAA structure
BAD — shared dict modified from multiple threads
BAD — check-then-act race on file existence
BAD — coroutine created but never awaited
BAD — lock ordering violation
BAD — CPU-bound work in async handler
BAD — unbounded concurrency
BAD — fire-and-forget coroutine with no error handling
BAD — background task with no cancellation
BAD — sequential writes without transaction
BAD — read-modify-write without protection
BAD — write skew on booking overlap check
BAD — phantom: both transactions see zero conflicts, both insert
GOOD — unique constraint + handle violation
BAD — counter increment is not idempotent
BAD — threading interleaved with computation
BAD — f-string SQL injection
BAD — password in source
BAD — no authorization check (any authenticated user can access any user's data)
BAD — rendering user input without escaping in Jinja2
BAD — directory traversal
BAD — user input flows to eval (code injection)
BAD — config.yaml committed to git with real secrets
config.yaml
aws:
access_key_id: "AKIAIOSFODNN7EXAMPLE"
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
GOOD — reference environment variables in config
config.yaml
aws:
access_key_id: ${AWS_ACCESS_KEY_ID}
BAD — no timeout on external call
BAD — no ownership check
BAD — mass assignment via **kwargs
BAD — merging user input into config dict
BAD — decorator-based auth (opt-in; forget decorator = public endpoint)
BAD — trusts Content-Type, uses user filename
BAD — startsWith bypass with //evil.com
BAD — no signature verification on webhook
BAD — introspection enabled, no limits
GOOD — disable introspection, add depth validation
BAD — no security headers in Flask
BAD — token as plain string
BAD — changing a required field to optional breaks consumers expecting it
v1 modified: {"status": "active"} # email removed — breaks consumers
BAD — no validation
BAD — POST creates duplicate on retry
BAD — no pagination
BAD — adding a required field to a Pydantic model used in API serialization
BAD — deprecated without alternative
BAD — removing a field from API response breaks old clients
Old mobile app crashes: KeyError: 'legacy_id'
GOOD — deprecate, retain, then remove after transition
^^ still included but documented as deprecated
BAD — DELETE with body
GOOD — query params for filtered delete
BAD -- rate limit exists but 429 response has no retry information
GOOD -- rate limit headers on every response, clear 429 with Retry-After
BAD — circular import
models/user.py
models/order.py
BAD — model layer imports from Flask
models/user.py
GOOD — model is framework-agnostic
BAD — adding requests just to make one HTTP call when httpx is already in the project
requirements.txt
GOOD — use what's already available
BAD — importing private module
GOOD — use public API
BAD — importing from vendored sub-package
BAD — hardcoded database URL
GOOD — from environment with validation
BAD — using a sub-dependency of requests
GOOD — add to requirements.txt
requirements.txt:
BAD — god module
utils/__init__.py that re-exports everything
Every import of `from utils import format_date` triggers loading ALL of utils
BAD — config threaded through functions that don't use it
BAD — business logic directly imports concrete email library
services/notification.py
GOOD — depend on a protocol
ports/email.py
services/notification.py
BAD -- boto3 used directly in 6 files
upload.py
download.py
GOOD -- wrapped in a storage adapter
BAD -- factory pattern with one product
Only EmailNotification exists. The factory is dead weight.
GOOD -- direct instantiation until a second type appears
BAD -- adding a new user field requires changes in 4 files
models.py: add field
serializer.py: add field to serializer
form.py: add field to form
template.html: add field to template
All four files "know" about user fields -- not orthogonal
GOOD -- auto-generated forms and serializers from model
models.py: add field (single source of truth)
serializer.py: fields = "__all__" (auto-includes new fields)
BAD -- 6 required config parameters for a connection pool
GOOD -- sensible defaults; user only overrides what they understand
BAD — vague comment
BAD — no actionable fix
BAD -- N+1: one query per order to get the customer
BAD -- deep eager-load chain
BAD -- loads entire Post objects
BAD
BAD -- HTTP call inside transaction
BAD -- no statement timeout, long-running transaction can block others
BAD -- scalar_one_or_none() returns None, next line crashes
BAD -- partial write if second insert fails
BAD -- no migration history, may drop data
GOOD -- versioned, reviewable migration
Review the SQL in prisma/migrations/
BAD -- Django syncdb / Prisma db push equivalent
python manage.py syncdb # deprecated and dangerous
GOOD -- Django migrations
python manage.py makemigrations # creates migration file
python manage.py migrate # apply to staging first
BAD -- apply untested DDL directly to production
GOOD -- test on staging, then apply to production
BAD -- direct connection from Lambda
BAD -- default pool size
BAD -- new engine on every module reload
GOOD -- module-level singleton (Python doesn't have hot reload issues as often,
BAD -- separate STRING keys for each field
Reading all fields: 3 round trips
GOOD -- single HASH key
BAD -- no expiration
GOOD -- with TTL
BAD -- large list cached as single value
GOOD -- paginated cache or store in DB, cache IDs only
BAD -- f-string SQL injection
BAD -- string format
GOOD -- parameterized query with psycopg
BAD -- relative import reaching outside package boundary
In services/api/handlers/user.py:
fragile: depends on directory depth and internal structure
GOOD -- import via installed package name
In services/api/handlers/user.py:
myorg_shared is declared in pyproject.toml as a workspace dependency
BAD — unstructured print, no level, no timestamp
BAD — basic logging with string formatting, not machine-parseable
GOOD — structlog with bound context
BAD — validation failure logged as ERROR
BAD — database connection failure logged as WARNING
BAD — no correlation, logs from concurrent requests are interleaved and inseparable
GOOD — correlation ID middleware with contextvars
BAD — logs entire request payload including password
BAD — logs the auth token
GOOD — log only safe identifiers
BAD — per-row logging in data pipeline
BAD — bare error message
BAD — just the exception, no business context
BAD — inconsistent names, no units
BAD — database query with no timing
BAD — alert with no remediation guidance
BAD — f-string message, every entry is unique
"User u_123 signed up via google" — unique, ungrepable without regex
BAD — %-format in the message field
Produces unique strings; cannot group or count in log aggregator
GOOD — static event with structured context (structlog)
Event is always "user_signed_up" — searchable, countable, alertable
GOOD — static message with extra dict (stdlib logging + JSON formatter)
BAD — no timeout; hangs indefinitely if server is slow
BAD — only connect timeout, no read timeout
GOOD — explicit (connect_timeout, read_timeout) tuple
BAD — retry loop without idempotency key
BAD — fixed 1-second interval; all instances retry in sync
BAD — retries all HTTP errors including permanent ones
BAD — direct call; 1000 concurrent requests pile up when inventory is down
BAD — dual write with no atomicity guarantee
BAD — ReportingService reads InventoryService's private tables
In reporting-service:
GOOD — ReportingService queries a local read model updated via events
inventory-service publishes StockUpdated events
BAD — property access transparently calls remote service
BAD — synchronous-looking name and return type for a network call
BAD — removes a required field from Pydantic model; breaks old message readers
BAD — changes field type; breaks all existing data
BAD — Queue() with no maxsize = unbounded
BAD — discovers missing S3 credentials during processing, after acquiring DB lock
BAD — trivial health check; load balancer can't tell if we're really healthy
BAD — pricing service outage kills the product page
BAD — shallow module; caller must understand internals
BAD — UserUpdatedEvent duplicated across services, drifted between them
In user-service/events.py:
In analytics-service/events.py: (stale copy — missing `tier`)
GOOD — shared schema package published to internal PyPI
Package: mycompany-events (pip install mycompany-events==1.3.0)
mycompany_events/user.py:
In user-service:
19.1 — Find HTTP calls without timeout
19.2 — Find retry loops without idempotency key
Manually verify each file contains "idempotency" or "Idempotency-Key"
19.3 — Find sleep/delay in retry logic without exponential/jitter
19.5 — Find integration points without circuit breaker
19.6 — Find dual writes (DB write followed immediately by event/message send)
19.10 — Find unbounded queues
19.12 — Find health check endpoints that only return 200