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.
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:
- PR template (recommended). Drop the checklist into
.github/pull_request_template.mdso 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. - 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.
- CODEOWNERS for gated sections. For the security-critical variant in the table below, use
CODEOWNERSto 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 / File | Issue | Severity | Action |
|---|---|---|---|
users.py:87 | email written to logs in plaintext | Blocker | Remove or mask before merge |
UserRepository.find() | `LIKE ’%‘ | name | |
get_user():34 | Comment restates the code | Nit | Optional 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
| Context | Adjustments | Notes |
|---|---|---|
| Frontend / UI code | Add accessibility, responsive design, and browser compatibility sections | Visual changes need manual verification |
| Data pipelines | Add data quality checks, schema compatibility, and backfill considerations | Data changes are harder to undo than code changes |
| Infrastructure / Terraform | Add state safety, plan review, and blast radius assessment | One misconfiguration can take down production |
| Security-critical code | Make security section mandatory and require security team sign-off | Finance, healthcare, and auth systems |
| Hotfix / incident response | Shorten checklist to security, correctness, and rollback only | Speed 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
- Keep the checklist visible. Embed it in the PR template so reviewers see it automatically
- Distinguish nits from blockers. Not every issue prevents merge; use severity labels to keep reviews moving
- Rotate reviewers. Checklists reduce the expertise gap, making it easier to spread review load
- Review the checklist quarterly. Remove items that are now automated; add items that keep slipping through
- Time-box reviews. If a review takes more than 30 minutes, the PR is probably too large
- 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
- Treating the checklist as a substitute for thinking. The checklist catches common omissions, not design flaws
- Making every item mandatory. This slows reviews without improving quality; only block on correctness, security, and tests
- Not updating the checklist. As tools improve, manual checks should be automated
- Reviewing alone. Pair reviews on critical changes catch issues solo reviewers miss
- Focusing only on the diff. Reviewers should also check that the PR description, tests, and documentation are consistent
- 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
- Google Engineering Practices — How to do a code review: the canonical process reference behind most of the heuristics here (PR size, turnaround time, nit handling).
- GitHub Docs — Reviewing proposed changes in a pull request: mechanics of PR reviews, suggested changes, and required approvals.
- CodeQL documentation: query reference for extending the automated analysis above beyond the default suites.
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.
Related Resources
Code Review Best Practices: Authors and Reviewers
A practical guide to useful code reviews: how to write reviewable code, give constructive feedback, and keep reviews fast and focused.
DocPull Request Template
A thorough pull request template to standardize code reviews and improve merge quality.
DocSecurity Review Checklist for PRs
Checklist for security checks during pull request review: input validation, authentication, authorization, secrets, dependencies, injection, XSS, CSRF, logging, and automated tooling integration with code examples for secure patterns.
DocGit Branching Strategy Document
A document template for defining Git workflow, branching conventions, merge requirements, and release procedures for engineering teams.
DocEngineering Handbook Template
A template for documenting team culture, development processes, technical standards, and operational practices in a single referenceable handbook.
DocBackend Engineer Onboarding Checklist
A thorough checklist for onboarding new backend engineers covering environment setup, codebase orientation, security training, and first-week goals.