Skip to main content

Code Review Practices: Guidelines for Effective Code Reviews

Created: March 19, 2026 Larry Qu 13 min read

Introduction

Code reviews are essential for maintaining code quality, sharing knowledge, and catching bugs before they reach production. Effective reviews balance thoroughness with speed, providing constructive feedback that improves both code and developers.

Research shows that code reviews catch 60-90% of defects before they reach production, making them one of the most cost-effective quality practices. Beyond bug detection, reviews facilitate knowledge transfer, enforce standards, and build team cohesion.

This guide covers review checklists, feedback techniques, common issues to watch for, automation strategies, and building a positive review culture.

Why Code Reviews Matter

Quality: Catch bugs, security vulnerabilities, and performance issues early Knowledge Sharing: Spread understanding of codebase across team Standards Enforcement: Ensure consistent style and patterns Mentorship: Junior developers learn from senior feedback Collective Ownership: Everyone feels responsible for code quality Documentation: Review comments explain design decisions

Review Checklist

Code Quality

  • Code follows project style guidelines and conventions
  • Variable and function names are descriptive and follow naming conventions
  • Complex logic is well-commented with “why” not “what”
  • Functions are small (< 50 lines) and do one thing
  • Duplication is eliminated (DRY principle)
  • Error handling is comprehensive and specific
  • No hardcoded values (use constants/config)
  • Magic numbers are replaced with named constants
  • Code is readable without excessive cleverness
  • Abstractions are appropriate (not over-engineered)

Testing

  • New functionality has unit tests
  • Tests are meaningful and not trivial
  • Edge cases are covered (null, empty, boundary values)
  • Tests are independent and repeatable
  • Test names describe what they verify
  • No flaky tests (tests that randomly fail)
  • Integration tests for critical paths
  • Test coverage meets project standards (typically 80%+)
  • Tests follow AAA pattern (Arrange, Act, Assert)
  • Mocks and stubs are used appropriately

Security

  • No sensitive data in logs (passwords, tokens, PII)
  • Input validation on all external inputs
  • SQL injection prevention (use parameterized queries)
  • XSS prevention (escape output, use CSP)
  • Authentication and authorization checks
  • No secrets in code (use environment variables)
  • HTTPS enforced for sensitive operations
  • Rate limiting on public endpoints
  • CSRF protection for state-changing operations
  • Dependency vulnerabilities checked

Performance

  • No unnecessary database queries (N+1 problem)
  • Appropriate use of caching
  • Efficient algorithms and data structures
  • Async operations for I/O-bound tasks
  • No blocking operations in hot paths
  • Database indexes for frequently queried columns
  • Pagination for large result sets
  • Connection pooling configured
  • Memory leaks prevented (resources closed)
  • No premature optimization

Architecture

  • Changes align with system architecture
  • Dependencies point in correct direction
  • No circular dependencies introduced
  • Appropriate separation of concerns
  • Interfaces/contracts are stable
  • Backward compatibility maintained
  • Database migrations are reversible
  • API changes are versioned

Providing Feedback

Constructive Comments

# Good feedback examples

## Suggestion (not demand)
"Consider using a dictionary here for O(1) lookups instead of the current list iteration. 
This would improve performance from O(n) to O(1) for the lookup operation."

## Question (not accusation)
"What's the reasoning behind using recursion here? I'm wondering if an iterative 
approach might be simpler and avoid potential stack overflow for large inputs."

## Explanation (with rationale)
"This pattern could be confusing because it mixes business logic with data access. 
Using the repository pattern would make it clearer by separating concerns and 
making the code more testable."

## Praise (when deserved)
"Nice solution! The error handling here is very thorough and the error messages 
are actionable. This will make debugging much easier."

## Offering alternatives
"Instead of manually parsing the JSON, we could use Pydantic models which would 
give us automatic validation and better type safety. Here's an example: [link]"

## Asking for clarification
"I'm not sure I understand the purpose of this function. Could you add a docstring 
explaining what it does and when it should be used?"

# Avoid

