In the fast-paced landscape of software development in 2025, AI coding assistants generate boilerplate faster than ever. However, the role of the Senior Python Developer has never been more critical. While tools can generate code, humans must ensure architecture, security, and maintainability.
A robust code review process is the firewall between “it works” and “it is production-ready.” It is not just about catching bugs; it is about knowledge sharing and maintaining a consistent codebase.
This article provides a definitive 10-point checklist for reviewing Python code, tailored for the modern stack (Python 3.12+), designed to help you catch subtle issues that automated linters often miss.
Prerequisites: The Automated Baseline #
Before a human ever looks at the code, your CI/CD pipeline should have already validated the basics. We shouldn’t waste human cognitive load on things a machine can do.
Ensure your project uses a pyproject.toml configured with modern tooling. In 2025, Ruff has largely consolidated the linter market.
Recommended Tooling Setup:
- Linter/Formatter: Ruff (replaces Flake8, Black, Isort)
- Type Checker: MyPy (or Pyright) in strict mode
- Security Scanner: Bandit
Example pyproject.toml Configuration
#
[project]
name = "code-review-demo"
version = "2025.1.0"
requires-python = ">=3.12"
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "B", "SIM", "I"] # Error, Pyflakes, Bugbear, Simplify, Isort
ignore = []
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = trueThe Code Review Workflow #
Understanding where the human review fits into the pipeline is crucial.
Once the code reaches the “Human Code Review” stage, apply the following 10 checkpoints.
1. Modern Type Hinting & Strictness #
Python is dynamically typed, but modern enterprise Python is gradually typed. In 2025, type hints are not documentation; they are a contract.
What to check:
- Are
Anytypes used lazily? (Red flag). - Are generic types specific? (e.g.,
list[int]instead of justlist). - Does the code use the modern
typekeyword (PEP 695)?
Bad Code:
# Vague typing
def process_data(data: dict) -> list:
results = []
for key, value in data.items():
results.append(value * 2)
return resultsClean Code:
# Explicit typing with modern aliases
type UserData = dict[str, int]
def process_data(data: UserData) -> list[int]:
results: list[int] = []
for value in data.values():
results.append(value * 2)
return results2. Preventing Mutable Default Arguments #
This is the oldest trick in the Python book, yet it still appears in code reviews constantly. Default arguments are evaluated only once at function definition time, not at call time.
What to check:
- Look for
list,dict, or class instances as default parameters.
The Trap:
def add_log(message: str, history: list[str] = []) -> list[str]:
# 'history' persists across function calls!
history.append(message)
return historyThe Fix:
def add_log(message: str, history: list[str] | None = None) -> list[str]:
if history is None:
history = []
history.append(message)
return history3. Cognitive Complexity and Nesting #
If you see an “Arrowhead” pattern (code indented heavily to the right), request a refactor. High cyclomatic complexity makes testing difficult and bugs likely.
What to check:
- Can
if/elseblocks be replaced by Guard Clauses (Early Returns)? - Are loops nested more than 2 levels deep?
Refactoring Example:
# Before: Hard to follow
def verify_access(user):
if user.is_active:
if user.has_permission("admin"):
if not user.is_locked:
return True
else:
return False
else:
return False
else:
return False
# After: Guard Clauses
def verify_access(user) -> bool:
if not user.is_active:
return False
if not user.has_permission("admin"):
return False
if user.is_locked:
return False
return True4. Proper Use of Exceptions #
Code that swallows errors makes debugging a nightmare in production.
What to check:
- Never allow
except:orexcept Exception:without re-raising or logging extensively. - Are custom exceptions used for domain-specific errors?
Bad Code:
try:
result = api_call()
except:
# Silent failure. We will never know why this failed.
passClean Code:
import logging
class APIConnectionError(Exception):
"""Custom exception for API failures."""
pass
try:
result = api_call()
except ConnectionError as e:
logging.error(f"Failed to connect to API: {e}")
raise APIConnectionError("External service unavailable") from e5. Modern Data Structures & Performance #
Python lists are great, but they are not always the right tool. Using the wrong data structure is a common source of performance degradation in large applications.
What to check:
- Is the code searching for items in a large
list? (O(n)). Suggest aset(O(1)). - Are strings being concatenated in a loop with
+? Suggest"".join().
Comparison Table: Common Data Structure Swaps
| Scenario | Common (Poor) Choice | Better Choice | Why? |
|---|---|---|---|
| Checking existence | if item in list: |
if item in set: |
O(1) lookup vs O(n) |
| FIFO Queue | list.pop(0) |
collections.deque |
O(1) pop vs O(n) shift |
| Immutable Data | dict |
NamedTuple / dataclass(frozen=True) |
Memory efficiency & Safety |
| Counting items | dict loop |
collections.Counter |
Optimized C-implementation |
6. Resource Management (Context Managers) #
Resource leaks (file handles, database connections, thread locks) crash servers. Python solves this elegantly with Context Managers.
What to check:
- Does
open()have a correspondingclose()? (It shouldn’t—it should be in awithblock). - Are locks acquired and released manually?
Clean Code:
from threading import Lock
lock = Lock()
# Auto-release lock even if errors occur
with lock:
critical_section_update()
# Auto-close file
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()7. Security: Secrets and Injection #
In 2025, security reviews are mandatory.
What to check:
- Hardcoded Secrets: Are API keys, passwords, or tokens strings in the code? (Major Fail).
- SQL Injection: Are SQL queries constructed using f-strings?
Bad Code (SQL Injection Vulnerability):
user_input = "admin'; --"
# DANGEROUS: Never do this
query = f"SELECT * FROM users WHERE name = '{user_input}'"Clean Code:
# Use parameterized queries provided by your ORM or DB driver
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))8. Pythonic Iteration #
Reviewers should encourage “Pythonic” idioms over C-style iteration.
What to check:
- Use
enumerate(items)instead ofrange(len(items)). - Use
zip(a, b)instead of indexing two lists. - Use Dictionary Comprehensions for transforming dictionaries.
Refactoring:
names = ["Alice", "Bob"]
ages = [30, 25]
# C-Style (Avoid)
for i in range(len(names)):
print(f"{names[i]} is {ages[i]}")
# Pythonic
for name, age in zip(names, ages, strict=True):
print(f"{name} is {age}")Note: strict=True was added in Python 3.10 to ensure iterables are the same length.
9. Dependency Management & Imports #
A messy import section usually indicates a messy module structure.
What to check:
- Wildcard Imports:
from module import *pollutes the namespace and confuses linters. - Circular Imports: Are logic flows forcing imports inside functions? This suggests a need to extract code to a third module.
- Standard Library: Is the developer using
os.path(old) instead ofpathlib(modern)?
Modern Usage:
from pathlib import Path
# Instead of os.path.join
config_path = Path.home() / "app" / "config.json"10. Test Coverage & Logic #
Finally, automated tests check if the code runs; reviewers check if the tests make sense.
What to check:
- Happy Path vs. Edge Cases: Did they only test the perfect scenario? What happens if the network is down? What if the input is empty?
- Mocking: Are they mocking too much? If you mock the database, the network, and the file system, you might just be testing your mocks, not your logic.
Summary #
Code review is a conversation, not an interrogation. When you spot these issues, provide the “Why” alongside the “How” to help your team grow.
Quick Recap:
- Type Hints: Use strict, modern aliases.
- Defaults: No mutable default arguments.
- Complexity: Flatten nested
ifs with Guard Clauses. - Exceptions: Catch specific errors, never bare
except. - Structures: Use Sets for lookups, Deques for queues.
- Resources: Always use
withstatements. - Security: No f-strings in SQL; no secrets in code.
- Idioms: Use
zip,enumerate, andpathlib. - Imports: No wildcards.
- Tests: Check edge cases.
By integrating this checklist into your 2025 workflow, you ensure that your Python codebase remains robust, readable, and ready for scale.
Found this checklist useful? Subscribe to Python DevPro for more deep dives into advanced Python architecture and performance tuning.