Python Sensei — Best Practices Enforcer
Version: 1.1.0 | Author: Shadows Company | License: MIT
WHEN TO TRIGGER
- Writing new Python code
- Reviewing existing Python code
- User says "python review", "best practices", "clean up this python"
- Refactoring Python modules
- Setting up a new Python project
WHEN NOT TO TRIGGER
- Quick scripts where quality doesn't matter
- User explicitly says "just make it work"
- Non-Python code
PREREQUISITES
Requires python or python3 on PATH for syntax checking (python -m py_compile) and test execution (python -m pytest).
Optional tools (auto-detected, recommendations adapt accordingly):
- pytest — test execution (
pip install pytest) - mypy or pyright — static type checking
- ruff — fast linting and formatting (
pip install ruff)
The skill checks which tools are available and tailors its recommendations to the user's installed toolchain.
CODE STANDARDS
1. Module Structure
"""Module docstring — one line describing purpose."""
# Standard library imports
import os
from pathlib import Path
# Third-party imports
import httpx
from pydantic import BaseModel
# Local imports
from .config import Settings
# Constants
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
# Module code...
Rules:
- Max 500 lines per module — split if larger
- Imports grouped: stdlib → third-party → local (blank line between each group)
- Constants at top, ALL_CAPS naming
- One class per file for complex classes
2. Functions
async def fetch_user(user_id: str, *, include_profile: bool = False) -> User | None:
"""Fetch a user by ID.
Args:
user_id: The unique user identifier.
include_profile: Whether to include full profile data.
Returns:
User object if found, None otherwise.
Raises:
ConnectionError: If the API is unreachable.
"""
Rules:
- Type hints on all public functions (params + return)
- Keyword-only args after
*for clarity Nonereturn type when failure is normal (not exceptions)- Docstrings on public functions only
- Max 30 lines per function — extract helpers if larger
3. Data Models
from dataclasses import dataclass, field
from enum import StrEnum
class Status(StrEnum):
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
@dataclass
class User:
id: str
name: str
status: Status = Status.ACTIVE
tags: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"status": self.status.value,
"tags": self.tags,
}
Rules:
@dataclassfor simple models, Pydantic for validation-heavy modelsStrEnumfor states (JSON-serializable)- Always include
to_dict()for serialization - Immutable by default (
frozen=Truewhen appropriate)
4. Error Handling
# GOOD: Specific exceptions, minimal try blocks
try:
response = await client.get(url)
response.raise_for_status()
except httpx.TimeoutException:
logger.warning("Request timed out: %s", url)
return None
except httpx.HTTPStatusError as e:
logger.error("HTTP %d: %s", e.response.status_code, url)
raise
# BAD: Catching everything
try:
do_everything()
except Exception:
pass # Never do this
Rules:
- Catch specific exceptions, never bare
except: - Only validate at system boundaries (user input, external APIs)
- Trust internal code — do not add defensive checks everywhere
- Use
loggingmodule, notprint()for errors
5. Async Patterns
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks, return_exceptions=True)
return [r.json() for r in responses if not isinstance(r, Exception)]
Rules:
async withfor resource managementasyncio.gather()for parallel I/Oreturn_exceptions=Trueto handle partial failures- Never mix sync and async I/O in the same function
6. Testing
import pytest
class TestUserService:
def test_create_user_with_valid_data(self):
user = create_user(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert user.status == Status.ACTIVE
def test_create_user_rejects_empty_name(self):
with pytest.raises(ValueError, match="name cannot be empty"):
create_user(name="", email="alice@example.com")
@pytest.mark.asyncio
async def test_fetch_user_returns_none_for_missing(self):
result = await fetch_user("nonexistent-id")
assert result is None
Rules:
- Test file:
tests/test_{module}.py - Test names describe the scenario:
test_[action]_[condition]_[expected] - One assertion per test (prefer)
- Use
pytest.raisesfor expected exceptions - Use fixtures for shared setup
7. Project Setup
project/
src/
project_name/
__init__.py
main.py
config.py
models.py
tests/
test_main.py
test_models.py
pyproject.toml
requirements.txt
.gitignore
pyproject.toml over setup.py. Pin major versions in requirements.txt.
ANTI-PATTERNS TO FLAG
| Anti-Pattern | Fix |
|---|---|
import * | Explicit imports |
| Mutable default args | field(default_factory=list) |
| Global mutable state | Dependency injection |
| Nested try/except | Extract and flatten |
| String concatenation for SQL | Parameterized queries |
type() checks | isinstance() |
os.path | pathlib.Path |
requests (sync) | httpx (async-ready) |
SECURITY CONSIDERATIONS
This skill reads and writes Python source files within the working directory. It does not access files outside the project scope.
- Commands executed:
python -m py_compile <file>for syntax checking,python -m pytest <test_file>for test execution. These run local project code — only use on trusted repositories or in sandboxed environments. - Data read: Source files in the working directory only. No access to secrets, credentials, or system files.
- Network access: None required. The skill operates entirely offline.
- Credentials: None stored or accessed.
- Persistence: All modifications are to source files in the working directory only. No global config changes.
- Sandboxing: Recommended to run in a virtual environment (
venv) to isolate dependencies.
OUTPUT FORMAT
Reviews and code output follow the standards defined above. Each review identifies specific anti-patterns with line references and provides corrected code blocks.
RULES
- Type hints everywhere — public functions must have full type annotations
- 500 lines max — split large modules into focused sub-modules
- Test every module —
tests/test_{module}.pywith descriptive test names - Async by default — use async for any I/O operation
- pathlib over os.path — modern Python path handling
- No print debugging — use
loggingmodule with appropriate levels
Published by Shadows Company — "We work in the shadows to serve the Light."