StackPractices
beginner By Mathias Paulenko

Code Review Checklist Template

A structured checklist template for conducting consistent, thorough code reviews that catch bugs, improve readability, and share knowledge across the team.

Overview

A code review checklist is a standardized set of checks a reviewer applies to every pull request, covering the dimensions a team cares about: correctness, security, performance, tests, style, documentation, and operations. It’s the most useful quality gate in software delivery, but only when it’s consistent. Without a checklist, reviewers focus on what they personally care about: one engineer checks for SQL injection, another obsesses over variable names, and a third only looks at test coverage. A shared checklist makes sure every review covers what matters to the team, while leaving room for human judgment on design and architecture.

This template covers the reviewer’s side of the workflow. Pair it with a pull request template so authors submit review-ready PRs, and see the code review best practices guide for the process around it — reviewer load, turnaround expectations, and how to give feedback that lands. For changes that touch auth, payments, or user data, layer in the security review checklist for PRs.

When to Use

Use this checklist when:

  • You want to standardize what “done” means for code review across the team
  • New reviewers are unsure what to look for in a pull request
  • Reviews are taking too long because reviewers are unsure of scope
  • You’re onboarding a new team and need to establish shared standards quickly
  • You want to reduce post-merge defects and production incidents

Skip the full checklist when it would just train people to ignore it: solo-maintainer repos where you review your own PRs, typo or comment-only changes, and config-only PRs that CI already validates end to end. A heavyweight process on trivial changes is how checklists die.

Prerequisites

Before adopting this checklist:

  • The team agrees on which items are mandatory vs. optional
  • CI is configured to catch automated issues (linting, formatting, type checks)
  • Reviewers understand that checklists augment judgment; they don’t replace it
  • There’s a documented escalation path for disagreements between author and reviewer
  • The checklist is stored where reviewers can reference it easily (PR template, wiki, or pinned message)

Solution

# Code Review Checklist

> Reviewer: ______ | Author: ______ | PR: ______ | Date: ______

## 1. Logic and Correctness
- [ ] The code does what the PR description claims it does
- [ ] Edge cases are handled (empty inputs, nulls, timeouts, failures)
- [ ] No off-by-one errors, race conditions, or infinite loops
- [ ] Error paths are explicit and do not swallow exceptions silently
- [ ] Business logic matches the spec or ticket requirements

## 2. Security
- [ ] No secrets, tokens, or credentials in code
- [ ] User inputs are validated and sanitized
- [ ] Authorization checks exist for protected operations
- [ ] No SQL injection, XSS, or command injection vulnerabilities
- [ ] Dependencies added are vetted and from trusted sources

## 3. Performance
- [ ] No N+1 queries or obvious algorithmic inefficiencies
- [ ] Large data sets are paginated or streamed
- [ ] No unnecessary network calls or blocking I/O in hot paths
- [ ] Caching is used appropriately where applicable
- [ ] Resource leaks (connections, file handles, memory) are prevented

## 4. Testing
- [ ] Unit tests cover the new or changed logic
- [ ] Integration tests cover external dependencies and boundaries
- [ ] Edge cases and failure paths are tested, not just happy paths
- [ ] Tests are deterministic and do not rely on timing or order
- [ ] Test names describe behavior, not implementation

## 5. Maintainability and Style
- [ ] Code is readable without needing to ask the author
- [ ] Variable and function names are descriptive and consistent
- [ ] No duplicated logic that could be extracted or reused
- [ ] Comments explain "why," not "what" (the code shows what)
- [ ] Complexity is appropriate; over-engineering is flagged

## 6. Documentation
- [ ] Public APIs have updated documentation or OpenAPI specs
- [ ] README or runbooks are updated if behavior changed
- [ ] Breaking changes are called out explicitly in the PR description
- [ ] Migration steps are documented if schema or config changed

## 7. Deployment and Operations
- [ ] Feature flags are used for risky or irreversible changes
- [ ] Monitoring and alerts are added or updated for new paths
- [ ] Rollback procedure is understood and tested
- [ ] Database migrations are backward-compatible or have a plan

---

## Review Notes

| Line / File | Issue | Severity | Action |
|-------------|-------|----------|--------|
| ______ | ______ | Nit / Suggestion / Blocker | ______ |

**Final verdict:** Approve / Request changes / Comment
**Merge readiness:** Ready / Needs work / Blocked

## Reviewer Confirmation

