
Writing Python
- 120 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Write clean, idiomatic Python for services, scripts, CLIs, and agent tooling with consistent structure, typing, and error handling.
About
Guides Claude to produce production-quality Python across backends, CLIs, and automation: clear module boundaries, type hints, consistent naming, safe I/O, and patterns that scale from scripts to services without rework.
- Idiomatic Python patterns
- Typing and module structure
- API and script scaffolding
- Error handling conventions
- Test-friendly code layout
Writing Python by the numbers
- 120 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #90 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill writing-pythonAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 120 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Write clean, idiomatic Python for services, scripts, CLIs, and agent tooling with consistent structure, typing, and error handling.
Files
Python Development (3.14+)
Core Principles
- Stdlib first: External deps only when justified
- Type hints everywhere: All functions, all parameters
- Explicit over implicit: Clear is better than clever
- Fail fast: Raise early with informative errors
Toolchain
uv # Package management (not pip/poetry)
ruff # Lint + format (not flake8/black)
pytest # Testing
mypy # Type checkingQuick Patterns
Type Hints
def process_users(users: list[User], limit: int | None = None) -> list[Result]:
...
async def fetch_data(url: str, timeout: float = 30.0) -> dict[str, Any]:
...Dataclasses
from dataclasses import dataclass, field
@dataclass
class Config:
host: str
port: int = 8080
tags: list[str] = field(default_factory=list)Pattern Matching
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "key", "code": code}:
handle_key(code)
case _:
raise ValueError(f"Unknown event: {event}")Python 3.14 Features
- Deferred annotations: No more
from __future__ import annotations - Template strings (t""):
t"Hello {name}"returns Template object - except without parens:
except ValueError, TypeError: - concurrent.interpreters: True parallelism via subinterpreters
- compression.zstd: Zstandard in stdlib
- Free-threaded build: No GIL (opt-in)
References
- PATTERNS.md - Code patterns and style
- CLI.md - CLI application patterns
- TESTING.md - Testing with pytest
Tooling
uv sync # Install deps
ruff check --fix . # Lint and autofix
ruff format . # Format
pytest -v # Test
mypy . # Type check---
Absorbed sub-skills (post-consolidation)
This skill now subsumes the former python-code-style and python-type-safety skills. Their original SKILL.md content is preserved as deep reference:
| Subject | Path |
|---|---|
| ruff, mypy, naming, imports, docstrings (Google style) | References/code-style.md |
| Type annotations, generics, protocols, strict checking patterns | References/type-safety.md |
For system-reliability concerns (background jobs, retries, observability), see the sibling `python-infrastructure` skill. For dependency management and project scaffolding, see `uv`.
---
Gotchas
- `from __future__ import annotations` makes ALL annotations strings — runtime introspection (
typing.get_type_hints) needs the actual types available; forward refs to local-scope classes fail. - f-string debug syntax (`f"{var=}"`) is 3.8+ — quietly fails (treats
=as literal) in 3.7 and earlier. - `dataclass(slots=True)` is 3.10+ — silently does nothing in 3.9. Use
__slots__manually for portability. - PEP 604 union syntax (`int | None`) is 3.10+ at runtime — works as a string annotation in 3.9 with
from __future__ import annotations, fails at runtime introspection. - `async def` vs `def` for FastAPI dependencies: async deps run in the event loop; sync deps run in a thread pool. Mixing without thought causes either blocking or extra context-switch overhead.
Python CLI Patterns
Framework: typer (Recommended)
Built on Click, with type hints for argument parsing.
import typer
app = typer.Typer()
@app.command()
def process(
input_file: Path,
output: Path = Path("output.json"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
dry_run: bool = typer.Option(False, "--dry-run"),
):
"""Process input files."""
if dry_run:
typer.echo(f"Would process {input_file} -> {output}")
return
result = do_process(input_file)
output.write_text(json.dumps(result))
@app.command()
def list_items(
format: str = typer.Option("table", help="Output format: table, json, csv"),
):
"""List all items."""
items = fetch_items()
print_items(items, format)
if __name__ == "__main__":
app()Alternative: argparse (stdlib)
import argparse
def main():
parser = argparse.ArgumentParser(description="Process files")
parser.add_argument("input", type=Path, help="Input file")
parser.add_argument("-o", "--output", type=Path, default=Path("output.json"))
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if args.dry_run:
print(f"Would process {args.input} -> {args.output}")
return
process(args.input, args.output)
if __name__ == "__main__":
main()Output Formats
import csv
import json
import sys
from io import StringIO
def print_items(items: list[dict], format: str = "table") -> None:
match format:
case "json":
print(json.dumps(items, indent=2))
case "csv":
if not items:
return
writer = csv.DictWriter(sys.stdout, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
case _:
if not items:
return
headers = list(items[0].keys())
widths = [max(len(h), max(len(str(item.get(h, ""))) for item in items)) for h in headers]
print(" ".join(h.ljust(w) for h, w in zip(headers, widths)))
print(" ".join("-" * w for w in widths))
for item in items:
print(" ".join(str(item.get(h, "")).ljust(w) for h, w in zip(headers, widths)))Progress Display
from rich.progress import Progress, SpinnerColumn, TextColumn
def process_with_progress(items: list[Item]) -> None:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
) as progress:
task = progress.add_task("Processing...", total=len(items))
for item in items:
progress.update(task, description=f"Processing {item.name}")
process_item(item)
progress.advance(task)Confirmation Prompts
import typer
def delete_item(name: str, force: bool = False) -> None:
if not force:
confirm = typer.confirm(f"Delete {name}?")
if not confirm:
raise typer.Abort()
do_delete(name)Environment Configuration
import os
from dataclasses import dataclass
@dataclass
class Config:
api_url: str
api_key: str
timeout: int = 30
@classmethod
def from_env(cls) -> "Config":
return cls(
api_url=os.environ.get("API_URL", "https://api.example.com"),
api_key=os.environ["API_KEY"],
timeout=int(os.environ.get("TIMEOUT", 30)),
)Exit Codes
import sys
EXIT_OK = 0
EXIT_ERROR = 1
EXIT_USAGE = 2
def main() -> int:
try:
run()
return EXIT_OK
except UsageError as e:
print(f"Usage error: {e}", file=sys.stderr)
return EXIT_USAGE
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return EXIT_ERROR
if __name__ == "__main__":
sys.exit(main())Entry Point
In pyproject.toml:
[project.scripts]
mytool = "mypackage.__main__:main"In src/mypackage/__main__.py:
from mypackage.cli import app
def main():
app()
if __name__ == "__main__":
main()Python Patterns Reference
Project Structure
src/
└── mypackage/
├── __init__.py
├── __main__.py # CLI entry
├── domain/ # Business logic
├── services/ # Operations
└── adapters/ # External integrations
tests/
pyproject.tomlType Hints
Functions
def get_user(user_id: str) -> User | None:
...
def process_items(items: Iterable[Item], *, limit: int = 100) -> list[Result]:
...
async def fetch(url: str, timeout: float = 30.0) -> bytes:
...Generics
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else NoneProtocol (Structural Typing)
from typing import Protocol
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
def process(source: Readable) -> str:
data = source.read()
return data.decode()TypedDict
from typing import TypedDict, NotRequired
class UserDict(TypedDict):
id: str
name: str
email: NotRequired[str]Error Handling
Custom Exceptions
class AppError(Exception):
pass
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
self.resource = resource
self.id = id
super().__init__(f"{resource} not found: {id}")
class ValidationError(AppError):
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"{field}: {message}")Error Handling Pattern
def get_user(user_id: str) -> User:
user = db.get(user_id)
if user is None:
raise NotFoundError("User", user_id)
return userConfiguration
Environment-Based
import os
from dataclasses import dataclass
@dataclass
class Config:
database_url: str
port: int = 8080
debug: bool = False
@classmethod
def from_env(cls) -> "Config":
return cls(
database_url=os.environ["DATABASE_URL"],
port=int(os.environ.get("PORT", 8080)),
debug=os.environ.get("DEBUG", "").lower() == "true",
)Async Patterns
Concurrent Tasks
import asyncio
async def fetch_all(urls: list[str]) -> list[bytes]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)Timeout
async def fetch_with_timeout(url: str, timeout: float = 30.0) -> bytes:
async with asyncio.timeout(timeout):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.read()Context Managers
Resource Management
from contextlib import contextmanager
@contextmanager
def open_db_connection(url: str):
conn = create_connection(url)
try:
yield conn
finally:
conn.close()Async Context Manager
from contextlib import asynccontextmanager
@asynccontextmanager
async def get_session():
session = await create_session()
try:
yield session
finally:
await session.close()Data Validation
With Pydantic (when needed)
from pydantic import BaseModel, EmailStr, field_validator
class CreateUserRequest(BaseModel):
name: str
email: EmailStr
@field_validator("name")
@classmethod
def name_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("name cannot be empty")
return v.strip()File Operations
Pathlib
from pathlib import Path
def process_files(directory: Path) -> list[Path]:
return list(directory.glob("**/*.json"))
def read_config(path: Path) -> dict:
return json.loads(path.read_text())Logging
Structured Logging
import logging
import sys
def setup_logging(level: str = "INFO") -> None:
logging.basicConfig(
level=level,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
handlers=[logging.StreamHandler(sys.stderr)],
)
logger = logging.getLogger(__name__)
logger.info("Processing started", extra={"count": len(items)})Style Guidelines
- Use
snake_casefor functions and variables - Use
PascalCasefor classes - Use
UPPER_CASEfor constants - Prefer
pathlib.Pathoveros.path - Use f-strings for formatting
- Use context managers for resources
- Avoid mutable default arguments
Python Code Style & Documentation
Consistent code style and clear documentation make codebases maintainable and collaborative. This skill covers modern Python tooling, naming conventions, and documentation standards.
When to Use This Skill
- Setting up linting and formatting for a new project
- Writing or reviewing docstrings
- Establishing team coding standards
- Configuring ruff, mypy, or pyright
- Reviewing code for style consistency
- Creating project documentation
Core Concepts
1. Automated Formatting
Let tools handle formatting debates. Configure once, enforce automatically.
2. Consistent Naming
Follow PEP 8 conventions with meaningful, descriptive names.
3. Documentation as Code
Docstrings should be maintained alongside the code they describe.
4. Type Annotations
Modern Python code should include type hints for all public APIs.
Quick Start
# Install modern tooling
pip install ruff mypy
# Configure in pyproject.toml
[tool.ruff]
line-length = 120
target-version = "py312" # Adjust based on your project's minimum Python version
[tool.mypy]
strict = trueFundamental Patterns
Pattern 1: Modern Python Tooling
Use ruff as an all-in-one linter and formatter. It replaces flake8, isort, and black with a single fast tool.
# pyproject.toml
[tool.ruff]
line-length = 120
target-version = "py312" # Adjust based on your project's minimum Python version
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
]
ignore = ["E501"] # Line length handled by formatter
[tool.ruff.format]
quote-style = "double"
indent-style = "space"Run with:
ruff check --fix . # Lint and auto-fix
ruff format . # Format codePattern 2: Type Checking Configuration
Configure strict type checking for production code.
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = falseAlternative: Use pyright for faster checking.
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"Pattern 3: Naming Conventions
Follow PEP 8 with emphasis on clarity over brevity.
Files and Modules:
# Good: Descriptive snake_case
user_repository.py
order_processing.py
http_client.py
# Avoid: Abbreviations
usr_repo.py
ord_proc.py
http_cli.pyClasses and Functions:
# Classes: PascalCase
class UserRepository:
pass
class HTTPClientFactory: # Acronyms stay uppercase
pass
# Functions and variables: snake_case
def get_user_by_email(email: str) -> User | None:
retry_count = 3
max_connections = 100Constants:
# Module-level constants: SCREAMING_SNAKE_CASE
MAX_RETRY_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com"Pattern 4: Import Organization
Group imports in a consistent order: standard library, third-party, local.
# Standard library
import os
from collections.abc import Callable
from typing import Any
# Third-party packages
import httpx
from pydantic import BaseModel
from sqlalchemy import Column
# Local imports
from myproject.models import User
from myproject.services import UserServiceUse absolute imports exclusively:
# Preferred
from myproject.utils import retry_decorator
# Avoid relative imports
from ..utils import retry_decoratorAdvanced Patterns
Pattern 5: Google-Style Docstrings
Write docstrings for all public classes, methods, and functions.
Simple Function:
def get_user(user_id: str) -> User:
"""Retrieve a user by their unique identifier."""
...Complex Function:
def process_batch(
items: list[Item],
max_workers: int = 4,
on_progress: Callable[[int, int], None] | None = None,
) -> BatchResult:
"""Process items concurrently using a worker pool.
Processes each item in the batch using the configured number of
workers. Progress can be monitored via the optional callback.
Args:
items: The items to process. Must not be empty.
max_workers: Maximum concurrent workers. Defaults to 4.
on_progress: Optional callback receiving (completed, total) counts.
Returns:
BatchResult containing succeeded items and any failures with
their associated exceptions.
Raises:
ValueError: If items is empty.
ProcessingError: If the batch cannot be processed.
Example:
>>> result = process_batch(items, max_workers=8)
>>> print(f"Processed {len(result.succeeded)} items")
"""
...Class Docstring:
class UserService:
"""Service for managing user operations.
Provides methods for creating, retrieving, updating, and
deleting users with proper validation and error handling.
Attributes:
repository: The data access layer for user persistence.
logger: Logger instance for operation tracking.
Example:
>>> service = UserService(repository, logger)
>>> user = service.create_user(CreateUserInput(...))
"""
def __init__(self, repository: UserRepository, logger: Logger) -> None:
"""Initialize the user service.
Args:
repository: Data access layer for users.
logger: Logger for tracking operations.
"""
self.repository = repository
self.logger = loggerPattern 6: Line Length and Formatting
Set line length to 120 characters for modern displays while maintaining readability.
# Good: Readable line breaks
def create_user(
email: str,
name: str,
role: UserRole = UserRole.MEMBER,
notify: bool = True,
) -> User:
...
# Good: Chain method calls clearly
result = (
db.query(User)
.filter(User.active == True)
.order_by(User.created_at.desc())
.limit(10)
.all()
)
# Good: Format long strings
error_message = (
f"Failed to process user {user_id}: "
f"received status {response.status_code} "
f"with body {response.text[:100]}"
)Pattern 7: Project Documentation
README Structure:
# Project Name
Brief description of what the project does.
## Installation
\`\`\`bash
pip install myproject
\`\`\`
## Quick Start
\`\`\`python
from myproject import Client
client = Client(api_key="...")
result = client.process(data)
\`\`\`
## Configuration
Document environment variables and configuration options.
## Development
\`\`\`bash
pip install -e ".[dev]"
pytest
\`\`\`CHANGELOG Format (Keep a Changelog):
# Changelog
## [Unreleased]
### Added
- New feature X
### Changed
- Modified behavior of Y
### Fixed
- Bug in ZBest Practices Summary
1. Use ruff - Single tool for linting and formatting 2. Enable strict mypy - Catch type errors before runtime 3. 120 character lines - Modern standard for readability 4. Descriptive names - Clarity over brevity 5. Absolute imports - More maintainable than relative 6. Google-style docstrings - Consistent, readable documentation 7. Document public APIs - Every public function needs a docstring 8. Keep docs updated - Treat documentation as code 9. Automate in CI - Run linters on every commit 10. Target Python 3.10+ - For new projects, Python 3.12+ is recommended for modern language features
Python Type Safety
Leverage Python's type system to catch errors at static analysis time. Type annotations serve as enforced documentation that tooling validates automatically.
When to Use This Skill
- Adding type hints to existing code
- Creating generic, reusable classes
- Defining structural interfaces with protocols
- Configuring mypy or pyright for strict checking
- Understanding type narrowing and guards
- Building type-safe APIs and libraries
Core Concepts
1. Type Annotations
Declare expected types for function parameters, return values, and variables.
2. Generics
Write reusable code that preserves type information across different types.
3. Protocols
Define structural interfaces without inheritance (duck typing with type safety).
4. Type Narrowing
Use guards and conditionals to narrow types within code blocks.
Quick Start
def get_user(user_id: str) -> User | None:
"""Return type makes 'might not exist' explicit."""
...
# Type checker enforces handling None case
user = get_user("123")
if user is None:
raise UserNotFoundError("123")
print(user.name) # Type checker knows user is User hereFundamental Patterns
Pattern 1: Annotate All Public Signatures
Every public function, method, and class should have type annotations.
def get_user(user_id: str) -> User:
"""Retrieve user by ID."""
...
def process_batch(
items: list[Item],
max_workers: int = 4,
) -> BatchResult[ProcessedItem]:
"""Process items concurrently."""
...
class UserRepository:
def __init__(self, db: Database) -> None:
self._db = db
async def find_by_id(self, user_id: str) -> User | None:
"""Return User if found, None otherwise."""
...
async def find_by_email(self, email: str) -> User | None:
...
async def save(self, user: User) -> User:
"""Save and return user with generated ID."""
...Use mypy --strict or pyright in CI to catch type errors early. For existing projects, enable strict mode incrementally using per-module overrides.
Pattern 2: Use Modern Union Syntax
Python 3.10+ provides cleaner union syntax.
# Preferred (3.10+)
def find_user(user_id: str) -> User | None:
...
def parse_value(v: str) -> int | float | str:
...
# Older style (still valid, needed for 3.9)
from typing import Optional, Union
def find_user(user_id: str) -> Optional[User]:
...Pattern 3: Type Narrowing with Guards
Use conditionals to narrow types for the type checker.
def process_user(user_id: str) -> UserData:
user = find_user(user_id)
if user is None:
raise UserNotFoundError(f"User {user_id} not found")
# Type checker knows user is User here, not User | None
return UserData(
name=user.name,
email=user.email,
)
def process_items(items: list[Item | None]) -> list[ProcessedItem]:
# Filter and narrow types
valid_items = [item for item in items if item is not None]
# valid_items is now list[Item]
return [process(item) for item in valid_items]Pattern 4: Generic Classes
Create type-safe reusable containers.
from typing import TypeVar, Generic
T = TypeVar("T")
E = TypeVar("E", bound=Exception)
class Result(Generic[T, E]):
"""Represents either a success value or an error."""
def __init__(
self,
value: T | None = None,
error: E | None = None,
) -> None:
if (value is None) == (error is None):
raise ValueError("Exactly one of value or error must be set")
self._value = value
self._error = error
@property
def is_success(self) -> bool:
return self._error is None
@property
def is_failure(self) -> bool:
return self._error is not None
def unwrap(self) -> T:
"""Get value or raise the error."""
if self._error is not None:
raise self._error
return self._value # type: ignore[return-value]
def unwrap_or(self, default: T) -> T:
"""Get value or return default."""
if self._error is not None:
return default
return self._value # type: ignore[return-value]
# Usage preserves types
def parse_config(path: str) -> Result[Config, ConfigError]:
try:
return Result(value=Config.from_file(path))
except ConfigError as e:
return Result(error=e)
result = parse_config("config.yaml")
if result.is_success:
config = result.unwrap() # Type: ConfigAdvanced Patterns
Pattern 5: Generic Repository
Create type-safe data access patterns.
from typing import TypeVar, Generic
from abc import ABC, abstractmethod
T = TypeVar("T")
ID = TypeVar("ID")
class Repository(ABC, Generic[T, ID]):
"""Generic repository interface."""
@abstractmethod
async def get(self, id: ID) -> T | None:
"""Get entity by ID."""
...
@abstractmethod
async def save(self, entity: T) -> T:
"""Save and return entity."""
...
@abstractmethod
async def delete(self, id: ID) -> bool:
"""Delete entity, return True if existed."""
...
class UserRepository(Repository[User, str]):
"""Concrete repository for Users with string IDs."""
async def get(self, id: str) -> User | None:
row = await self._db.fetchrow(
"SELECT * FROM users WHERE id = $1", id
)
return User(**row) if row else None
async def save(self, entity: User) -> User:
...
async def delete(self, id: str) -> bool:
...Pattern 6: TypeVar with Bounds
Restrict generic parameters to specific types.
from typing import TypeVar
from pydantic import BaseModel
ModelT = TypeVar("ModelT", bound=BaseModel)
def validate_and_create(model_cls: type[ModelT], data: dict) -> ModelT:
"""Create a validated Pydantic model from dict."""
return model_cls.model_validate(data)
# Works with any BaseModel subclass
class User(BaseModel):
name: str
email: str
user = validate_and_create(User, {"name": "Alice", "email": "a@b.com"})
# user is typed as User
# Type error: str is not a BaseModel subclass
result = validate_and_create(str, {"name": "Alice"}) # Error!Pattern 7: Protocols for Structural Typing
Define interfaces without requiring inheritance.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
"""Any class that can be serialized to/from dict."""
def to_dict(self) -> dict:
...
@classmethod
def from_dict(cls, data: dict) -> "Serializable":
...
# User satisfies Serializable without inheriting from it
class User:
def __init__(self, id: str, name: str) -> None:
self.id = id
self.name = name
def to_dict(self) -> dict:
return {"id": self.id, "name": self.name}
@classmethod
def from_dict(cls, data: dict) -> "User":
return cls(id=data["id"], name=data["name"])
def serialize(obj: Serializable) -> str:
"""Works with any Serializable object."""
return json.dumps(obj.to_dict())
# Works - User matches the protocol
serialize(User("1", "Alice"))
# Runtime checking with @runtime_checkable
isinstance(User("1", "Alice"), Serializable) # TruePattern 8: Common Protocol Patterns
Define reusable structural interfaces.
from typing import Protocol
class Closeable(Protocol):
"""Resource that can be closed."""
def close(self) -> None: ...
class AsyncCloseable(Protocol):
"""Async resource that can be closed."""
async def close(self) -> None: ...
class Readable(Protocol):
"""Object that can be read from."""
def read(self, n: int = -1) -> bytes: ...
class HasId(Protocol):
"""Object with an ID property."""
@property
def id(self) -> str: ...
class Comparable(Protocol):
"""Object that supports comparison."""
def __lt__(self, other: "Comparable") -> bool: ...
def __le__(self, other: "Comparable") -> bool: ...Pattern 9: Type Aliases
Create meaningful type names.
Note: The type statement was introduced in Python 3.10 for simple aliases. Generic type statements require Python 3.12+.
# Python 3.10+ type statement for simple aliases
type UserId = str
type UserDict = dict[str, Any]
# Python 3.12+ type statement with generics
type Handler[T] = Callable[[Request], T]
type AsyncHandler[T] = Callable[[Request], Awaitable[T]]
# Python 3.9-3.11 style (needed for broader compatibility)
from typing import TypeAlias
from collections.abc import Callable, Awaitable
UserId: TypeAlias = str
Handler: TypeAlias = Callable[[Request], Response]
# Usage
def register_handler(path: str, handler: Handler[Response]) -> None:
...Pattern 10: Callable Types
Type function parameters and callbacks.
from collections.abc import Callable, Awaitable
# Sync callback
ProgressCallback = Callable[[int, int], None] # (current, total)
# Async callback
AsyncHandler = Callable[[Request], Awaitable[Response]]
# With named parameters (using Protocol)
class OnProgress(Protocol):
def __call__(
self,
current: int,
total: int,
*,
message: str = "",
) -> None: ...
def process_items(
items: list[Item],
on_progress: ProgressCallback | None = None,
) -> list[Result]:
for i, item in enumerate(items):
if on_progress:
on_progress(i, len(items))
...Configuration
Strict Mode Checklist
For mypy --strict compliance:
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = trueIncremental adoption goals:
- All function parameters annotated
- All return types annotated
- Class attributes annotated
- Minimize
Anyusage (acceptable for truly dynamic data) - Generic collections use type parameters (
list[str]notlist)
For existing codebases, enable strict mode per-module using # mypy: strict or configure per-module overrides in pyproject.toml.
Best Practices Summary
1. Annotate all public APIs - Functions, methods, class attributes 2. Use `T | None` - Modern union syntax over Optional[T] 3. Run strict type checking - mypy --strict in CI 4. Use generics - Preserve type info in reusable code 5. Define protocols - Structural typing for interfaces 6. Narrow types - Use guards to help the type checker 7. Bound type vars - Restrict generics to meaningful types 8. Create type aliases - Meaningful names for complex types 9. Minimize `Any` - Use specific types or generics. Any is acceptable for truly dynamic data or when interfacing with untyped third-party code 10. Document with types - Types are enforceable documentation
Python Testing Reference
Framework: pytest
uv add --dev pytest pytest-asyncio pytest-cov
pytest -v
pytest --cov=srcBasic Tests
def test_validate_email_valid():
assert validate_email("user@example.com") is None
def test_validate_email_empty():
with pytest.raises(ValidationError, match="email required"):
validate_email("")
def test_validate_email_invalid():
with pytest.raises(ValidationError, match="invalid format"):
validate_email("invalid")Parametrized Tests
@pytest.mark.parametrize("email,expected_error", [
("user@example.com", None),
("", "email required"),
("invalid", "invalid format"),
("user@", "invalid format"),
])
def test_validate_email(email: str, expected_error: str | None):
if expected_error:
with pytest.raises(ValidationError, match=expected_error):
validate_email(email)
else:
assert validate_email(email) is NoneFixtures
@pytest.fixture
def user_service(mock_repo):
return UserService(repo=mock_repo)
@pytest.fixture
def mock_repo():
return Mock(spec=UserRepository)
def test_get_user(user_service, mock_repo):
mock_repo.get.return_value = User(id="123", name="Test")
result = user_service.get_user("123")
assert result.name == "Test"
mock_repo.get.assert_called_once_with("123")Mocking
from unittest.mock import Mock, patch, AsyncMock
def test_with_mock():
mock_client = Mock()
mock_client.fetch.return_value = {"status": "ok"}
service = Service(client=mock_client)
result = service.process()
assert result == "ok"
mock_client.fetch.assert_called_once()
@patch("mypackage.services.external_api")
def test_with_patch(mock_api):
mock_api.call.return_value = {"data": "test"}
result = process_data()
assert result == "test"Async Tests
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data("https://api.example.com")
assert result is not None
@pytest.fixture
async def async_client():
client = AsyncClient()
yield client
await client.close()
@pytest.mark.asyncio
async def test_with_async_fixture(async_client):
result = await async_client.get("/users")
assert result.status == 200Test Organization
tests/
├── conftest.py # Shared fixtures
├── test_domain/
│ └── test_user.py
├── test_services/
│ └── test_user_service.py
└── test_integration/
└── test_api.pyconftest.py
import pytest
@pytest.fixture
def sample_user():
return User(id="123", name="Test", email="test@example.com")
@pytest.fixture
def db_session():
session = create_test_session()
yield session
session.rollback()
session.close()Integration Tests
@pytest.mark.integration
def test_database_integration(db_session):
repo = UserRepository(db_session)
user = User(name="Test", email="test@example.com")
repo.save(user)
result = repo.get(user.id)
assert result.name == "Test"Run integration tests:
pytest -m integration
pytest -m "not integration" # Skip themCoverage
pytest --cov=src --cov-report=html
pytest --cov=src --cov-fail-under=80Guidelines
- One assertion per test (when practical)
- Descriptive test names
- Use fixtures for setup
- Parametrize for multiple cases
- Keep tests independent
- Test behavior, not implementation