mcpbeat Sign in

Python Agent Skill

Use when the task is Python itself, in any framework or none: PEP 695 generics, mypy --strict typing, dataclass/Protocol/TypedDict/Enum choices, asyncio.TaskGroup, stdlib idioms, src/ layout + pyproject.toml with uv, ruff+mypy+pytest gate. NOT a FastAPI/ASGI service (that is fastapi), NOT a deep pytest suite (that is testing-py).

10k tokens
context cost
the whole folder, loaded on every use
7
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill python

What comes with it

25 823 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/async.md
references/stdlib.md
references/typing.md
scripts/verify.sh

The instruction itself

14 sections, as written by the author

Modern Python at the language level

Write, review, modernize, type, and package Python that reads like a typed,

flat-control-flow, stdlib-first program whose dependencies and tooling all live in one

pyproject.toml. Types are part of the design, not decoration; the stdlib is large and you

reach for it before a dependency; correctness is ruff + a type checker + pytest in one gate.

Targets Python 3.12+ (floor) / 3.14 (current, released 7 Oct 2025): PEP 695 inline

type parameters (class Box[T]:, type Alias = ...), asyncio.TaskGroup, and — in 3.14 —

deferred annotation evaluation by default (PEP 649/749, no more `from __future__ import

annotations), PEP 750 template strings (t"..."), and compression.zstd`. Tooling pins:

uv 0.11 (project + package manager), ruff 0.15 (lint + format), **mypy 1.20

--strict (or Astral's ty, still preview — default to mypy), pytest 8**.

Scope — what this skill delegates

  • Building a FastAPI / ASGI service (routes, Pydantic models, SQLAlchemy, uvicorn) ->

fastapi. That skill owns the service shape; this one owns the

language it is written in.

  • A pytest suite as the primary task — fixtures, parametrize matrices, mocking, coverage,

property-based testing -> testing-py (this skill keeps only the *baseline*: a few tests

so verify.sh has something to run, then hands off).

  • Language-agnostic threat modeling / authz / OWASP review ->

secure-coding (this skill keeps Python-specific safety: no

eval/pickle of untrusted data, subprocess without shell=True, secrets over random).

  • Containerfile / CI pipeline / deploy mechanics -> deployment

(this skill ships only a uv-based CI note).

  • Another language -> go, typescript, rust, etc. Django ORM/models/migrations -> django.

Python typing, async language semantics, and uv packaging live here, not in a separate

skill — this skill is the canonical authority for the language substrate under any Python program.

Decision rules

Apply on every Python edit:

  • Type the boundary. Every public function, method, and module-level name gets an

annotation; let inference handle locals. Untyped public API defeats --strict.

  • Flat control flow. Guard-clause and return/raise early; keep the happy path

unindented — arrow code hides the logic.

  • Stdlib before a dependency. pathlib, itertools, functools, dataclasses,

collections cover most needs; a new dep is a maintenance liability you must justify.

  • Prefer immutability. @dataclass(frozen=True, slots=True) for value objects; mutate

only where you must — shared mutable state is the bug you debug at 2am.

  • Everything passes the gate. ruff check + ruff format --check + mypy --strict +

pytest — green locally via scripts/verify.sh before you call it done.

Typing

Type the boundary; run mypy --strict so untyped code and implicit Any are errors, not

silent gaps. Use PEP 695 inline syntax for all new generic code — no explicit TypeVar

objects:

# Good (3.12+): inline type parameter and the `type` alias statement.
def first[T](xs: list[T]) -> T:
    return xs[0]

class Box[T]: ...
type UserId = int                 # `type` statement: a real alias, lazily evaluated
# Bad (legacy): `T = TypeVar("T")` then a Generic[T] — fine in old code, don't write it new.

Core choices: Protocol (structural, no inheritance) over an ABC when you only need "has these

methods"; X | None (not Optional[X]); Literal/Enum for closed value sets; Final for

constants; Self for fluent returns; @overload for signature families. Narrow with

isinstance, assert, or an is None guard — mypy follows the flow. In 3.14 annotations

are lazy by default (PEP 649/749), so forward references resolve without `from __future__ import

annotations`.

Full PEP 695 bounds/constraints/variance, Protocol vs ABC, TypedDict

Required/NotRequired, ParamSpec/TypeVarTuple, TypeGuard/TypeIs, cast, and common

--strict errors with fixes -> references/typing.md.

Data modeling

Pick the carrier by what the data *is*, not by habit:

| Need | Use | Why |

| --- | --- | --- |

| Immutable value object, typed fields, methods | @dataclass(frozen=True, slots=True) | Hashable, no __dict__ overhead, real types |

| Small fixed tuple, positional + named, immutable | NamedTuple | Tuple semantics + field names; cheap |

| Shape of an external/JSON dict, no runtime class | TypedDict | Types a plain dict without wrapping it |

| Closed set of named constants | Enum / StrEnum / IntEnum | One source of truth; Literal-friendly |

| Mutable bag of related state with behavior | plain class / @dataclass | When you genuinely need mutation |

| Runtime-validated I/O model (parse untrusted data) | Pydantic -> fastapi | Validation is a service concern, not language |

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float
    def translated(self, dx: float, dy: float) -> "Point":
        return Point(self.x + dx, self.y + dy)   # returns a new value, never mutates

Frozen-slots dataclass when you want methods + hashability + clear types; NamedTuple when

the thing genuinely *is* a small tuple you also unpack positionally.

Stdlib idioms

Reach into the stdlib before adding a dependency.

  • pathlib for all filesystem paths: Path("data") / name, p.read_text(),

p.glob("*.json"), p.with_suffix(".bak") — typed and composable, never os.path.join.

  • collections: defaultdict(list), Counter(words), deque(maxlen=100) for ring buffers.
  • itertools: chain, groupby, islice, batched (3.12+) instead of hand-rolled loops.
  • functools: @cache / @lru_cache for pure memoization, @cached_property, partial.
  • contextlib: @contextmanager, ExitStack for dynamic resource sets, suppress(FileNotFoundError).
  • logging, never print in a library: print writes to a caller's stdout you do not own;

logging.getLogger(__name__) lets them configure it.

Prefer comprehensions over map/filter+lambda; prefer a generator ((... for ...)) when

you only iterate once. Use match for structural dispatch over a chain of isinstance:

from functools import cache
from pathlib import Path

@cache
def config_dir() -> Path:                 # computed once, memoized
    return Path.home() / ".config" / "myapp"

def area(shape: object) -> float:
    match shape:                          # structural dispatch, captures by attribute/key
        case {"kind": "circle", "r": float(r)}:
            return 3.14159 * r * r
        case _:
            raise TypeError(f"unknown shape: {shape!r}")

f-strings for formatting; note 3.14's PEP 750 t"..." template strings yield a Template

(not a str) for *safe custom interpolation* (e.g. escaping) — use them when an f-string would

inject untrusted text. Full cookbook (itertools/functools/collections recipes, match

patterns, dataclass field/default_factory/__post_init__, Enum/StrEnum/IntFlag) ->

references/stdlib.md.

Errors & resources

Define a small exception hierarchy rooted in one base so callers can catch broadly or

narrowly; chain causes with raise ... from; never write a bare except:.

class AppError(Exception): ...
class NotFoundError(AppError): ...

def load(path: Path) -> str:
    try:
        return path.read_text()
    except FileNotFoundError as e:
        raise NotFoundError(f"missing {path}") from e   # preserves the cause chain

Use except* to handle an ExceptionGroup (what a TaskGroup raises) by member type.

Prefer EAFP (try the operation, handle the failure) over LBYL race-prone pre-checks.

Always release resources with with (a context manager), not manual try/finally close.

Async (language level)

Use asyncio.run(main()) as the single entry point. **asyncio.TaskGroup (3.11+) over bare

gather** — it is the structured-concurrency primitive: a child failure cancels its siblings

and surfaces as an ExceptionGroup, and no task outlives the block.

import asyncio

# Bad: gather leaks the other tasks on first failure and loses structure.
async def fetch_all_bad(ids: list[int]) -> list[bytes]:
    return await asyncio.gather(*(fetch(i) for i in ids))

# Good: TaskGroup — sibling cancellation on error, bounded lifetime, real grouping.
async def fetch_all(ids: list[int]) -> list[bytes]:
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch(i)) for i in ids]
    return [t.result() for t in tasks]                  # block exited => all done or raised

