| name | writing-python |
| description | Idiomatic Python 3.14+ development. Use when writing Python code, CLI tools, scripts, or services. Emphasizes stdlib, type hints, uv/ruff toolchain, and minimal dependencies. |
| allowed-tools | Read, Bash, Grep, Glob |
Python Development (3.14+)
Core Philosophy
Stdlib and Mature Libraries First
- Always prefer Python stdlib solutions
- External deps only when stdlib insufficient
- Prefer dataclasses over attrs, pathlib over os.path
Type Hints Everywhere (No Any)
- Python 3.14 has lazy annotations by default
- Use Protocol for structural typing (duck typing)
- Avoid Any—use concrete types or generics
Protocol Over ABC
- Protocol for implicit interface satisfaction
- ABC only when runtime isinstance() needed
- Protocols are more flexible and Pythonic
Flat Control Flow
- Guard clauses with early returns
- Pattern matching to flatten conditionals
- Maximum 2 levels of nesting
Explicit Error Handling
- Custom exception hierarchy for domain errors
- Raise early, handle at boundaries
- except ValueError | TypeError: (no parens)
Quick Patterns
Protocol-Based Dependency Injection
from typing import Protocol
# Protocol at consumer (like Go interfaces)
class UserStore(Protocol):
def get(self, id: str) -> User | None: ...
def save(self, user: User) -> None: ...
class UserService:
def __init__(self, store: UserStore):
self.store = store # accepts any matching impl
Flat Control Flow (No Nesting)
# GOOD: guard clauses, early returns
def process(user: User | None) -> Result:
if user is None:
raise ValueError("user required")
if not user.email:
raise ValueError("email required")
if not is_valid_email(user.email):
raise ValueError("invalid email")
return do_work(user) # happy path at end
# BAD: nested conditions
def process(user: User | None) -> Result:
if user is not None:
if user.email:
if is_valid_email(user.email):
return do_work(user)
raise ValueError("invalid")
Pattern Matching
# Flatten complex conditionals with match
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}")
Type Hints (No Any)
# GOOD: concrete types
def process_users(users: list[User], limit: int | None = None) -> list[Result]:
...
# GOOD: generics when needed
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
# BAD: unnecessary Any
def process(data: Any) -> Any: # Don't do this
...
Error Handling
# except without parens (3.14)
except ValueError | TypeError:
handle_error()
# Custom exceptions
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
self.resource = resource
self.id = id
super().__init__(f"{resource} not found: {id}")
Python 3.14 Features
- Deferred annotations: No
from __future__ import annotations - Template strings (t""):
t"Hello {name}"returns Template - except without parens:
except ValueError | TypeError: - concurrent.interpreters: True parallelism
- 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