| name | python |
| description | This skill MUST be loaded when ANY .py file is being read, reviewed, edited, or created that does NOT involve FastAPI, FastMCP, OpenAI Agents SDK, or n8n Code nodes. Also use when the user asks to "create a Python project", "set up a Python package", "write a Python script", "add type hints", "create a dataclass", "write pytest tests", "add logging", "create a decorator", "write a generator", "implement a context manager", "handle exceptions", "read/write files in Python", "use async/await in Python", "manage Python dependencies", "set up pyproject.toml", "create a virtual environment", "use pathlib", "parse arguments with argparse", or mentions Python typing, dataclasses, pytest, logging, PEP 8, Python packaging, pip, uv, poetry, or general Python development patterns. |
Python Development Guide
This skill provides comprehensive guidance for writing modern, production-quality Python code following current best practices (Python 3.10+).
Project Structure
project/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── utils.py
│ └── exceptions.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_main.py
│ └── test_utils.py
├── pyproject.toml
├── README.md
└── .gitignore
pyproject.toml (modern standard)
[project]
name = "mypackage"
version = "0.1.0"
description = "My Python package"
requires-python = ">=3.10"
dependencies = [
"pydantic>=2.0",
"httpx>=0.27",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.5",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
[tool.ruff]
target-version = "py310"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
Type Hints
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
def find_user(user_id: int) -> User | None:
...
def process(items: list[str], lookup: dict[str, int]) -> tuple[str, ...]:
...
from collections.abc import Callable
def retry(func: Callable[..., str], attempts: int = 3) -> str:
...
Advanced typing patterns
from typing import TypeVar, Protocol, Literal, TypeAlias
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Renderable(Protocol):
def render(self) -> str: ...
def display(item: Renderable) -> None:
print(item.render())
def set_mode(mode: Literal["read", "write", "append"]) -> None:
...
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
Dataclasses and Pydantic Models
Dataclasses (for internal data structures)
from dataclasses import dataclass, field
@dataclass(slots=True)
class Point:
x: float
y: float
@dataclass(frozen=True)
class Config:
host: str = "localhost"
port: int = 8000
tags: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
if self.port < 0:
raise ValueError(f"Invalid port: {self.port}")
Pydantic (for validation and serialization)
from pydantic import BaseModel, Field, field_validator
class User(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str
age: int = Field(ge=0, le=150)
tags: list[str] = []
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("Invalid email")
return v.lower()
user = User(name="Alice", email="ALICE@example.com", age=30)
data = user.model_dump()
json_str = user.model_dump_json()
user2 = User.model_validate(data)
When to use which:
| Use Case | Choice |
|---|
| Internal data containers | @dataclass(slots=True) |
| Immutable values | @dataclass(frozen=True) |
| External data / validation | Pydantic BaseModel |
| Simple tuples with names | NamedTuple |
| Config with env vars | Pydantic BaseSettings |
Error Handling
Custom exception hierarchy
class AppError(Exception):
"""Base exception for the application."""
class NotFoundError(AppError):
def __init__(self, resource: str, id: str) -> None:
self.resource = resource
self.id = id
super().__init__(f"{resource} not found: {id}")
class ValidationError(AppError):
def __init__(self, field: str, message: str) -> None:
self.field = field
super().__init__(f"Validation error on '{field}': {message}")
Proper exception patterns
try:
data = json.loads(raw_text)
except json.JSONDecodeError as e:
raise ValidationError("body", "Invalid JSON") from e
try:
file = open(path)
except FileNotFoundError:
logger.warning("File not found: %s", path)
return default_value
else:
data = file.read()
finally:
file.close()
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(task1())
tg.create_task(task2())
except* ValueError as eg:
for exc in eg.exceptions:
logger.error("Value error: %s", exc)
except* TypeError as eg:
for exc in eg.exceptions:
logger.error("Type error: %s", exc)
Decorators
import functools
import time
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
time.sleep(delay)
raise last_exc
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url: str) -> dict:
...
@functools.lru_cache(maxsize=128)
def expensive_computation(n: int) -> int:
...
@functools.cache
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Context Managers
from contextlib import contextmanager, asynccontextmanager
@contextmanager
def timer(label: str):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
with timer("processing"):
process_data()
class DatabaseConnection:
def __init__(self, url: str) -> None:
self.url = url
def __enter__(self):
self.conn = create_connection(self.url)
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
self.conn.close()
return False
@asynccontextmanager
async def http_client():
import httpx
async with httpx.AsyncClient() as client:
yield client
Generators
def read_large_file(path: str, chunk_size: int = 8192):
with open(path, "rb") as f:
while chunk := f.read(chunk_size):
yield chunk
squares = (x ** 2 for x in range(1_000_000))
def flatten(nested: list[list[int]]) -> list[int]:
for sublist in nested:
yield from sublist
def pipeline():
lines = read_lines("data.txt")
stripped = (line.strip() for line in lines)
non_empty = (line for line in stripped if line)
parsed = (parse_record(line) for line in non_empty)
return list(parsed)
Async/Await
import asyncio
async def fetch_url(url: str) -> str:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.text
async def fetch_all(urls: list[str]) -> list[str]:
results = await asyncio.gather(
*(fetch_url(url) for url in urls),
return_exceptions=True,
)
return [r for r in results if isinstance(r, str)]
async def main():
task = asyncio.create_task(background_job())
await do_other_work()
result = await task
asyncio.run(main())
async def fetch_all_v2(urls: list[str]) -> list[str]:
results = []
async with asyncio.TaskGroup() as tg:
for url in urls:
tg.create_task(fetch_and_append(url, results))
return results
Common pitfall: Never call blocking I/O in async code — use asyncio.to_thread() for sync functions:
result = await asyncio.to_thread(blocking_function, arg1, arg2)
File I/O and pathlib
from pathlib import Path
root = Path("project")
config_path = root / "config" / "settings.toml"
config_path.parent.mkdir(parents=True, exist_ok=True)
text = config_path.read_text(encoding="utf-8")
config_path.write_text("key = 'value'\n", encoding="utf-8")
data = Path("image.png").read_bytes()
py_files = list(Path("src").rglob("*.py"))
import json
data = json.loads(Path("data.json").read_text())
Path("output.json").write_text(json.dumps(data, indent=2))
import csv
with open("data.csv", newline="") as f:
reader = csv.DictReader(f)
rows = list(reader)
import tomllib
with open("config.toml", "rb") as f:
config = tomllib.load(f)
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
temp_path = f.name
Testing with pytest
import pytest
from mypackage.main import process, calculate
def test_process_valid_input():
result = process("hello")
assert result == "HELLO"
@pytest.mark.parametrize("input_val, expected", [
(0, 0),
(1, 1),
(5, 120),
(10, 3628800),
])
def test_factorial(input_val, expected):
assert calculate(input_val) == expected
def test_negative_input_raises():
with pytest.raises(ValueError, match="must be non-negative"):
calculate(-1)
@pytest.fixture
def sample_user():
return User(name="Alice", email="alice@test.com", age=30)
@pytest.fixture