
Python Code Review
- 677 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
python-code-review is a Claude agent skill that audits Python files for PEP8 violations, missing type hints, unsafe async patterns, and common exception-handling mistakes before merge.
About
python-code-review is a structured Python review skill with four reference guides: pep8-style, type-safety, async-patterns, and error-handling. The skill checks indentation, line length, naming, missing or incorrect type hints including Any misuse, blocking calls inside async code, missing await usage, bare except clauses, and weak logging practices when reviewing .py files. Developers reach for python-code-review during pull request review or agent-driven audits before merging Python services. The quick-reference table routes each issue type to focused markdown references so reviews stay consistent across modules.
- Reviews .py files against 5 reference checklists: PEP8 style, type safety, async patterns, error handling, and common mi
- Enforces 4-space indentation, ≤79 char lines, proper import grouping, and snake_case naming
- Flags missing type hints, inappropriate use of Any, blocking calls in async code, and bare except clauses
- Checks for mutable defaults, stray print statements, missing context managers, and poor logging
- Delivers severity-classified findings that feed directly into code-quality gates
Python Code Review by the numbers
- 677 all-time installs (skills.sh)
- Ranked #194 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill python-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 677 |
|---|---|
| repo stars | ★ 74 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
How do you review Python code for type and async issues?
Automatically audit Python files for PEP8 violations, missing type hints, unsafe async patterns, and common runtime mistakes before merging.
Who is it for?
Teams merging Python services who want consistent PEP8, typing, and async audits without manually rereading style guides each review.
Skip if: Non-Python codebases or projects where automated linters like Ruff and mypy already gate CI with no need for agent-guided review.
When should I use this skill?
A developer asks to review .py files, check type hints, audit async/await usage, or evaluate Python exception handling before merge.
What you get
A categorized review report referencing PEP8, type-safety, async-patterns, and error-handling findings across reviewed .py files.
- Categorized review findings
- PEP8 and typing issue list
- Async and exception-handling notes
By the numbers
- Bundles 4 reference guides: pep8-style, type-safety, async-patterns, and error-handling
- Maps 4 issue-type categories in its quick-reference review table
Files
Python Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| Indentation, line length, whitespace, naming | references/pep8-style.md |
| Missing/wrong type hints, Any usage | references/type-safety.md |
| Blocking calls in async, missing await | references/async-patterns.md |
| Bare except, missing context, logging | references/error-handling.md |
| Mutable defaults, print statements | references/common-mistakes.md |
Review Checklist
PEP8 Style
- [ ] 4-space indentation (no tabs)
- [ ] Line length ≤79 characters (≤72 for docstrings/comments)
- [ ] Two blank lines around top-level definitions, one within classes
- [ ] Imports grouped: stdlib → third-party → local (blank line between groups)
- [ ] No whitespace inside brackets or before colons/commas
- [ ] Naming:
snake_casefor functions/variables,CamelCasefor classes,UPPER_CASEfor constants - [ ] Inline comments separated by at least two spaces
Type Safety
- [ ] Type hints on all function parameters and return types
- [ ] No
Anyunless necessary (with comment explaining why) - [ ] Proper
T | Nonesyntax (Python 3.10+)
Async Patterns
- [ ] No blocking calls (
time.sleep,requests) in async functions - [ ] Proper
awaiton all coroutines
Error Handling
- [ ] No bare
except:clauses - [ ] Specific exception types with context
- [ ]
raise ... fromto preserve stack traces
Common Mistakes
- [ ] No mutable default arguments
- [ ] Using
loggernotprint()for output - [ ] f-strings preferred over
.format()or%
Valid Patterns (Do NOT Flag)
These patterns are intentional and correct - do not report as issues:
- Type annotation vs type assertion - Annotations declare types but are not runtime assertions; don't confuse with missing validation
- Using `Any` when interacting with untyped libraries - Required when external libraries lack type stubs
- Empty `__init__.py` files - Valid for package structure, no code required
- `noqa` comments - Valid when linter rule doesn't apply to specific case
- Using `cast()` after runtime type check - Correct pattern to inform type checker of narrowed type
Context-Sensitive Rules
Only flag these issues when the specific conditions apply:
| Issue | Flag ONLY IF |
|---|---|
| Generic exception handling | Specific exception types are available and meaningful |
| Unused variables | Variable lacks _ prefix AND isn't used in f-strings, logging, or debugging |
Gates (reporting workflow)
Complete in order. Do not advance until each pass condition is met.
1. Scope — Pass: You list every .py path (or explicit glob) you inspected this run. 2. False-positive screen — Pass: For each issue you plan to report, you checked Valid Patterns and Context-Sensitive Rules above; you drop or narrow the finding if those sections say not to flag it. 3. Evidence — Pass: Each remaining finding includes `[FILE:LINE]` (or a bounded line range). Symbols or short verbatim snippets may supplement the location anchor but do not replace it. 4. Verification protocol — Pass: You load review-verification-protocol and complete its mandatory steps for each reported issue before the user-facing write-up. 5. Ship — Pass: The user-visible output matches whatever structure that protocol requires (no issues-only dump that skips its checks).
When to Load References
- Reviewing code formatting/style → pep8-style.md
- Reviewing function signatures → type-safety.md
- Reviewing
async deffunctions → async-patterns.md - Reviewing try/except blocks → error-handling.md
- General Python review → common-mistakes.md
Review Questions
1. Does the code follow PEP8 formatting (indentation, line length, whitespace)? 2. Are imports properly grouped (stdlib → third-party → local)? 3. Do names follow conventions (snake_case, CamelCase, UPPER_CASE)? 4. Are all function signatures fully typed? 5. Are async functions truly non-blocking? 6. Do exceptions include meaningful context? 7. Are there any mutable default arguments?
Before reporting: complete Gates (reporting workflow) above (especially gate 4).
Async Patterns
Critical Anti-Patterns
1. Blocking Calls in Async Functions
Problem: Blocks the event loop, defeats async benefits.
# BAD - blocks event loop
async def fetch_data():
response = requests.get(url) # BLOCKING!
time.sleep(1) # BLOCKING!
return response.json()
# GOOD - non-blocking
async def fetch_data():
async with httpx.AsyncClient() as client:
response = await client.get(url)
await asyncio.sleep(1)
return response.json()2. Missing await on Coroutines
Problem: Coroutine never executes.
# BAD - coroutine created but never awaited
async def process():
fetch_data() # Returns coroutine, doesn't execute!
# GOOD
async def process():
await fetch_data()3. Sequential Instead of Concurrent
Problem: Misses parallelization opportunity.
# BAD - sequential (slow)
async def get_all():
user = await get_user()
posts = await get_posts()
comments = await get_comments()
return user, posts, comments
# GOOD - concurrent (fast)
async def get_all():
user, posts, comments = await asyncio.gather(
get_user(),
get_posts(),
get_comments()
)
return user, posts, comments4. Missing async with for Async Context Managers
Problem: Resource not properly managed.
# BAD
async def query():
session = aiosqlite.connect(db) # Not entered!
return await session.execute(sql)
# GOOD
async def query():
async with aiosqlite.connect(db) as session:
return await session.execute(sql)5. Sync File I/O in Async Context
Problem: File operations block event loop.
# BAD - blocks event loop
async def read_config():
with open("config.json") as f:
return json.load(f)
# GOOD - use aiofiles
import aiofiles
async def read_config():
async with aiofiles.open("config.json") as f:
content = await f.read()
return json.loads(content)
# ACCEPTABLE - for small files, run in executor
async def read_config():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, load_config_sync)Review Questions
1. Are there any requests, time.sleep, or open() calls in async functions? 2. Is every coroutine call awaited? 3. Are independent async calls parallelized with gather()? 4. Are async context managers used with async with?
Common Mistakes
Critical Anti-Patterns
1. Mutable Default Arguments
Problem: Default value is shared across all calls.
# BAD - same list reused!
def add_item(item, items=[]):
items.append(item)
return items
add_item("a") # ["a"]
add_item("b") # ["a", "b"] - unexpected!
# GOOD
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
# BETTER - using dataclass
from dataclasses import dataclass, field
@dataclass
class Container:
items: list = field(default_factory=list)2. Using print() for Logging
Problem: No log levels, no timestamps, hard to filter.
# BAD
print(f"Processing {item}")
print(f"Error: {e}")
# GOOD
from loguru import logger
logger.info(f"Processing {item}")
logger.error(f"Error: {e}")3. String Formatting Inconsistency
Problem: Mixing formats reduces readability.
# BAD - mixed formats
msg = "Hello %s" % name
msg = "Hello {}".format(name)
msg = f"Hello {name}"
# GOOD - f-strings consistently
msg = f"Hello {name}"
total = f"Count: {count:,}" # with formatting
path = f"{base}/{sub}/{file}"4. Unused Variables
Problem: Dead code, confusing to readers.
# BAD
result = process() # never used
# GOOD - use underscore for intentionally ignored
_, second, _ = get_triple()
# Or just don't assign
process() # if result not needed5. Import Order
Problem: Hard to scan, may cause issues.
# BAD - random order
from myapp.utils import helper
import os
from typing import Optional
import sys
from myapp.models import User
# GOOD - standard order
import os
import sys
from typing import Optional
from myapp.models import User
from myapp.utils import helper6. Magic Numbers
Problem: Unclear intent, hard to maintain.
# BAD
if len(items) > 100:
paginate()
time.sleep(3600)
# GOOD
MAX_PAGE_SIZE = 100
CACHE_TTL_SECONDS = 3600
if len(items) > MAX_PAGE_SIZE:
paginate()
time.sleep(CACHE_TTL_SECONDS)7. Nested Conditionals
Problem: Hard to read and maintain.
# BAD
def process(user):
if user:
if user.active:
if user.verified:
return do_work(user)
return None
# GOOD - early returns
def process(user):
if not user:
return None
if not user.active:
return None
if not user.verified:
return None
return do_work(user)Review Questions
1. Are there any mutable default arguments (list, dict, set)? 2. Is print() used instead of logger? 3. Are f-strings used consistently? 4. Are there magic numbers that should be constants? 5. Are deeply nested conditionals flattened with early returns?
Error Handling
Critical Anti-Patterns
1. Bare Except Clause
Problem: Catches everything including KeyboardInterrupt, SystemExit.
# BAD
try:
process()
except:
pass
# GOOD - specific exception
try:
process()
except ValueError as e:
logger.error(f"Invalid value: {e}")
raise
# ACCEPTABLE - if you must catch all
try:
process()
except Exception as e: # Still allows KeyboardInterrupt
logger.error(f"Unexpected error: {e}")
raise2. Swallowing Exceptions
Problem: Hides errors, makes debugging impossible.
# BAD
try:
result = risky_operation()
except Exception:
pass # Error silently ignored!
# GOOD - log and handle
try:
result = risky_operation()
except OperationError as e:
logger.warning(f"Operation failed: {e}")
result = default_value3. Losing Exception Context
Problem: Original stack trace lost.
# BAD - loses original traceback
try:
parse_config()
except ValueError:
raise ConfigError("Invalid config")
# GOOD - preserves chain
try:
parse_config()
except ValueError as e:
raise ConfigError("Invalid config") from e4. Missing Context in Error Messages
Problem: Can't diagnose issue from logs.
# BAD
except KeyError:
raise ValueError("Missing key")
# GOOD - include context
except KeyError as e:
raise ValueError(f"Missing required key: {e.args[0]}") from e5. Not Logging Before Re-raising
Problem: Exception might be caught elsewhere without logging.
# BAD - no record if caught upstream
try:
process(item)
except ProcessError:
raise
# GOOD - log before re-raising
try:
process(item)
except ProcessError as e:
logger.error(f"Failed to process item {item.id}: {e}")
raiseLogging Best Practices
from loguru import logger
# BAD
print(f"Processing {item}")
print(f"Error: {e}")
# GOOD
logger.debug(f"Processing item {item.id}")
logger.info(f"Completed batch of {count} items")
logger.warning(f"Retry {attempt}/3 for {operation}")
logger.error(f"Failed to process {item.id}: {e}")
# With exception info
logger.exception(f"Unexpected error processing {item.id}")Review Questions
1. Are there any bare except: clauses? 2. Is exception context preserved with raise ... from e? 3. Do error messages include enough context to diagnose? 4. Is logging used instead of print statements?
PEP8 Style Guide
Indentation
Rule: Use 4 spaces per indentation level. Never use tabs.
# BAD - 2 spaces
def foo():
return bar
# BAD - tabs (shown with → for visibility)
def foo():
→ return bar # ← actual code would have tab here
# GOOD - 4 spaces
def foo():
return barContinuation Lines
# GOOD - aligned with opening delimiter
result = function_name(arg_one, arg_two,
arg_three, arg_four)
# GOOD - hanging indent
result = function_name(
arg_one, arg_two,
arg_three, arg_four,
)
# BAD - no alignment
result = function_name(arg_one, arg_two,
arg_three, arg_four)Line Length
Rule: Maximum 79 characters for code, 72 for docstrings/comments.
# BAD - too long
result = some_function(argument_one, argument_two, argument_three, argument_four, argument_five)
# GOOD - broken across lines
result = some_function(
argument_one,
argument_two,
argument_three,
argument_four,
argument_five,
)Blank Lines
Rule: Two blank lines around top-level definitions, one blank line around methods.
# GOOD
import os
class MyClass:
"""Docstring."""
def method_one(self):
pass
def method_two(self):
pass
def top_level_function():
pass
def another_function():
passImports
Rule: Group imports in order: stdlib → third-party → local. One blank line between groups.
# GOOD
import os
import sys
from pathlib import Path
import requests
from pydantic import BaseModel
from myapp.models import User
from myapp.utils import helperRule: Avoid wildcard imports.
# BAD
from module import *
# GOOD
from module import specific_function, SpecificClassWhitespace
Inside Brackets
# BAD
spam( ham[ 1 ], { eggs: 2 } )
# GOOD
spam(ham[1], {eggs: 2})Before Colons and Commas
# BAD
if x == 4 :
print(x , y)
# GOOD
if x == 4:
print(x, y)Around Operators
# BAD
x=1
y = x+1
z = x +1
# GOOD
x = 1
y = x + 1
# Exception: indicate precedence
result = x*2 + y*3Function Arguments
# BAD
def function(arg1 = None, arg2 = 0):
pass
# GOOD
def function(arg1=None, arg2=0):
passNaming Conventions
| Type | Convention | Example |
|---|---|---|
| Functions/variables | snake_case | my_function, user_count |
| Classes | CamelCase | MyClass, HttpClient |
| Constants | UPPER_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| Private | Leading underscore | _internal_method |
| "Protected" | Double underscore | __name_mangled |
# BAD
def MyFunction(): # should be snake_case
pass
class my_class: # should be CamelCase
maxSize = 100 # should be MAX_SIZE if constant
# GOOD
def my_function():
pass
class MyClass:
MAX_SIZE = 100Comments
Inline Comments
Rule: Separate by at least two spaces. Use sparingly.
# BAD
x = x + 1# increment
# GOOD
x = x + 1 # compensate for borderBlock Comments
# GOOD - aligned with code, complete sentences
# This is a block comment explaining the
# following code section. Each sentence
# ends with a period.
result = complex_operation()Docstrings
# GOOD
def fetch_users(limit: int = 100) -> list[User]:
"""Fetch users from the database.
Args:
limit: Maximum number of users to return.
Returns:
List of User objects.
Raises:
DatabaseError: If connection fails.
"""
passReview Questions
1. Is indentation consistently 4 spaces (no tabs)? 2. Are lines ≤79 characters (≤72 for docstrings)? 3. Are there two blank lines around top-level definitions? 4. Are imports grouped correctly with blank lines between groups? 5. Is there extraneous whitespace inside brackets or around operators? 6. Do names follow conventions (snake_case, CamelCase, UPPER_CASE)? 7. Are inline comments separated by at least two spaces?
Type Safety
Critical Anti-Patterns
1. Missing Return Type
Problem: Callers don't know what to expect.
# BAD
def get_user(id: int):
return User.query.get(id)
# GOOD
def get_user(id: int) -> User | None:
return User.query.get(id)2. Using Any Without Justification
Problem: Defeats the purpose of type checking.
# BAD
def process(data: Any) -> Any:
return data
# GOOD - with justification
def process(data: Any) -> dict: # Any: accepts JSON from external API
return json.loads(data)
# BETTER - use proper types
def process(data: str | bytes) -> dict:
return json.loads(data)3. Optional vs Union Syntax
Problem: Inconsistent syntax, less readable.
# OLD (pre-3.10)
from typing import Optional, Union
def find(id: int) -> Optional[User]: ...
def parse(val: Union[str, int]) -> str: ...
# GOOD (3.10+)
def find(id: int) -> User | None: ...
def parse(val: str | int) -> str: ...4. Missing Generic Types
Problem: Loses type information in collections.
# BAD
def get_items() -> list:
return [Item(...)]
# GOOD
def get_items() -> list[Item]:
return [Item(...)]
# BAD
def get_config() -> dict:
return {"key": "value"}
# GOOD
def get_config() -> dict[str, str]:
return {"key": "value"}5. TypedDict for Structured Dicts
Problem: Plain dict loses key/value type information.
# BAD
def get_user_data() -> dict:
return {"name": "Alice", "age": 30}
# GOOD
from typing import TypedDict
class UserData(TypedDict):
name: str
age: int
def get_user_data() -> UserData:
return {"name": "Alice", "age": 30}Review Questions
1. Are all function parameters typed? 2. Are all return types specified? 3. Is Any used only when necessary with a comment? 4. Are collection types generic (list[T], dict[K, V])? 5. Is T | None used instead of Optional[T]?
Related skills
How it compares
Use python-code-review for agent-guided qualitative audits; pair with Ruff or mypy in CI when you need enforced automated gates.
FAQ
What issue types does python-code-review cover?
python-code-review covers PEP8 style, type-safety including Any misuse, async patterns such as blocking calls and missing await, and error-handling problems like bare except clauses.
How is python-code-review organized?
python-code-review uses a quick-reference table linking issue types to four markdown references—pep8-style, type-safety, async-patterns, and error-handling—for consistent reviews.
Is Python Code Review safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.