- "Change this" (imperative without explanation)
- "Wrong" (judgmental without context)
- "Why did you..." (accusatory tone)
- "This is bad" (unhelpful criticism)
- "Everyone knows..." (condescending)
- "Just use X" (dismissive of author's approach)

Feedback Severity Levels

## Blocking (must fix before merge)
**Blocking**: The database connection isn't closed in the error path. This will 
cause connection leaks in production.

## Major (should fix, but not blocking)
**Major**: This function has cyclomatic complexity of 25. Consider breaking it 
into smaller functions for maintainability.

## Minor (nice to have)
**Minor**: Consider extracting this magic number (86400) into a named constant 
like SECONDS_PER_DAY for clarity.

## Nit (style/preference, non-blocking)
**Nit**: Personal preference, but I find this more readable with the condition 
on a separate line.

## Question (seeking understanding)
**Question**: Why are we using a custom implementation instead of the standard 
library function? Is there a performance reason?

## Praise (positive feedback)
**Praise**: Excellent use of the strategy pattern here. This makes adding new 
payment methods trivial.

Review Response Template

## Code Review: Add Payment Processing Feature

### Summary
**Reviewed by**: @reviewer
**Files changed**: 12
**Lines added**: 450
**Lines removed**: 80
**Review time**: 45 minutes

### What Works Well
- Clean separation between payment gateway adapters
- Comprehensive test coverage (92%)
- Clear error messages with actionable guidance
- Good use of dependency injection for testability
- Proper logging at appropriate levels

### Suggestions

#### Major
1. **Payment retry logic**: The retry mechanism doesn't implement exponential backoff. 
   This could overwhelm the payment gateway during outages. Consider using a library 
   like `tenacity` or implementing exponential backoff manually.

2. **Transaction atomicity**: The order status update and payment record creation 
   aren't in a database transaction. If one fails, we'll have inconsistent state.

#### Minor
3. **Error handling**: Consider creating custom exception types (PaymentDeclinedError, 
   PaymentTimeoutError) instead of generic exceptions. This makes error handling 
   more precise.

4. **Configuration**: The payment gateway URLs are hardcoded. Move these to 
   configuration for easier environment management.

5. **Documentation**: Add docstrings to the public methods explaining parameters 
   and return values.

### Concerns

**Blocking**: Line 145 - The API key is logged in the error message. This is a 
security vulnerability. Remove sensitive data from logs.

**Blocking**: Line 203 - SQL query is vulnerable to injection. Use parameterized 
queries instead of string formatting.

### Questions

1. Why did we choose to implement our own retry logic instead of using the payment 
   gateway's built-in retry mechanism?

2. What's the expected behavior if the payment succeeds but the webhook notification 
   fails? Do we have a reconciliation process?

### Approval Status
⏸️ **Changes Requested** - Please address the two blocking issues, then I'll approve.

### Next Steps
1. Fix security issues (API key logging, SQL injection)
2. Add transaction wrapper for atomicity
3. Consider implementing exponential backoff
4. Update once fixed and I'll re-review

Common Issues to Watch For

Logic Errors

# Bug: Off-by-one error
for i in range(len(items) - 1):  # Wrong: misses last item
    process(items[i])

# Fixed
for i in range(len(items)):
    process(items[i])

# Better: Pythonic iteration
for item in items:
    process(item)

# Bug: Mutable default argument
def add_item(item, items=[]):  # Wrong: shared across calls
    items.append(item)
    return items

# Fixed
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# Bug: Incorrect boolean logic
if user.is_active and not user.is_banned or user.is_admin:  # Ambiguous!
    allow_access()

# Fixed: Use parentheses for clarity
if (user.is_active and not user.is_banned) or user.is_admin:
    allow_access()

# Bug: Race condition
if not file_exists(path):
    create_file(path)  # Another process might create it between check and create

# Fixed: Handle the race
try:
    create_file(path)
except FileExistsError:
    pass  # File was created by another process, that's fine

Security Issues

# Bug: SQL injection
query = f"SELECT * FROM users WHERE username = '{username}'"  # Dangerous!
db.execute(query)

# Fixed: Parameterized query
query = "SELECT * FROM users WHERE username = ?"
db.execute(query, (username,))

# Bug: Logging sensitive data
logger.info(f"User logged in: {username}, password: {password}")  # Never log passwords!

# Fixed
logger.info(f"User logged in: {username}")

# Bug: Hardcoded secret
API_KEY = "sk-1234567890abcdef"  # Never commit secrets!

# Fixed
API_KEY = os.getenv("API_KEY")
if not API_KEY:
    raise ValueError("API_KEY environment variable is required")

# Bug: No input validation
def transfer_money(amount):
    account.balance -= amount  # What if amount is negative?

# Fixed
def transfer_money(amount):
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if amount > account.balance:
        raise InsufficientFundsError()
    account.balance -= amount

Performance Issues

# Bug: N+1 query problem
users = db.query("SELECT * FROM users")
for user in users:
    posts = db.query(
        "SELECT * FROM posts WHERE user_id = ?", user.id
    )  # Query per user!

# Fixed: Eager loading or JOIN
users = db.query("""
    SELECT u.*, p.*
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
""")

# Bug: Inefficient algorithm
def find_duplicates(items):
    duplicates = []
    for i, item in enumerate(items):
        for j, other in enumerate(items):
            if i != j and item == other:
                duplicates.append(item)  # O(n²)
    return duplicates

# Fixed: Use set for O(n)
def find_duplicates(items):
    seen = set()
    duplicates = set()
    for item in items:
        if item in seen:
            duplicates.add(item)
        seen.add(item)
    return list(duplicates)

# Bug: Loading entire dataset into memory
def process_large_file(filename):
    data = open(filename).read()  # Loads entire file!
    for line in data.split('\n'):
        process(line)

# Fixed: Stream processing
def process_large_file(filename):
    with open(filename) as f:
        for line in f:  # Reads line by line
            process(line)

Testing Issues

# Bug: Test depends on execution order
class TestUser:
    def test_create_user(self):
        user = User.create("[email protected]")
        assert user.id == 1  # Assumes this runs first!
    
    def test_delete_user(self):
        user = User.get(1)  # Depends on previous test!
        user.delete()

# Fixed: Independent tests
class TestUser:
    def test_create_user(self):
        user = User.create("[email protected]")
        assert user.id is not None
        user.delete()  # Clean up
    
    def test_delete_user(self):
        user = User.create("[email protected]")  # Create own data
        user_id = user.id
        user.delete()
        assert User.get(user_id) is None

# Bug: Testing implementation instead of behavior
def test_calculate_total(self):
    cart = Cart()
    cart.items = [Item(price=10), Item(price=20)]  # Accessing internals!
    assert cart._calculate_subtotal() == 30  # Testing private method!

# Fixed: Test public interface
def test_calculate_total(self):
    cart = Cart()
    cart.add_item(Item(price=10))
    cart.add_item(Item(price=20))
    assert cart.total == 30  # Test public behavior

Automated Code Review Tools

Linters and Formatters

# Python
black .                    # Code formatter
flake8 .                   # Style checker
pylint src/                # Static analyzer
mypy src/                  # Type checker

# JavaScript/TypeScript
eslint src/                # Linter
prettier --write src/      # Formatter
tsc --noEmit              # Type checker

# Go
gofmt -w .                # Formatter
golint ./...              # Linter
go vet ./...              # Static analyzer

# Rust
cargo fmt                 # Formatter
cargo clippy              # Linter

Security Scanners

# Dependency vulnerabilities
npm audit                 # Node.js
pip-audit                 # Python
cargo audit               # Rust

# Static security analysis
bandit -r src/            # Python security
semgrep --config=auto .   # Multi-language
snyk test                 # Dependency scanning

# Secret detection
gitleaks detect           # Find committed secrets
trufflehog git file://.   # Secret scanner

Code Quality Tools

# Complexity analysis
radon cc src/ -a          # Python cyclomatic complexity
lizard src/               # Multi-language complexity

# Test coverage
pytest --cov=src tests/   # Python
jest --coverage           # JavaScript
go test -cover ./...      # Go

# Code duplication
jscpd src/                # Multi-language

CI/CD Integration

# GitHub Actions example
name: Code Review Checks

on: [pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run linters
        run: |
          black --check .
          flake8 .
          mypy src/
  
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tests
        run: |
          pytest --cov=src --cov-report=xml
      - name: Upload coverage
        uses: codecov/codecov-action@v3
  
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Security scan
        run: |
          bandit -r src/
          pip-audit
          gitleaks detect

Building Review Culture

For Authors

  1. Keep PRs small - Reviewable in 15-20 minutes (< 400 lines)
  2. Write clear descriptions - Explain what, why, and how
  3. Self-review first - Catch obvious issues before requesting review
  4. Respond to feedback - Don’t take it personally, engage constructively
  5. Ask questions - If you don’t understand feedback, ask for clarification
  6. Update promptly - Address feedback quickly to keep momentum
  7. Add context - Link to tickets, design docs, or related PRs
  8. Test thoroughly - Don’t rely on reviewers to find bugs
  9. Follow up - Ensure feedback is addressed, don’t leave comments unresolved
  10. Say thanks - Appreciate the reviewer’s time and insights

For Reviewers

  1. Review promptly - Within hours, not days (aim for < 24 hours)
  2. Be specific - Explain issues clearly with examples
  3. Be kind - Assume good intent, use collaborative language
  4. Focus on important issues - Don’t nitpick style (use automated tools)
  5. Approve when ready - Don’t block on minor issues
  6. Explain the “why” - Help authors learn, don’t just point out problems
  7. Offer alternatives - Suggest solutions, don’t just criticize
  8. Praise good work - Positive feedback motivates and teaches
  9. Be consistent - Apply same standards to all code
  10. Know when to discuss offline - Complex issues may need synchronous discussion

Team Practices

Review Assignment:

  • Rotate reviewers to spread knowledge
  • Assign domain experts for complex changes
  • Pair junior and senior reviewers for mentorship
  • Use CODEOWNERS file for automatic assignment

Review SLAs:

  • First response within 4 hours
  • Complete review within 24 hours
  • Urgent fixes reviewed within 1 hour
  • Track and report on review metrics

Review Size Guidelines:

  • Small: < 100 lines (quick review)
  • Medium: 100-400 lines (thorough review)
  • Large: > 400 lines (consider splitting)

Review Meetings:

  • Weekly review retrospectives
  • Discuss common issues and patterns
  • Share learning from reviews
  • Calibrate review standards

Review Metrics

Useful Metrics

Review Turnaround Time: Time from PR creation to merge Review Depth: Number of comments per PR Defect Escape Rate: Bugs found in production that passed review Review Coverage: Percentage of code changes reviewed Reviewer Distribution: How evenly reviews are distributed

Metrics to Avoid

Lines of Code Reviewed: Encourages large PRs Number of Comments: Encourages nitpicking Approval Rate: Discourages thorough reviews

Advanced Review Techniques

Pair Programming as Review

For complex or critical changes, consider pair programming instead of async review:

**Benefits**:
- Immediate feedback
- Knowledge transfer in real-time
- Faster iteration
- Builds relationships

**When to use**:
- Complex algorithms
- Critical security code
- Architectural changes
- Onboarding new team members

Mob Review

For architectural decisions or major refactorings, gather the team for a mob review:

**Process**:
1. Author presents the change (10 min)
2. Team reviews code together (30 min)
3. Discussion and decision (20 min)

**Benefits**:
- Entire team understands the change
- Faster consensus on controversial decisions
- Shared ownership

Review Checklists by Language

# Python-specific checklist
- [ ] Type hints on function signatures
- [ ] Docstrings follow Google/NumPy style
- [ ] No mutable default arguments
- [ ] Context managers for resource management
- [ ] List comprehensions for simple transformations
- [ ] f-strings for string formatting
- [ ] Pathlib for file operations
- [ ] Dataclasses for data containers
// JavaScript-specific checklist
- [ ] Const/let instead of var
- [ ] Arrow functions for callbacks
- [ ] Async/await instead of callbacks
- [ ] Destructuring for object/array access
- [ ] Template literals for strings
- [ ] Optional chaining (?.) for nested access
- [ ] Nullish coalescing (??) for defaults
- [ ] No == (use ===)
// Go-specific checklist
- [ ] Error handling on every error return
- [ ] Defer for cleanup (close, unlock)
- [ ] Context for cancellation
- [ ] Goroutine leaks prevented
- [ ] Race conditions checked (go run -race)
- [ ] Exported functions have comments
- [ ] Interfaces are small and focused

Conclusion

Effective code reviews improve code quality, spread knowledge, and build team cohesion. Use comprehensive checklists to ensure consistency, provide constructive feedback with clear rationale, and automate mechanical checks with linters and CI/CD. Keep PRs small for faster reviews, respond promptly to feedback, and foster a culture where reviews are about learning and collaboration, not gatekeeping. Track metrics to improve the process, but focus on outcomes (quality, knowledge sharing) rather than vanity metrics (lines reviewed, comment count).

Resources

Comments

Share this article

Scan to read on mobile

👍 Was this article helpful?