| 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
def db_session():
session = create_session()
yield session
session.rollback()
session.close()
def test_user_creation(db_session, sample_user):
db_session.add(sample_user)
assert db_session.query(User).count() == 1
conftest.py (shared fixtures)
import pytest
@pytest.fixture(scope="session")
def app_config():
return Config(host="localhost", port=8000)
@pytest.fixture(autouse=True)
def reset_state():
yield
cleanup()
Mocking
from unittest.mock import patch, MagicMock
def test_fetch_data():
with patch("mypackage.main.httpx.get") as mock_get:
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"key": "value"},
)
result = fetch_data("https://api.example.com")
assert result == {"key": "value"}
mock_get.assert_called_once()
Logging
import logging
logger = logging.getLogger(__name__)
def process_data(data: dict) -> dict:
logger.info("Processing %d items", len(data))
try:
result = transform(data)
except Exception:
logger.exception("Failed to process data")
raise
logger.debug("Result: %s", result)
return result
Application-level configuration
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "DEBUG",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"maxBytes": 10_485_760,
"backupCount": 5,
"formatter": "standard",
},
},
"root": {
"level": "INFO",
"handlers": ["console", "file"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
Log levels:
| Level | Use For |
|---|
DEBUG | Detailed diagnostic info (dev only) |
INFO | Routine operations (startup, requests) |
WARNING | Unexpected but handled situations |
ERROR | Failures that need attention |
CRITICAL | System-level failures |
Virtual Environments and Dependencies
uv venv
uv pip install -e ".[dev]"
uv pip compile pyproject.toml -o requirements.lock
uv sync
python -m venv .venv
.venv\Scripts\activate
source .venv/bin/activate
pip install -e ".[dev]"
pip install -e .
Common Standard Library
from collections import defaultdict, Counter, deque
word_count = Counter(words)
grouped = defaultdict(list)
queue = deque(maxlen=100)
from itertools import chain, groupby, islice, batched
all_items = list(chain(list1, list2))
first_10 = list(islice(generator, 10))
chunks = list(batched(items, 5))
from functools import lru_cache, partial, reduce
cached_fn = lru_cache(maxsize=256)(expensive_fn)
add_ten = partial(add, 10)
from enum import Enum, auto
class Status(Enum):
PENDING = auto()
ACTIVE = auto()
CLOSED = auto()
from abc import ABC, abstractmethod
class Repository(ABC):
@abstractmethod
def get(self, id: str) -> dict:
...
@abstractmethod
def save(self, data: dict) -> None:
...
import argparse
parser = argparse.ArgumentParser(description="My CLI tool")
parser.add_argument("input", help="Input file path")
parser.add_argument("-o", "--output", default="out.json", help="Output path")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
Best Practices
- Use type hints everywhere — enables IDE support and catches bugs early
- Format with ruff —
ruff check --fix . && ruff format .
- Prefer
pathlib.Path over os.path for all file operations
- Use dataclasses or Pydantic for data containers — avoid raw dicts
- Write custom exceptions for domain-specific errors
- Use
logging not print() for operational output
- Write tests with pytest from the start — aim for fixtures over setup/teardown
- Use
pyproject.toml as the single config file (not setup.py/setup.cfg)
- Prefer composition over inheritance — use Protocol for interfaces
- Use
uv for fast dependency management and virtual environments