- [ ] I read every changed line
- [ ] I ran the tests locally
- [ ] I verified the PR description matches the changes
- [ ] I checked for breaking changes beyond the author's claims

Companion files: the code-review-checklist-template companion repo has this checklist as a standalone file plus the automation workflow, validator, and metrics script from the Advanced Solutions section.

How the Checklist Fits the Review Flow

The checklist sits between automated checks and the merge decision. CI and static analysis run first and filter out mechanical problems — lint, types, test failures, known vulnerability patterns. The reviewer then works through the checklist on what’s left: the things that need judgment. If the verdict is “request changes,” the loop repeats on the next push.

Code review flow: a PR opens, automated checks (CI, lint, CodeQL, PR size) run first, the reviewer applies the checklist, then approves or requests changes; requested changes loop back to the author, and the checklist itself is reviewed quarterly

Two properties of this split matter. First, the checklist is for humans — anything a machine can verify belongs in CI, not on the list, which is why item 4 of “What works” below retires checks as tooling catches up. Second, the quarterly loop is what keeps the checklist honest: items that never fire are noise, and gaps you keep hitting in production belong on it.

How to Adopt It

The checklist only works if reviewers see it without going looking for it. Three placements cover most teams:

  1. PR template (recommended). Drop the checklist into .github/pull_request_template.md so every PR renders it for the reviewer. Keep the mandatory items near the top and trim optional ones — a template nobody finishes gets skipped. GitHub also lets you keep separate PR templates under .github/PULL_REQUEST_TEMPLATE/ if you want a short variant for docs or config changes.
  2. Wiki or engineering handbook. If your PR template is already crowded, link the checklist from it and keep the canonical version in the handbook. The downside is real: reviewers have to click through, so expect lower usage.
  3. CODEOWNERS for gated sections. For the security-critical variant in the table below, use CODEOWNERS to route auth, crypto, or payment paths to reviewers who can actually sign off — the checklist alone can’t enforce that.

Whatever the placement, agree on turnaround expectations up front. A common baseline is first-pass review within one business day for PRs under ~400 changed lines; larger PRs get a staged review or a polite request to split. Pairing the checklist with a review rotation spreads the load and stops one senior from becoming the permanent gatekeeper.

Example: A Filled Review

The Review Notes table is what turns a checklist into a conversation. Here’s how a real entry looks on a PR that adds a user endpoint:

