Claude
Skills
Sign in
Back

python-pro

Included with Lifetime
$97 forever

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.

Design

What this skill does


# Python Pro

Senior-level Python expertise for production projects. Focuses on modern tooling, strict type checking, and Pythonic idioms.

## When Invoked

1. Review `pyproject.toml` for project conventions and tooling config
2. For build system setup, invoke the **just-pro** skill
3. 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](https://mise.jdx.dev): `mise use [email protected]` (creates `.mise.toml` — commit it). Team members run `mise install`. See **mise** skill for setup.

### New Project Quick Start with uv

[uv](https://docs.astral.sh/uv/) is the modern Python package manager (10-100x faster than pip).

```bash
# Initialize project
uv init project-name
cd project-name

# Or in existing directory
uv init

# Add dependencies
uv add httpx pydantic

# Add dev dependencies
uv add --dev pytest pytest-cov pytest-asyncio ruff pyright

# Set up src layout (required for imports to work)
mkdir -p src/projectname tests
mv *.py src/projectname/ 2>/dev/null || true
touch src/projectname/__init__.py tests/__init__.py tests/conftest.py

# Copy configs from this skill's references/ directory:
#   references/gitignore               -> .gitignore
#   references/pyproject-template.toml -> use as pyproject.toml base
#   references/pyrightconfig.json      -> pyrightconfig.json
# For build system, invoke just-pro skill

# Verify
just check   # Or: uv run ruff check . && uv run pyright
```

### pyproject.toml Requirements

**Note:** uv init creates a minimal pyproject.toml. For src layout to work, add these sections:

```toml
[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

```bash
git clone <repo> && cd <repo>
just setup         # Runs mise trust/install + uv sync
just check         # Verify everything works
```

Or manually:
```bash
mise trust && mise install  # Get pinned Python version
uv sync                     # Install dependencies from lockfile
```

**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:

```bash
just fix             # Or: uv run ruff check --fix . && uv run ruff format .
```

**Verification:**
```bash
just check           # Or: uv run ruff check . && uv run pyright && uv run pytest
```

---

## Handling Strict Pyright

### Untyped Libraries

Some libraries lack type stubs. In pyright strict mode, use `Any`:

```python
from typing import Any

import structlog  # No complete type stubs

# Annotate as Any to silence pyright
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:

```python
from __future__ import annotations  # Required for forward refs

from typing import TYPE_CHECKING

from mypackage.service import run_service  # Runtime import

if TYPE_CHECKING:
    from mypackage.models import User  # Type-only import

def process(user: User) -> None:  # Works due to __future__ annotations
    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:

```python
# BAD - pyright error: "x" could be None
result.error_message.lower()

# GOOD - narrow the type first
assert result.error_message is not None
result.error_message.lower()

# OR use conditional
if result.error_message:
    result.error_message.lower()
```

---

## Quick Reference

### Type Annotations

```python
from typing import TypeVar, Protocol
from collections.abc import Callable, Iterator, Sequence

# Basic annotations
def process(items: list[str], timeout: float = 30.0) -> dict[str, int]:
    ...

# Generic functions
T = TypeVar("T")

def first(items: Sequence[T]) -> T | None:
    return items[0] if items else None

# Protocols for structural typing (duck typing with types)
class Readable(Protocol):
    def read(self, n: int = -1) -> bytes: ...

def load_data(source: Readable) -> bytes:
    return source.read()
```

### Error Handling

```python
# Custom exceptions with context
class ValidationError(Exception):
    def __init__(self, field: str, message: str) -> None:
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

# Explicit error handling
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)

# Use Result pattern for expected failures (optional)
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

```python
from dataclasses import dataclass, field
from pydantic import BaseModel, Field

# Simple data containers
@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

# Pydantic for validation and serialization
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

```python
import asyncio
from collections.abc import AsyncIterator

# Async context managers
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 generators
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

# Gather with error handling
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

```python
from contextlib import contextmanager, asynccontextmanager
from collections.abc import Generator, AsyncGenerator

@contextmanager
def temporary_config(overrides: dict

Related in Design