Bound every wait with async with asyncio.timeout(5.0):. On CancelledError, clean up and

re-raise — swallowing it breaks cancellation for the whole tree. Async is for IO-bound

concurrency only; CPU-bound work blocks the loop — push it to asyncio.to_thread / a

ProcessPoolExecutor (or 3.14's free-threaded build). **HTTP servers belong to

fastapi, not here.** Runtime model, ExceptionGroup/except*, queues

with backpressure, cancellation discipline, and sync<->async bridging ->

references/async.md.

Project layout & packaging (uv)

Use a src/ layout so tests import the installed package, not the source tree by accident:

myapp/
  src/myapp/__init__.py
  src/myapp/core.py
  tests/test_core.py
  pyproject.toml
  uv.lock              # committed
  scripts/verify.sh

pyproject.toml is the single config — PEP 621 metadata, dependency groups, and tool config.

Never hand-edit a requirements.txt; uv add writes the dep and updates uv.lock, which you commit.

[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["httpx>=0.27"]

[project.scripts]
myapp = "myapp.core:main"          # console entry point

[dependency-groups]
dev = ["ruff>=0.15", "mypy>=1.13", "pytest>=8"]

[build-system]
requires = ["uv_build>=0.11"]
build-backend = "uv_build"         # uv's own backend, stable since July 2025

[tool.ruff]
line-length = 100

[tool.mypy]
strict = true

Core uv verbs (each updates uv.lock, which you commit):

uv init --package myapp        # scaffold pyproject.toml + src/ + .venv
uv add httpx                   # add a runtime dep
uv add --dev ruff mypy pytest  # add to the dev group
uv sync --frozen               # install exactly from the lockfile (CI + fresh clones)
uv run pytest -q               # run inside the managed venv
uv python install 3.14         # pin/install an interpreter

Quality gate + verify.sh

One local gate, mirroring CI: ruff check --fix ., ruff format ., mypy --strict src

(or ty check), pytest -q. scripts/verify.sh runs all of them, skips a missing tool/dir

with a warning, and exits non-zero on any real failure — run ./scripts/verify.sh from the

project root before declaring done. CI is astral-sh/setup-uv + uv sync --frozen + the same

four commands; full pipeline -> deployment.

pytest baseline

Carry just enough to make verify.sh meaningful — plain test_* functions, assert,

pytest.raises, one parametrize, tmp_path for files:

import pytest
from myapp.core import head

@pytest.mark.parametrize("xs, expected", [(["a", "b"], "a"), (["x"], "x")])
def test_head_returns_first(xs: list[str], expected: str) -> None:
    assert head(xs) == expected

def test_head_rejects_empty() -> None:
    with pytest.raises(ValueError):
        head([])

def test_writes_file(tmp_path) -> None:
    (tmp_path / "f.txt").write_text("hi")
    assert (tmp_path / "f.txt").read_text() == "hi"

Deep fixtures, mocking, coverage gates, and property-based testing belong to testing-py

stop at the baseline and hand off.

Security (embedded, Python-specific)

Generic threat modeling and authz live in secure-coding; these Python-specific controls stay here:

# Bad                                          # Good
eval(user_input)                                ast.literal_eval(user_input)   # never eval/exec input
pickle.loads(network_bytes)                     json.loads(network_bytes)      # never unpickle untrusted data
subprocess.run(cmd, shell=True)                 subprocess.run(["ls", path])   # list args, no shell=True
random.random()  # tokens                       secrets.token_urlsafe(32)      # secrets, not random, for secrets

Keep deps locked (uv.lock) and audited (pip-audit / uv resolution); read secrets from

env or a secret manager, never hardcode or log them.

Anti-patterns -> STOP

| Tempting move | Reality / do instead |

| --- | --- |

| "def f(xs=[]) is fine, it's empty" | One list shared across all calls; use = None then xs = xs or []. |

| "bare except: to be safe" | Swallows KeyboardInterrupt/bugs; catch a specific type. |

| "from module import *" | Pollutes the namespace, breaks tooling; import names explicitly. |

| "print() to debug this library" | Writes to a stdout you don't own; use logging.getLogger(__name__). |

| "os.path.join is what I know" | pathlib.Path is typed and composable; use /. |

| "I'll add types later" | Untyped public API defeats --strict; type the boundary now. |

| "edit requirements.txt by hand" | Drifts from the lock; uv add / uv remove and commit uv.lock. |

| "asyncio.gather is simpler" | Leaks siblings on failure; TaskGroup for structured concurrency. |

| "swallow CancelledError, it's noise" | Breaks cancellation for the whole tree; clean up and re-raise. |

| "time.sleep inside this coroutine" | Blocks the event loop; await asyncio.sleep(...). |

| "pickle.loads the cache, it's ours" | Any untrusted byte = code execution; use json. |

| "explicit TypeVar everywhere" | New code uses PEP 695 def f[T] / class C[T] / type X. |

Project grounding (02-DOCS)

In a project that has the harness wiki, record this project's Python

conventions in 02-DOCS/wiki/stack/python.md and index it in 02-DOCS/wiki/index.md — the

interpreter floor, src/ layout, uv workflow, ruff/mypy config, async-vs-sync stance, and

data-modeling defaults. Read it first on every use and bump its Updated date when a convention

changes. This is *recorded, not gated*: never block the task on it, and skip silently when there

is no 02-DOCS/ layer.

How to use it

Copy the folder

Take ericrisco/python from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.