Line / FileIssueSeverityAction
users.py:87email written to logs in plaintextBlockerRemove or mask before merge
UserRepository.find()`LIKE ’%‘name
get_user():34Comment restates the codeNitOptional cleanup

Final verdict: Request changes. Merge readiness: Blocked until the two blockers are fixed. The nits get fixed or explicitly skipped — either way they don’t hold the merge.

Notice the discipline: every blocker names a file and a concrete fix, so the author never has to guess what “request changes” means. That’s the difference between a checklist that moves code forward and a comment thread that stalls for two days.

Explanation

The checklist is organized by concern rather than by file type. Reviewers tend to get lost reading a diff line by line; evaluating the change against fixed dimensions — correctness, security, performance, testing, maintainability, documentation, operations — forces full coverage and produces comparable verdicts across reviewers.

The other decision that matters is separating what machines check from what humans check. Linting, formatting, and type errors are CI’s job; putting them on a human checklist wastes review minutes and teaches reviewers to skim. The checklist stays focused on what humans do better than tools: does the logic match the spec, are the tests testing the right things, will the next reader understand this.

Variants

ContextAdjustmentsNotes
Frontend / UI codeAdd accessibility, responsive design, and browser compatibility sectionsVisual changes need manual verification
Data pipelinesAdd data quality checks, schema compatibility, and backfill considerationsData changes are harder to undo than code changes
Infrastructure / TerraformAdd state safety, plan review, and blast radius assessmentOne misconfiguration can take down production
Security-critical codeMake security section mandatory and require security team sign-offFinance, healthcare, and auth systems
Hotfix / incident responseShorten checklist to security, correctness, and rollback onlySpeed matters; document what was skipped and why

Treat the table as a starting point, not a fork: teams do better with one checklist plus per-context sections than with five parallel templates that drift apart.

What works

  1. Keep the checklist visible. Embed it in the PR template so reviewers see it automatically
  2. Distinguish nits from blockers. Not every issue prevents merge; use severity labels to keep reviews moving
  3. Rotate reviewers. Checklists reduce the expertise gap, making it easier to spread review load
  4. Review the checklist quarterly. Remove items that are now automated; add items that keep slipping through
  5. Time-box reviews. If a review takes more than 30 minutes, the PR is probably too large
  6. Measure adoption, not just presence. If fewer than half of merged PRs show a completed checklist, the process is decorative — find out where it’s being skipped before adding more items

Common Mistakes

  1. Treating the checklist as a substitute for thinking. The checklist catches common omissions, not design flaws
  2. Making every item mandatory. This slows reviews without improving quality; only block on correctness, security, and tests
  3. Not updating the checklist. As tools improve, manual checks should be automated
  4. Reviewing alone. Pair reviews on critical changes catch issues solo reviewers miss
  5. Focusing only on the diff. Reviewers should also check that the PR description, tests, and documentation are consistent
  6. Reviewing test code less rigorously than production code. Fragile, non-deterministic tests, or tests that don’t assert the right things, give false confidence — apply the same checklist to test files:
# Anti-pattern: test that always passes
def test_user_creation():
    user = create_user(email="test@example.com")
    assert user is not None  # Doesn't verify email, id, or status

# Better: test that verifies behavior
def test_user_creation_sets_correct_fields():
    user = create_user(email="test@example.com")
    assert user.email == "test@example.com"
    assert user.id is not None
    assert user.status == "active"
    assert user.created_at is not None

Advanced Solutions

Automated code review with GitHub Actions and CodeQL

Combine the manual checklist with automated analysis to catch issues humans miss. This workflow runs CodeQL for vulnerability patterns, enforces lint and type checks, and warns on oversized PRs — the three automatable items that free the reviewer to focus on the checklist itself:

# .github/workflows/code-review-automation.yml
name: Automated Code Review
on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  codeql-analysis:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: python, javascript
      # autobuild is only needed for compiled languages
      - uses: github/codeql-action/analyze@v3

  lint-and-type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - name: Comment PR with lint results
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: "Linting or type checking failed. Please fix before requesting review."
            })

  pr-size-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Check PR size
        uses: actions/github-script@v7
        with:
          script: |
            // paginate: listFiles returns 30 files per page by default
            const files = await github.paginate(github.rest.pulls.listFiles, {
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
              per_page: 100,
            });
            const totalChanges = files.reduce(
              (sum, file) => sum + file.changes, 0
            );
            if (totalChanges > 500) {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.payload.pull_request.number,
                body: `This PR has ${totalChanges} changes. Consider splitting into smaller PRs for easier review.`
              });
            }

Review bot for checklist enforcement

Automatically verify that PRs meet checklist requirements before review. The validator below checks three mechanical rules — tests added alongside new source files, no secret-looking strings in added lines, and a complete PR description — which are exactly the kind of checks that waste reviewer attention when done by hand:

import re
import subprocess
from typing import Dict, List

class PRChecklistValidator:
    def __init__(self, pr_diff: str, pr_description: str):
        self.diff = pr_diff
        self.description = pr_description

    def check_tests_added(self) -> bool:
        """Verify that new code includes tests."""
        has_source = bool(re.search(r'\+\+\+ b/src/.*\.py', self.diff))
        has_tests = bool(re.search(r'\+\+\+ b/tests/.*\.py', self.diff))
        if has_source and not has_tests:
            return False
        return True

    def check_no_secrets(self) -> bool:
        """Check for common secret patterns in the diff."""
        secret_patterns = [
            r'(?i)api[_-]?key\s*=\s*["\'][^"\']+["\']',
            r'(?i)password\s*=\s*["\'][^"\']+["\']',
            r'(?i)secret\s*=\s*["\'][^"\']+["\']',
            r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',
            r'(?i)aws_secret_access_key\s*=\s*["\'][^"\']+["\']',
        ]
        added_lines = [line for line in self.diff.split('\n') if line.startswith('+')]
        for line in added_lines:
            for pattern in secret_patterns:
                if re.search(pattern, line):
                    return False
        return True

    def check_pr_description(self) -> bool:
        """Verify PR description has required sections."""
        required_sections = ["## Summary", "## Testing", "## Breaking Changes"]
        return all(section in self.description for section in required_sections)

    def run_all_checks(self) -> List[Dict]:
        """Run all checklist validations and return results."""
        checks = [
            ("Tests added for new code", self.check_tests_added),
            ("No secrets in diff", self.check_no_secrets),
            ("PR description complete", self.check_pr_description),
        ]
        results = []
        for name, check_fn in checks:
            passed = check_fn()
            results.append({
                "check": name,
                "passed": passed,
                "status": "PASS" if passed else "FAIL",
            })
        return results

# Example usage: diff and description of the current branch vs main
diff = subprocess.run(
    ["git", "diff", "origin/main...HEAD"],
    capture_output=True, text=True
).stdout
pr_description = subprocess.run(
    ["git", "log", "-1", "--pretty=%B"],
    capture_output=True, text=True
).stdout

validator = PRChecklistValidator(diff, pr_description)
results = validator.run_all_checks()
for r in results:
    print(f"{r['status']}: {r['check']}")

Review metrics dashboard

Track review cycle time, comment quality, and defect escape rate. Cycle time tells you whether reviews are a bottleneck; defect escape rate tells you whether the checklist is actually catching things or just adding ceremony:

from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Dict, List

@dataclass
class ReviewMetric:
    pr_number: int
    author: str
    reviewer: str
    created_at: datetime
    merged_at: datetime
    comments_count: int
    blockers_count: int
    post_merge_defects: int

def calculate_review_metrics(metrics: List[ReviewMetric]) -> Dict:
    """Calculate aggregate review metrics for the team."""
    if not metrics:
        return {}

    cycle_times = [(m.merged_at - m.created_at).total_seconds() / 3600 for m in metrics]
    comment_counts = [m.comments_count for m in metrics]
    defect_rate = sum(1 for m in metrics if m.post_merge_defects > 0) / len(metrics)

    by_reviewer = defaultdict(list)
    for m in metrics:
        by_reviewer[m.reviewer].append(m)

    reviewer_stats = {}
    for reviewer, reviews in by_reviewer.items():
        reviewer_stats[reviewer] = {
            "total_reviews": len(reviews),
            "avg_comments": sum(r.comments_count for r in reviews) / len(reviews),
            "avg_blockers": sum(r.blockers_count for r in reviews) / len(reviews),
            "defects_missed": sum(r.post_merge_defects for r in reviews),
        }

    return {
        "avg_cycle_time_hours": sum(cycle_times) / len(cycle_times),
        "avg_comments_per_pr": sum(comment_counts) / len(comment_counts),
        "defect_escape_rate": defect_rate,
        "by_reviewer": reviewer_stats,
    }

# Example usage
metrics = [
    ReviewMetric(101, "alice", "bob", datetime.now() - timedelta(hours=5), datetime.now(), 3, 1, 0),
    ReviewMetric(102, "charlie", "bob", datetime.now() - timedelta(hours=2), datetime.now(), 5, 2, 1),
    ReviewMetric(103, "alice", "dave", datetime.now() - timedelta(hours=8), datetime.now(), 1, 0, 0),
]

results = calculate_review_metrics(metrics)
print(f"Average cycle time: {results['avg_cycle_time_hours']:.1f}h")
print(f"Defect escape rate: {results['defect_escape_rate']:.0%}")

Two readings worth tracking quarterly: a falling defect_escape_rate means the checklist is working, and a wide spread in avg_cycle_time_hours across reviewers usually means the checklist is applied unevenly — a training problem, not a tooling one.

Further Reading

Frequently Asked Questions

How long should a code review take?

Small PRs (under 200 lines) should be reviewed within a few hours. Large PRs should be split or reviewed in stages. If a review consistently takes longer than 30 minutes, the team should investigate whether PRs are too large or the checklist is too broad.

Should junior engineers review senior engineers' code?

Yes. Code review is a learning opportunity as much as a quality gate. Junior reviewers catch clarity issues seniors overlook because they don't yet share the author's assumptions. The checklist levels the playing field by telling everyone what to check.

What if the author disagrees with a review comment?

Discuss it. If the conversation stalls, escalate to a tech lead or the team's documented tiebreaker. The checklist exists to reduce subjective debates by making expectations explicit, but it can't eliminate all disagreement.

What's the difference between a nit and a blocker?

A nit is a stylistic or minor improvement the author can take or leave — naming preferences, comment wording, an alternative idiom. A blocker is a correctness, security, or data-integrity problem that must be fixed before merge. Mark severity explicitly on every comment; mixing the two is what makes reviews feel arbitrary.

Doesn't CI already cover this checklist?

Only the mechanical third of it. Linters, type checkers, tests, and CodeQL catch style violations, broken builds, and known vulnerability patterns. They can't tell whether the code matches the ticket, whether a test asserts the right behavior, or whether a migration is actually reversible. The checklist covers the judgment calls — that's why the first prerequisite is wiring the automatable parts into CI.