From 4768dcdf27e1238a7c0317242d149b601a46886a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 02:12:28 +0000 Subject: [PATCH] Add portable Python best-practices AI skill (SKILL.md) Co-authored-by: dongnt97 --- skills/python-best-practices/README.md | 33 ++ skills/python-best-practices/SKILL.md | 540 +++++++++++++++++++++++++ 2 files changed, 573 insertions(+) create mode 100644 skills/python-best-practices/README.md create mode 100644 skills/python-best-practices/SKILL.md diff --git a/skills/python-best-practices/README.md b/skills/python-best-practices/README.md new file mode 100644 index 0000000000..f109c650fc --- /dev/null +++ b/skills/python-best-practices/README.md @@ -0,0 +1,33 @@ +# Python Best Practices — Portable AI Skill + +A single, tool-agnostic skill that teaches any AI IDE/agent to write modern, +idiomatic **Python 3.10+**. The content lives in [`SKILL.md`](./SKILL.md) and is +plain Markdown with YAML front matter, so it is readable by virtually every +assistant. + +## What it covers + +Type hints (PEP 484/585/604), dataclasses & Pydantic v2, context managers, +generators/iterators, the decorator pattern, async/await with asyncio, +structural pattern matching (3.10+), the `src/` project layout, dependency +management with **Poetry/uv**, and PEP 8 + **Black** + **Ruff** formatting/linting. + +## Use it in your tool + +The same `SKILL.md` works everywhere. Pick whichever wiring your tool expects: + +| Tool / Agent | How to enable | +| --- | --- | +| **Claude / Claude Code (Agent Skills)** | Place this folder under your skills dir (e.g. `.claude/skills/python-best-practices/`) — `SKILL.md` is auto-discovered via its front matter `name` + `description`. | +| **Cursor** | Reference it from a rule, or add a `.cursor/rules/python-best-practices.mdc` that points to / inlines this file. You can also `@`-mention `SKILL.md` in chat. | +| **GitHub Copilot** | Copy the body into `.github/copilot-instructions.md` (or link to it). | +| **Windsurf / Codeium** | Add the body to `.windsurfrules`. | +| **Cline / Aider / others** | Add `SKILL.md` to the project context or instruction file the agent reads. | +| **Any agent** | Just feed `SKILL.md` as context — it's standard Markdown. | + +## Tips + +- If your repository already defines Python conventions, those win — this skill + is a sensible default, not a mandate. +- Keep the front-matter `description` intact: skill-aware agents use it to decide + *when* to activate the skill automatically. diff --git a/skills/python-best-practices/SKILL.md b/skills/python-best-practices/SKILL.md new file mode 100644 index 0000000000..ff3f214699 --- /dev/null +++ b/skills/python-best-practices/SKILL.md @@ -0,0 +1,540 @@ +--- +name: python-best-practices +version: 1.0.0 +description: >- + Apply modern, idiomatic Python (3.10+) best practices when writing, refactoring, + or reviewing Python code. Covers type hints (PEP 484), dataclasses and Pydantic + models, context managers, generators/iterators, the decorator pattern, async/await + with asyncio, structural pattern matching, the src project layout, dependency + management with Poetry/uv, and formatting/linting with PEP 8, Black, and Ruff. + Use this skill whenever the task involves authoring or improving Python code. +license: MIT +compatibility: >- + Universal Markdown + YAML front matter. Works as an Anthropic/Claude Agent Skill + (SKILL.md), and as a generic instruction/context file for Cursor, GitHub Copilot, + Windsurf, Codeium, Cline, Aider, and any agent that can read Markdown. +tags: + - python + - typing + - pydantic + - asyncio + - packaging + - linting +--- + +# Python Best Practices + +> A portable engineering "skill" for writing modern, production-grade Python. +> Target runtime: **Python 3.10+** (some notes call out 3.11/3.12 features). +> Default tooling: **uv** or **Poetry**, **Ruff**, **Black**, **mypy**/**pyright**, **pytest**. + +When this skill is active, follow the principles and patterns below. Prefer +clarity over cleverness, make illegal states unrepresentable through types, and +keep functions small and pure where practical. + +--- + +## 0. Quick checklist (apply to every change) + +- [ ] Public functions, methods, and module-level constants have **type hints**. +- [ ] Data containers use `@dataclass` (internal) or **Pydantic** (validation/IO boundaries). +- [ ] Resources (files, sockets, locks, DB sessions) are managed with **context managers**. +- [ ] Large/streamed sequences use **generators**, not eagerly-built lists. +- [ ] Cross-cutting concerns (retry, cache, timing, auth) use **decorators**. +- [ ] I/O-bound concurrency uses **async/await + asyncio**; CPU-bound work uses processes. +- [ ] Branching on the *shape* of data uses **structural pattern matching** (`match`). +- [ ] Code lives under a **`src/` layout** with a single `pyproject.toml`. +- [ ] Code passes **Ruff** (lint + import sort) and **Black** (format) with no errors. +- [ ] No bare `except:`; exceptions are specific and never silently swallowed. +- [ ] No mutable default arguments; use `None` sentinel + assignment inside the body. + +--- + +## 1. Type hints (PEP 484 and beyond) + +Type hints are mandatory for public APIs and strongly encouraged everywhere. +Use **modern built-in generics** (PEP 585) and the **`X | Y` union syntax** (PEP 604). + +```python +from __future__ import annotations # safe forward refs + cheaper annotations + +from collections.abc import Iterable, Mapping, Sequence +from typing import Final, Literal, Protocol, TypeVar, overload + +DEFAULT_TIMEOUT: Final[float] = 5.0 # module constant, immutable intent + +def normalize(values: Sequence[float]) -> list[float]: + total = sum(values) or 1.0 + return [v / total for v in values] + +# Prefer `X | None` over Optional[X]; prefer built-in generics over typing.List etc. +def find_user(user_id: int) -> User | None: ... + +# Literal for closed sets of string/enum-like options. +def set_mode(mode: Literal["r", "w", "a"]) -> None: ... +``` + +Guidelines: + +- Use `collections.abc` types (`Iterable`, `Sequence`, `Mapping`, `Callable`) for + **parameters** (accept the most general type) and concrete types (`list`, `dict`) + for **return values** (be specific about what you produce). +- Use `TypeVar`/generics for reusable polymorphic code; use `ParamSpec` for + decorators that must preserve a wrapped function's signature. +- Use `Protocol` for **structural typing** ("duck typing with checks") instead of + forcing inheritance. +- Use `typing.Self` (3.11+) for fluent/builder return types; `assert_never` for + exhaustiveness checks in `match` statements. +- Run a static type checker in CI: **mypy** (`--strict`) or **pyright**. Treat type + errors as build failures. + +```python +from typing import ParamSpec, TypeVar + +P = ParamSpec("P") +R = TypeVar("R") + +class Comparable(Protocol): + def __lt__(self, other: object, /) -> bool: ... + +def smallest[T: Comparable](items: Iterable[T]) -> T: # PEP 695 generics (3.12+) + return min(items) +``` + +--- + +## 2. Dataclasses vs. Pydantic models + +**Rule of thumb:** use `@dataclass` for *internal*, trusted data; use **Pydantic** +at the *boundaries* (API requests/responses, config, deserializing external data) +where you need validation, parsing, and serialization. + +### 2.1 Dataclasses (stdlib, zero deps) + +```python +from dataclasses import dataclass, field + +@dataclass(slots=True, frozen=True, kw_only=True) +class Point: + x: float + y: float + tags: list[str] = field(default_factory=list) # never use mutable defaults + + def distance_to(self, other: "Point") -> float: + return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5 +``` + +- `slots=True` -> lower memory, faster attribute access, blocks accidental attrs. +- `frozen=True` -> immutable & hashable; prefer immutability by default. +- `kw_only=True` -> explicit, order-independent construction. +- Use `field(default_factory=...)` for mutable defaults. +- Use `__post_init__` for light cross-field derivation (not heavy validation). + +### 2.2 Pydantic v2 (validation boundary) + +```python +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field, field_validator + +class CreateUser(BaseModel): + name: str = Field(min_length=1, max_length=100) + email: EmailStr + age: int = Field(ge=0, le=150) + created_at: datetime = Field(default_factory=datetime.utcnow) + + @field_validator("name") + @classmethod + def strip_name(cls, v: str) -> str: + return v.strip() + +# Settings from env/files: +from pydantic_settings import BaseSettings, SettingsConfigDict + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env") + database_url: str + debug: bool = False +``` + +- Validate untrusted input once at the edge, then pass typed objects inward. +- Use `model_validate` / `model_dump` (v2) rather than legacy `parse_obj`/`dict`. +- Don't reach for Pydantic for purely internal structs — a dataclass is lighter. + +--- + +## 3. Context managers (deterministic resource handling) + +Always use `with` for anything that must be released. Prefer `contextlib` helpers +over hand-written `__enter__`/`__exit__` when possible. + +```python +from contextlib import contextmanager, suppress +from collections.abc import Iterator +from pathlib import Path +import time + +@contextmanager +def timer(label: str) -> Iterator[None]: + start = time.perf_counter() + try: + yield + finally: + elapsed = time.perf_counter() - start + print(f"{label}: {elapsed:.3f}s") + +with timer("load"): + data = Path("big.json").read_text(encoding="utf-8") + +# Multiple resources in one statement: +with open("in.txt") as src, open("out.txt", "w") as dst: + dst.write(src.read()) + +# Targeted suppression instead of try/except/pass: +with suppress(FileNotFoundError): + Path("maybe.tmp").unlink() +``` + +- Async resources use `async with` and `contextlib.asynccontextmanager`. +- Use `contextlib.ExitStack` to manage a dynamic number of resources. +- Never leak file handles, sessions, or locks via manual open/close. + +--- + +## 4. Generators and iterators (lazy, memory-efficient) + +Prefer lazy evaluation for large or streaming data. Generators keep memory flat +and compose cleanly. + +```python +from collections.abc import Iterator, Iterable + +def read_lines(path: str) -> Iterator[str]: + with open(path, encoding="utf-8") as f: + for line in f: # lazily streams; doesn't load whole file + yield line.rstrip("\n") + +def batched[T](items: Iterable[T], size: int) -> Iterator[list[T]]: + batch: list[T] = [] + for item in items: + batch.append(item) + if len(batch) == size: + yield batch + batch = [] + if batch: + yield batch +``` + +- Use generator **expressions** `(x for x in it)` instead of building throwaway lists. +- Use `yield from` to delegate to sub-generators. +- Reach for `itertools` (`chain`, `islice`, `groupby`, `tee`, `accumulate`) and + `functools.reduce` before writing manual loops. +- For 3.12+, `itertools.batched` exists in the stdlib. + +--- + +## 5. Decorator pattern + +Use decorators for cross-cutting concerns. **Always** preserve metadata with +`functools.wraps`, and preserve the type signature with `ParamSpec`. + +```python +import functools +import time +from collections.abc import Callable +from typing import ParamSpec, TypeVar + +P = ParamSpec("P") +R = TypeVar("R") + +def retry(times: int = 3, delay: float = 0.5) -> Callable[[Callable[P, R]], Callable[P, R]]: + def decorator(func: Callable[P, R]) -> Callable[P, R]: + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + last_exc: Exception | None = None + for attempt in range(1, times + 1): + try: + return func(*args, **kwargs) + except Exception as exc: # narrow this in real code + last_exc = exc + if attempt < times: + time.sleep(delay * attempt) + assert last_exc is not None + raise last_exc + return wrapper + return decorator + +@retry(times=5) +def fetch(url: str) -> bytes: ... +``` + +- Use `functools.lru_cache` / `functools.cache` for pure, hashable-arg functions. +- Use `functools.cached_property` for expensive, lazily-computed attributes. +- For async functions, write `async def wrapper` and `await func(...)`. +- Keep decorators single-purpose and composable. + +--- + +## 6. async/await with asyncio + +Use async for **I/O-bound** concurrency (network, disk, DB). It does **not** speed +up CPU-bound work — use `multiprocessing` / `ProcessPoolExecutor` for that. + +```python +import asyncio +from collections.abc import Sequence + +async def fetch_one(client, url: str) -> str: + async with client.get(url) as resp: + return await resp.text() + +async def fetch_all(urls: Sequence[str]) -> list[str]: + import aiohttp + async with aiohttp.ClientSession() as client: + async with asyncio.TaskGroup() as tg: # 3.11+; structured concurrency + tasks = [tg.create_task(fetch_one(client, u)) for u in urls] + return [t.result() for t in tasks] + +def main() -> None: + urls = ["https://example.com"] + results = asyncio.run(fetch_all(urls)) + print(len(results)) +``` + +Guidelines: + +- One entry point: `asyncio.run(main())`. Don't manually manage event loops. +- Prefer `asyncio.TaskGroup` (3.11+) over bare `gather` for cancellation safety. +- Use `asyncio.timeout()` (3.11+) / `wait_for` for deadlines. +- Never call blocking functions inside a coroutine; wrap them with + `await asyncio.to_thread(blocking_fn, ...)`. +- Don't mix sync and async APIs randomly; keep boundaries explicit. + +--- + +## 7. Structural pattern matching (3.10+) + +Use `match`/`case` to branch on the **structure** of data — far cleaner than long +`if/elif` chains for ASTs, commands, JSON shapes, and tagged unions. + +```python +from typing import assert_never + +type Shape = Circle | Rectangle # PEP 695 type alias (3.12+) + +def area(shape: Shape) -> float: + match shape: + case Circle(radius=r): + return 3.14159 * r * r + case Rectangle(width=w, height=h): + return w * h + case _ as unreachable: + assert_never(unreachable) # exhaustiveness checked statically + +def handle(event: dict) -> str: + match event: + case {"type": "click", "x": int(x), "y": int(y)}: + return f"click at {x},{y}" + case {"type": "key", "value": str(k)}: + return f"key {k}" + case {"type": str(t)}: + return f"unknown: {t}" + case _: + return "malformed" +``` + +- Use **capture**, **class**, **mapping**, **sequence**, and **OR** (`a | b`) patterns. +- Add **guards**: `case Point(x=x, y=y) if x == y:`. +- Use `assert_never` (with mypy/pyright) to guarantee all cases are handled. +- Don't overuse it for simple equality — a dict dispatch or `if` may be clearer. + +--- + +## 8. Project structure — the `src/` layout + +Use a **`src/` layout**. It prevents accidental imports of the in-tree package +(forcing you to test the *installed* package) and keeps the root clean. + +``` +my-project/ +├── pyproject.toml # single source of truth (build, deps, tools) +├── README.md +├── LICENSE +├── .gitignore +├── .pre-commit-config.yaml +├── src/ +│ └── my_package/ +│ ├── __init__.py +│ ├── py.typed # ships type info (PEP 561) +│ ├── core.py +│ └── cli.py +└── tests/ + ├── __init__.py + └── test_core.py +``` + +- Package name uses underscores; the distribution/repo name may use hyphens. +- Add an empty `py.typed` marker so consumers get your type hints. +- Keep `__init__.py` thin: expose a curated public API, avoid heavy import-time work. +- Put runnable entry points behind `if __name__ == "__main__":` and/or + `[project.scripts]`. + +--- + +## 9. Dependency management — Poetry or uv + +Use **`pyproject.toml`** as the single configuration file. Prefer **uv** for speed +(it's a fast resolver/installer and project manager) or **Poetry** for a mature, +all-in-one workflow. **Always commit the lock file.** + +### 9.1 uv (recommended for new projects) + +```bash +uv init --package my-project # create src-layout project +uv add httpx pydantic # add runtime deps (updates pyproject + uv.lock) +uv add --dev ruff black mypy pytest +uv run pytest # run inside the managed venv +uv sync # reproduce env from uv.lock +uv lock --upgrade # refresh the lock file +``` + +### 9.2 Poetry + +```bash +poetry new --src my-project +poetry add httpx pydantic +poetry add --group dev ruff black mypy pytest +poetry install +poetry run pytest +``` + +### 9.3 `pyproject.toml` (PEP 621) skeleton + +```toml +[project] +name = "my-package" +version = "0.1.0" +description = "..." +requires-python = ">=3.10" +dependencies = ["httpx>=0.27", "pydantic>=2.6"] + +[project.optional-dependencies] +dev = ["ruff", "black", "mypy", "pytest", "pytest-cov", "pre-commit"] + +[project.scripts] +my-cli = "my_package.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +- Pin Python with `requires-python`. +- Separate runtime vs. dev dependencies (groups / optional-dependencies). +- Never `pip install` ad hoc into a shared environment; use the managed venv + + lock file so builds are reproducible. + +--- + +## 10. Style, formatting & linting — PEP 8, Black, Ruff + +Formatting and linting are **not negotiable** and should be automated, not debated. + +- **PEP 8** is the baseline style guide (naming, spacing, imports, line length). +- **Black** is the opinionated, deterministic formatter — accept its output. +- **Ruff** is an extremely fast linter (and formatter) that subsumes flake8, + isort, pyupgrade, pydocstyle, and many plugins. Use it as the primary linter and + import sorter; it's Black-compatible. + +Naming (PEP 8): + +- `snake_case` for functions, methods, variables, modules. +- `PascalCase` for classes and type aliases. +- `UPPER_SNAKE_CASE` for constants. +- `_leading_underscore` for non-public/internal; avoid `l`, `O`, `I` single names. + +### 10.1 Config in `pyproject.toml` + +```toml +[tool.black] +line-length = 88 +target-version = ["py310"] + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "PTH", "RUF"] +ignore = ["E501"] # let the formatter own line length + +[tool.ruff.lint.isort] +known-first-party = ["my_package"] + +[tool.mypy] +python_version = "3.10" +strict = true +warn_unused_ignores = true +``` + +### 10.2 Commands + +```bash +ruff check . --fix # lint + autofix (incl. import sorting) +ruff format . # or: black . +mypy src # static type check +pytest -q # tests +``` + +### 10.3 pre-commit (enforce locally and in CI) + +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.0 + hooks: + - id: mypy +``` + +--- + +## 11. General principles & anti-patterns + +**Do** + +- Fail fast with specific exceptions; add context (`raise X("msg") from err`). +- Prefer `pathlib.Path` over `os.path`; prefer f-strings over `%`/`.format`. +- Keep functions short and single-purpose; return early to reduce nesting. +- Make dependencies explicit (pass them in) for testability; avoid global state. +- Write docstrings for public APIs (summary line + args/returns/raises). +- Use `logging` (configured at the app edge), never `print`, for diagnostics. +- Write `pytest` tests; prefer `assert`, fixtures, and parametrization. + +**Avoid** + +- Mutable default arguments (`def f(x=[])`). Use `None` + assign inside. +- Bare `except:` or `except Exception: pass` that hides failures. +- `from module import *`; star imports pollute namespaces. +- Comparing to `None`/`True`/`False` with `==` (use `is`). +- Catch-and-reraise that loses the traceback (use `raise` / `from`). +- Premature optimization and clever one-liners that hurt readability. + +--- + +## 12. How to apply this skill + +1. **Before coding:** confirm the target Python version and whether a `src/` + project + `pyproject.toml` already exist; if not, scaffold them. +2. **While coding:** add type hints, choose dataclass vs. Pydantic deliberately, + manage resources with context managers, prefer generators/async where it fits, + and use `match` for structural branching. +3. **Before finishing:** run `ruff check --fix`, `ruff format` (or `black`), + `mypy`/`pyright`, and `pytest`. Fix all reported issues. +4. **Match the project:** if the repo already has conventions/config, follow them + over these defaults — consistency within a codebase wins.