| name | python-pro |
| description | Modern Python with uv, ruff, pyright, strict typing, and clean module design. Use when implementing, debugging, refactoring, or reviewing Python code; designing module boundaries; setting up Python projects; resolving type errors; tightening pyproject.toml; choosing between dataclasses and Pydantic; or working with async code, pytest, or packaging. Applies to any Python work unless a more specific role overrides. |
Python Pro
Senior-level Python expertise for production projects. Focuses on modern tooling, strict type checking, and Pythonic idioms.
When Invoked
- Review
pyproject.toml for project conventions and tooling config
- For build system setup, invoke the just-pro skill
- Apply Python idioms and established project patterns
Core Standards
Required:
- All public functions/classes have docstrings
- All functions have type annotations (params + return)
- ruff passes with project configuration
- pyright passes in strict mode
- Meaningful tests with pytest
Foundational Principles:
- Single Responsibility: One module = one purpose, one function = one job
- No God Classes: Split large classes; if it has 10+ methods, decompose
- Dependency Injection: Pass dependencies, don't create them internally
- Explicit over Implicit: Clear is better than clever
Project Setup (Python 3.12+)
Version Management
Pin Python version with mise: mise use python@3.12 (creates .mise.toml — commit it). Team members run mise install. See mise skill for setup.
New Project Quick Start with uv
uv is the modern Python package manager (10-100x faster than pip).
uv init project-name
cd project-name
uv init
uv add httpx pydantic
uv add --dev pytest pytest-cov pytest-asyncio ruff pyright
mkdir -p src/projectname tests
mv *.py src/projectname/ 2>/dev/null || true
touch src/projectname/__init__.py tests/__init__.py tests/conftest.py
just check
pyproject.toml Requirements
Note: uv init creates a minimal pyproject.toml. For src layout to work, add these sections:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/projectname"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Without [build-system] and [tool.hatch.build.targets.wheel], tests cannot import your package.
Developer Onboarding
git clone <repo> && cd <repo>
just setup
just check
Or manually:
mise trust && mise install
uv sync
Why uv? Lockfile-based reproducibility, automatic venv management, 10-100x faster than pip.
Build System
Invoke the just-pro skill for build system setup. It covers:
- Simple repos vs monorepos
- Hierarchical justfile modules
- Python-specific templates
Why just? Consistent toolchain frontend between agents and humans. Instead of remembering uv run ruff check --fix ., use just fix.
Quality Assurance
Auto-Fix First - Always try auto-fix before manual fixes:
just fix
Verification:
just check
Handling Strict Pyright
Untyped Libraries
Some libraries lack type stubs. In pyright strict mode, use Any:
from typing import Any
import structlog
logger: Any = structlog.get_logger()
When to use Any:
- Library has no stubs and you can't create them
- Return types are dynamic/unpredictable
- Interfacing with weakly-typed external systems
Avoid # type: ignore - it silences all errors. Explicit Any is clearer.
TYPE_CHECKING Imports
Ruff's TCH rules flag imports used only for type hints. Move them to a TYPE_CHECKING block:
from __future__ import annotations
from typing import TYPE_CHECKING
from mypackage.service import run_service
if TYPE_CHECKING:
from mypackage.models import User
def process(user: User) -> None:
run_service(user)
Rules:
- Imports used at runtime stay at top level
- Imports used only in type hints go in
TYPE_CHECKING
from __future__ import annotations enables string-based forward refs
- This also reduces import cycles
Optional Field Access
When a field might be None, assert before accessing:
result.error_message.lower()
assert result.error_message is not None
result.error_message.lower()
if result.error_message:
result.error_message.lower()
Quick Reference
Type Annotations
from typing import TypeVar, Protocol
from collections.abc import Callable, Iterator, Sequence
def process(items: list[str], timeout: float = 30.0) -> dict[str, int]:
...
T = TypeVar("T")
def first(items: Sequence[T]) -> T | None:
return items[0] if items else None
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
def load_data(source: Readable) -> bytes:
return source.read()
Error Handling
class ValidationError(Exception):
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
def parse_config(path: str) -> Config:
try:
with open(path) as f:
data = json.load(f)
except FileNotFoundError:
raise ConfigError(f"Config file not found: {path}") from None
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in {path}: {e}") from e
return Config.from_dict(data)
from dataclasses import dataclass
@dataclass
class Ok[T]:
value: T
@dataclass
class Err[E]:
error: E
type Result[T, E] = Ok[T] | Err[E]
Data Classes and Pydantic
from dataclasses import dataclass, field
from pydantic import BaseModel, Field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str
age: int = Field(ge=0, le=150)
model_config = {"strict": True}
Async Patterns
import asyncio
from collections.abc import AsyncIterator
async def fetch_with_timeout(url: str, timeout: float = 10.0) -> bytes:
async with asyncio.timeout(timeout):
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.content
async def paginate(client: Client, url: str) -> AsyncIterator[Item]:
while url:
response = await client.get(url)
for item in response.items:
yield item
url = response.next_url
async def fetch_all(urls: list[str]) -> list[bytes | Exception]:
tasks = [fetch_with_timeout(url) for url in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
Context Managers
from contextlib import contextmanager, asynccontextmanager
from collections.abc import Generator, AsyncGenerator
@contextmanager
def temporary_config(overrides: dict[str, str]) -> Generator[None, None, None]:
original = config.copy()
config.update(overrides)
try:
yield
finally:
config.clear()
config.update(original)
@asynccontextmanager
async def database_transaction(db: Database) -> AsyncGenerator[Transaction, None]:
tx = await db.begin()
try:
yield tx
await tx.commit()
except Exception:
await tx.rollback()
raise
Testing with pytest
import pytest
from unittest.mock import Mock, patch
@pytest.mark.parametrize(
"input_val,expected",
[
("hello", "HELLO"),
("", ""),
("123", "123"),
],
ids=["normal", "empty", "digits"],
)
def test_uppercase(input_val: str, expected: str) -> None:
assert uppercase(input_val) == expected
@pytest.fixture
def sample_user() -> User:
return User(name="Test", email="test@example.com")
@pytest.fixture
def mock_client() -> Mock:
client = Mock(spec=APIClient)
client.get.return_value = {"status": "ok"}
return client
@pytest.mark.asyncio
async def test_fetch_data(mock_client: Mock) -> None:
result = await fetch_data(mock_client, "test-id")
assert result.status == "ok"
def test_invalid_input_raises() -> None:
with pytest.raises(ValueError, match="must be positive"):
process_value(-1)
Structured Logging
import logging
from typing import Any
import structlog
logger: Any = structlog.get_logger()
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
logger.info("request_processed", method="GET", path="/api/users", duration_ms=42)
logger.error("operation_failed", error=str(e), user_id=user_id)
bound_logger: Any = logger.bind(request_id=request_id, user_id=user_id)
bound_logger.info("starting_operation")
Package Organization
project/
├── src/
│ └── projectname/
│ ├── __init__.py
│ ├── api/ # HTTP handlers
│ ├── domain/ # Business logic
│ └── infra/ # External integrations
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── test_api/
│ └── test_domain/
├── pyproject.toml
├── pyrightconfig.json
├── uv.lock
└── justfile
Rules: Use src/ layout for installable packages. One module = one purpose. Avoid utils, common, helpers modules.
DX Patterns
Doctor Recipe with Version Validation
# Validate toolchain versions meet requirements
doctor:
#!/usr/bin/env bash
set -euo pipefail
echo "Checking toolchain..."
# Validate Python version (requires 3.12+)
PY_VERSION=$(python --version | grep -oE '[0-9]+\.[0-9]+')
if [[ "$(printf '%s\n' "3.12" "$PY_VERSION" | sort -V | head -1)" != "3.12" ]]; then
echo "FAIL: Python $PY_VERSION < 3.12 required"
exit 1
fi
echo "OK: Python $PY_VERSION"
# Check uv is available
if ! command -v uv &> /dev/null; then
echo "FAIL: uv not found. Install: curl -LsSf https://astral.sh/uv/install.sh | sh"
exit 1
fi
echo "OK: uv $(uv --version | head -1)"
echo "All checks passed"
First-Run Detection
# Setup with first-run detection
setup:
#!/usr/bin/env bash
if [[ -f .setup-complete ]]; then
echo "Already set up. Run 'just setup-force' to reinstall."
exit 0
fi
mise trust && mise install
uv sync
touch .setup-complete
echo "Setup complete"
setup-force:
rm -f .setup-complete
@just setup
Anti-Patterns
- Bare
except: clauses (always specify exception type)
- Mutable default arguments (
def f(items=[]) - use None and create inside)
- Star imports (
from module import *)
- God classes with 10+ methods (extract into companion modules, don't compress)
- Compressing code or removing comments to fit length limits (extract into well-named functions/modules instead)
- Missing type annotations on public APIs
Any when specific types work
- Nested functions when a module-level function suffices
print() for logging (use logging/structlog)
- Ignoring return values of functions with side effects
AI Agent Guidelines
Before writing code:
- Read
pyproject.toml for project structure and dependencies
- Check ruff/pyright config for project-specific rules
- Identify existing patterns in the codebase to follow
When writing code:
- Add type annotations immediately - never leave functions untyped
- Add docstrings to public functions/classes
- Use existing project abstractions over creating new ones
- Prefer explicit types; use generics only when pattern repeats 3+ times
Before committing:
- Run
just check (standard for projects using just)
- Fallback:
uv run ruff check --fix . && uv run ruff format .
- Fallback:
uv run pyright && uv run pytest