agentleFS
base58edbase58ed/claude_code_files/skills/python-skill/SKILL.md

python

Skill1 starsChanged 3 months agoMITView on GitHub

Python standard and patterns. ALWAYS load for .py files, .ipynb notebooks, Python code, uv, pip, pyproject.toml, pytest, ruff, pyright, mypy, asyncio, FastAPI, pydantic, dataclasses, type hints, data scripts/ETL, PyTorch (pytorch-skill complements this skill — load both), or any Python-related task — even when the user doesn't say Python explicitly.

44 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use android-kotlin, csharp and 8 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • alinaqiSkill

    android-kotlin

    Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing

    Also in 4 projects that use this

  • testdino-hqSkill

    csharp

    Google's official C# style guide. Covers naming conventions, formatting, LINQ, async/await, XML documentation, and .NET best practices.

    Also in 4 projects that use this

  • ffsshhttiikkSkill

    flutter

    Flutter cross-platform framework

    Also in 4 projects that use this

  • mcollinaSkill

    node

    Provides domain-specific best practices for Node.js development with TypeScript, covering type stripping, async patterns, error handling, streams, modules, testing, performance, caching, logging, and more. Use when setting up Node.js projects with native TypeScript support, configuring type stripping (--experimental-strip-types), writing Node 22+ TypeScript without a build step, or when the user mentions 'native TypeScript in Node', 'strip types', 'Node 22 TypeScript', '.ts files without compilation', 'ts-node alternative', or needs guidance on error handling, graceful shutdown, flaky tests, profiling, or environment configuration in Node.js. Helps configure tsconfig.json for type stripping, set up package.json scripts, handle module resolution and import extensions, and apply robust patterns across the full Node.js stack.

    Also in 4 projects that use this

  • testdino-hqSkill

    cpp

    Google's official C++ style guide. Covers headers, naming conventions, formatting, classes, memory management, RAII, smart pointers, and modern C++ features.

    Also in 3 projects that use this

  • dylantarreSkill

    angular

    Builds token-driven Angular components with signals and standalone components. Use when creating Angular component libraries, integrating design tokens, or building design system components with new control flow syntax.

    Also in 3 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. 0 are common, 4 are distinctive, and 9 are very specific to this project.

  • 0

    Common

    Most of the 40 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use Python 3.14+. Verify feature availability when uncertain.

    line 8
  2. Very specific:

    No global variables or shared mutable state.

    line 24
  3. Distinctive:

    No variable name reuse across scopes: when the types differ — pyright may accept it, but mypy catches it and humans misread it.

    line 94
  4. Distinctive:

    Editing counts too: When you change a function, keep the comments already there, fix any your change made wrong, and add the missing …

    line 121
  5. Distinctive:

    When code review flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:

    line 128
  6. Very specific:

    Add case _ as unreachable: assert_never(unreachable) to EVERY match over a union type — describe() under Enums and ADTs below is the …

    line 165
  7. Very specific:

    No blocking calls in async context (time.sleep, blocking I/O) — freezes the event loop

    line 429
  8. Very specific:

    Load references/concurrency.md: when writing async/await, using TaskGroup/Semaphore, choosing an executor, or debugging a hang.

    line 435
  9. Very specific:

    Load references/runtime.md: when configuring uvicorn, implementing graceful shutdown, writing retry-with-backoff, setting timeouts, or …

    line 437
  10. Very specific:

    Before committing or completing a review, load references/checklist.md for the full checklist.

    line 578
  11. Very specific:

    Every variable has a type annotation.

    line 581
  12. Very specific:

    Every match over a union has case _ as unreachable: assert_never(unreachable)

    line 584
  13. Very specific:

    Trust but verify the formatter: After ruff format, run python -m py_compile on the touched files — formatters have shipped syntax-breaking …

    line 591

“Common” is measured: kept by at least 6 in 10 of the 40 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Version Policy

Use Python 3.14+. Verify feature availability when uncertain.

Philosophy

  • Modular, clear, readable code with care for performance and efficiency
  • Obvious over clever — if it needs a what-comment, simplify it; complexity (big-O) and WHY comments are mandatory, not a smell
  • PEP-585 types EVERYWHERE — all variables, parameters, returns, constants
  • Result[T] is the default — every fallible operation returns Result. Validating construction uses .create() factory methods that return Result. Raising exceptions is a narrow escape hatch (dataclass __post_init__ defensive guards, framework integration), NOT a peer of Result.err.
  • Pattern match over isinstance chains — sealed unions + match/case + assert_never for exhaustiveness
  • No dynamic magic — no runtime attribute lookup, no metaclasses, no monkey-patching (full list in "No Dynamic Magic" section below)
  • Explicit over implicit, flat over nested (early returns, guard clauses)
  • Guard clauses over .unwrap()/.expect() — return Err(), don't raise
  • Each module does ONE thing well; depend on abstractions (Protocol), not implementations; compose small pieces into larger systems
  • Solve the actual problem, not a generalized version; delete aggressively — git preserves history
  • assert for DEBUG-only invariants — stripped by python -O, never use for runtime validation ("Where to Assert" below has the full translation; S101 is globally ignored to make this legal)
  • No cast() without prior validation — cast lies to pyright
  • No global variables or shared mutable state. For stateful lifetimes (caches, pools, install guards): encapsulate in a class, expose one stable module-level instance, mutate self — never rebind the module-level name (rebinding breaks every from module import instance alias)

Rust → Python Rosetta

The house idioms, keyed by the Rust reflex they replace. Each row's full treatment lives in the named section.

RustHereSection
Result<T, E> + ?Result[T]; pre-declare, match, early return eResult[T] Pattern
enum + matchfrozen dataclass per variant + type union + assert_neverEnums and ADTs
traitProtocol — structural, no inheritanceProtocol
newtype@dataclass(frozen=True, slots=True) + .create()Make Invalid States Unrepresentable
Option<T>T | None, converted to Result at lookup boundariesNone and JSON at Boundaries
let (no mut)annotated binding, never rebound; Final for constantsType Annotations
unsafecast / # type: ignore — narrow, justified, commentedNo Dynamic Magic
cargo / clippy / rustcuv / ruff / basedpyright --strictTooling Enforcement

No Dynamic Magic

The goal is code that pyright --strict can verify end to end — that rules out features which defeat static analysis and make refactors unsafe:

  • NO getattr / setattr / hasattr with string literals for field access. Use explicit attributes; for a genuine dispatch table, a typed dict[str, Callable[..., T]].
  • NO __getattr__ / __setattr__ / __getattribute__ overrides on domain classes — the runtime diverges from the annotations. Same spirit: never object.__setattr__(frozen_model, field, value) to bypass frozen=True — see Data Modeling and references/frozen_models.md.
  • NO metaclasses outside test-framework internals. Use a decorator, a Protocol, or an explicit registry.
  • NO eval / exec / compile of user or configuration input; NO runtime class generation (type("Foo", (Base,), {...})).
  • NO monkey-patching (module.func = other_func) outside of test setup.
  • NO **kwargs: Any pass-through without documenting the expected keys. Prefer typed parameters, a TypedDict, or a dataclass carrying the options.
  • NO cast(T, x) without a preceding runtime check that actually narrows — use isinstance, a guard clause, or Result-based validation first.
  • PREFER Protocol over abc.ABC for interfaces — structural typing plus pyright strict beats runtime isinstance checks.
  • CAUTION with @runtime_checkable — see the Protocol section.

Performance

  • Correct first, then clear, then fast
  • Measure before optimizing — intuition is often wrong
  • Optimize hot paths only — 90% of code doesn't matter
  • Simple code is often fast code (cache-friendly, predictable); complexity for performance is a last resort, with comments explaining WHY (which benchmark, what gain)

Type Annotations (Mandatory)

Every variable assignment carries a type annotation — locals included — the house delta from common style, which leans on inference. PEP-585 built-in generics, | unions, PEP-695 type aliases.

def process_items(items: Sequence[str]) -> dict[str, int]:
    result: dict[str, int] = {}
    for item in items:
        count: int = len(item)   # yes, even loop locals
        result[item] = count
    return result

left: TreeNode | None = None     # no quotes on forward refs — 3.14 deferred annotations (PEP 649)
type Handler = Callable[[Request], Response]   # PEP-695 — typing.TypeVar/Generic are lint-banned

Accept broad, return narrow: parameters take read-only ABCs (Sequence[T] / Mapping[K, V] / Iterable[T]); returns are concrete (list[T] / dict[K, V]).

Immutable by default: module constants are Final[T] (MAX_RETRIES: Final[int] = 3); a local is never rebound to a new meaning — rename instead. Rust's let without mut is the default here too.

Function Discipline

  • Max ~40 lines per function, 3-4 parameters
  • 2-3 nesting levels — the Python translation of the global 2-level default (with/try/match each burn a level); the lintable ceiling is 3 (PLR1702, max-nested-blocks = 3)
  • Single responsibility: one reason to change; early returns over nested conditionals
  • No variable name reuse across scopes when the types differ — pyright may accept it, but mypy catches it and humans misread it. Rename the inner variable.

Non-trivial functions carry a big-O comment for compute AND memory, derived DFS through the call tree: callees count toward the bound, and recursion counts its activation frames as memory. Trivially O(1)/O(n) glue (single-pass comprehensions, dict passes) does not get annotated.

Where the big-O lives — the header comment. Any function that does real work gets a header comment above its def: one line saying what it is for (this matters most for public APIs), plus the O(...) for compute and memory. The big-O lives in the header, never on a line inside the body. Tiny obvious helpers are exempt.

# Group orders by customer for one-pass lookup.
# compute O(n), memory O(n) — one dict entry per order.
def index_orders(orders: Sequence[Order]) -> dict[CustomerId, list[Order]]:
    ...

Comment the code, not the work plan. Comments are for the person reading and reviewing: the public API, anything not obvious, and the reason behind the code as a plain sentence (no WHY: label). Keep the work plan out — no phases, plan steps, ticket numbers, or words like "for now", "temporary", or "later". Those go stale and someone has to find and delete them. Unfinished work belongs in plan.md, not in a comment.

Editing counts too. When you change a function, keep the comments already there, fix any your change made wrong, and add the missing header, big-O, or reason. Do this only for code you add or change, not for nearby code you did not touch.

Three Refactor Shapes (north star)

When code review flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:

PatternTriggerFix
1 — Phase ExtractionFunction exceeds 30-40 linesExtract validation / side-effect / verification phases into self-naming helpers; the entry fn reads as a 6-line narrative
2 — Params Dataclass + NewtypesSame 4+ args in 3+ signatures (rule of three — don't preemptively bag)Merge cohesive params into a reused @dataclass (13 args → 3)
3 — Hoist Invariant Work + dict IndexingLoop-invariant work recomputed per iteration, or next(... for ...) scans inside loopsHoist invariants out of the loop; pre-build dict indexes — O(N²) → O(N)

The three patterns cascade: too-many-args produces long functions; long functions produce deep nesting; deep nesting hides quadratic work. Fix the signature first and the rest unravels. Full before/after pairs (with real codebase examples) live in references/decomposition.md.

Comprehensions Over for-append

Single filter + single-expression map → comprehension (also set/dict). Feed generator expressions straight into sinks (sum, any, all, "".join, list.extend).

out: list[str] = []                  # BAD — accumulator mechanics, three control statements
for item in raw:
    if isinstance(item, str):
        out.append(item)

out: list[str] = [item for item in raw if isinstance(item, str)]   # GOOD — same runtime

Use a for loop when: the body is multi-statement; it needs try/except; it exists for side effects (file writes, API calls); or it would nest two deep (extract the inner loop to a named function). A comprehension is an expression of result; for is a statement of process.

Pattern Matching First

match/case over isinstance chains, over if/elif trees branching on a discriminant, over dict-dispatch by type. Destructuring is built in, guards stay typed, and — unlike isinstance chains — exhaustiveness is mechanically checkable.

Exhaustiveness via assert_never

Add case _ as unreachable: assert_never(unreachable) to EVERY match over a union type — describe() under Enums and ADTs below is the canonical form. Two guarantees, layered:

  1. Compile-time. pyright --strict + reportMatchNotExhaustive: add Cancelled to PaymentState without updating a match site and CI fails — drift never reaches production.
  2. Runtime. An out-of-band value (malformed deserialization, an object built outside the type system) raises immediately instead of silently falling through to None — the worst failure mode.

Dead Code After Exhaustive Match

A return/raise after a match covering all union members is unreachable (mypy warn_unreachable flags it). Fold the failure path into the match:

match provider.type:
    case LlmProviderType.OLLAMA:
        return ok(OllamaClient(provider.endpoint))
    case LlmProviderType.OPENAI | LlmProviderType.ANTHROPIC:
        return ok(OpenAiClient(provider.endpoint, provider.api_key))
    case _ as unreachable:
        assert_never(unreachable)  # not `return err(...)` after the match — dead code

Open Types

For matches over open types (str, object, dict) where not every value is meaningful, add case _: pass to make "skip unknown" explicit — reportMatchNotExhaustive flags the missing wildcard:

match key:
    case "orchestration":
        if isinstance(val, str):
            clean[key] = val
    case _:
        pass  # skip unknown settings keys — explicit, not accidental

Result[T] Pattern (No Exceptions)

Exceptions only at system boundaries (try_call, async_try_call). Internal code returns Result[T]. When creating result.py in a project, you MUST read references/result.md in this skill's directory for the canonical implementation — do not recreate from memory.

Key API: ok(v), err(code, msg), try_call(fn), async_try_call(fn), .map(), .and_then(), .unwrap_or(), .is_ok().

Pre-declare Variables Used After match

Python lets variables bound inside case branches leak into the enclosing scope. Don't rely on this — it's fragile and invisible. If a variable will be used after a match, declare it with a type annotation before the match. Scope becomes explicit, naked pass disappears, and reportPossiblyUnbound catches missing assignments at CI time — for ALL match patterns.

# GOOD: ADT extraction — pre-declare, assign in each branch
target_name: str
match strategy:
    case Single():
        target_name = agent.name
    case Supervisor(children):
        target_name = children[0].name
    case _ as unreachable:
        assert_never(unreachable)
log.info("routing", target=target_name)  # clearly in scope

# GOOD: Result[T] guard — pre-declare, assign in Ok, return in Err
user: User
match fetch_user(user_id):
    case Ok(u):
        user = u
    case Err() as e:
        return e
process(user)  # clearly in scope, type declared above

# GOOD: tuple destructuring from Result
conversation: Conversation
agent: Agent
match await service.resolve(account_id, conversation_id):
    case Ok((conv, ag)):
        conversation, agent = conv, ag
    case Err(failure):
        yield error_event(failure)
        return

# BAD: match-case-pass — variable bound in pattern, naked pass
match fetch_user(user_id):
    case Ok(user):
        pass                    # ← user bound HERE, used BELOW
    case Err() as e:
        return e                # ← remove this return → UnboundLocalError
do_something(user)              # ← fragile

.unwrap()/.expect() is acceptable only in: tests; right after an .is_ok() check you just did; process startup where crash = correct behavior.

None and JSON at Boundaries

Return empty collections over None. Convert None to Result at lookup boundaries — if user is None: return err("NOT_FOUND", ...) — so callers never receive T | None from a fallible lookup.

resp.json() returns Any — in pyright strict every downstream access becomes Unknown. Funnel each external response shape through ONE typed parse function (_parse_models_response(body: bytes) -> Result[list[str]]): isinstance narrowing inside (not cast), parse failure is Err not an exception, and the Any never escapes. More than 3 # type: ignore # JSON boundary lines in a file → extract the parse function.

Protocol (Structural Typing)

Python's equivalent of Rust traits — structural matching verified statically by pyright, no inheritance, no decorator. Narrow protocols for narrow needs (interface segregation).

from typing import Protocol

class UserRepo(Protocol):
    async def find(self, id: int) -> Result[User]: ...
    async def save(self, user: User) -> Result[None]: ...

class PgUserRepo:  # satisfies UserRepo structurally — no `implements`, no base class
    async def find(self, id: int) -> Result[User]: ...
    async def save(self, user: User) -> Result[None]: ...

No @runtime_checkable by default: it makes isinstance(obj, UserRepo) work, but verifies method names only, not signatures — a find taking completely different arguments still passes. Add it only when something genuinely requires runtime isinstance (e.g., plugin feature detection); for DI and typed interfaces, leave protocols purely structural.

When you do inherit (framework base classes — the exception, not the rule), mark every overriding method with @override (typing.override). The shipped config sets reportImplicitOverride = "error", so a silent shadow after a parent-class refactor is a CI failure, not a latent bug.

Make Invalid States Unrepresentable

Wrap primitives in newtypes whose constructors guarantee the invariant. Two construction paths: .create() returns Result[T] — the validating path at every untrusted boundary (API body, config, DB row, user input); __post_init__ is a defensive guard raising AppException when someone bypasses the factory — never assert there: python -O strips it and the invariant silently disappears.

@dataclass(frozen=True, slots=True)
class PositiveInt:
    value: int

    def __post_init__(self) -> None:
        if self.value <= 0:  # defensive guard — survives `python -O`, unlike assert
            raise AppException("VALIDATION", f"must be positive: {self.value}")

    @staticmethod
    def create(value: int) -> Result[PositiveInt]:
        """Validating factory. Use this at every boundary."""
        if value <= 0:
            return err("VALIDATION", f"must be positive: {value}")
        return Ok(PositiveInt(value=value))


# Boundary handler: .create() + pre-declare-then-match (see Result[T] section);
# EmailAddress is the same newtype shape over str.
def send_invoice_handler(to_raw: str, amount_raw: int) -> Result[None]:
    to: EmailAddress
    match EmailAddress.create(to_raw):
        case Ok(addr):
            to = addr
        case Err() as e:
            return e
    amount: PositiveInt
    match PositiveInt.create(amount_raw):
        case Ok(amt):
            amount = amt
        case Err() as e:
            return e
    return send_invoice(to, amount)

# Internal code receives validated types — the type system holds the invariant:
#     def send_invoice(to: EmailAddress, amount: PositiveInt) -> Result[None]

Full AppException implementation and the raise-vs-Result rationale: references/app_exception.md.

Where to Assert

The Python rendering of CLAUDE.md's "cheap asserts for invariants":

JobTool
Runtime validation (user input, API payloads, config)Result / AppException — never assert
Exhaustiveness at a match wildcardassert_never (a real function — survives -O, S101-clean)
__post_init__ defensive guardraise AppException
Test assertionsassert freely
Cheap debug-only invariants in srcassert — S101 is globally ignored for exactly this (see references/tooling.md)

Enums and Algebraic Data Types

StrEnum for flat string-valued tags — serializes cleanly, compares with ==, JSON round-trips by default. ADTs (frozen dataclass per variant + type alias union) for variants carrying different data — consumed via exhaustive match + assert_never, serialized with a "kind" discriminator.

@dataclass(frozen=True, slots=True)
class Pending:
    created_at: datetime

@dataclass(frozen=True, slots=True)
class Shipped:
    tracking: str
    shipped_at: datetime

type PaymentState = Pending | Shipped

def describe(state: PaymentState) -> str:
    match state:
        case Pending(created_at):
            return f"pending since {created_at}"
        case Shipped(tracking, shipped_at):
            return f"shipped {tracking} at {shipped_at}"
        case _ as unreachable:
            assert_never(unreachable)  # exhaustive: pyright + runtime guard

The payoff: add Cancelled to PaymentState and every forgetful match site lights up in pyright. A StrEnum + if/elif chain drifts silently.

Data Modeling

Pydantic at boundaries (external data). Dataclasses internally (already validated).

# Boundary form of the PositiveInt invariant — internal code uses the dataclass newtype with .create().
PositiveInt = Annotated[int, Field(gt=0)]

class UserInput(BaseModel):
    model_config = {"frozen": True}
    id: PositiveInt
    email: str

# Internal — already validated
@dataclass(frozen=True, slots=True)
class User:
    id: int
    email: str

Frozen models + derived fields: derived state is structural, not stored. A derived field on a frozen sub-model lives on the wrong class — never fill it by mutating in model_post_init / an after-validator via object.__setattr__; compute it where it's used, as a method on the model that owns the inputs (usually the root). Derived/computed fields, validator modes, or frozen-bypass temptation → read references/frozen_models.md.

Template Strings (PEP 750)

t"..." returns a Template, not a str — the consuming framework sanitizes each interpolation. f-strings for display; t-strings wherever interpolation must be sanitized (SQL, HTML, log-injection boundaries): query: Template = t"select * from users where id = {id}".

Concurrency & Runtime

Principles (full details in the references below):

  • asyncio.wait_for(coro, timeout=...) on every external call — no hanging
  • Bounded queues only (asyncio.Queue(maxsize=n) — unbounded leaks memory); asyncio.Semaphore(n) caps concurrent operations
  • Structured concurrency via asyncio.TaskGroup — exceptions propagate, cleanup is guaranteed
  • No blocking calls in async context (time.sleep, blocking I/O) — freezes the event loop
  • Event loop: uvicorn[standard] auto-selects uvloop where wheels exist — never depend on uvloop directly or call uvloop.install() (deprecated since 3.12; no cp314 wheels as of mid-2026). asyncio's default loop is the supported path otherwise.
  • Graceful shutdown: catch SIGTERM/SIGINT, finish in-flight requests, then exit
  • Retries have bounded attempts, exponential backoff, and only for idempotent or retriable failures
  • Primitive choice: asyncio for I/O-bound, InterpreterPoolExecutor for CPU-bound pure Python (PEP 734), ProcessPoolExecutor only for C-extension heavy work

Load references/concurrency.md when writing async/await, using TaskGroup/Semaphore, choosing an executor, or debugging a hang. Load references/runtime.md when configuring uvicorn, implementing graceful shutdown, writing retry-with-backoff, setting timeouts, or reasoning about request-path resilience (circuit breakers, load shedding, idempotency).

Project Structure (src layout)

project-name/
├── pyproject.toml
├── uv.lock
├── src/project_name/
│   ├── __init__.py
│   ├── py.typed            # PEP-561 marker
│   ├── main.py
│   ├── core/               # Result, domain models, protocols
│   ├── services/           # Business logic
│   └── infra/              # DB, HTTP, external
└── tests/                  # conftest.py, unit/, integration/

The full pyproject.toml — every lint rule explained, every strict pyright flag wired — lives in references/tooling.md.

Package Management (uv)

uv init project-name && cd project-name
uv python pin 3.14
# Before adding: check if already transitive — uv tree | grep <package>
uv add fastapi uvicorn[standard] httpx orjson asyncpg structlog
uv add --dev pytest pytest-asyncio hypothesis testcontainers ruff basedpyright mypy pre-commit
uv sync && uv run pytest && uv run ruff check . && uv run basedpyright

uv replaces pip + pip-tools + virtualenv + pyenv + poetry + pipenv with one Rust binary; deterministic lockfile; install never executes arbitrary Python. A conda-only package → conda for the scientific stack, uv for the service layer; don't mix.

Single-file Scripts (PEP 723)

A 50-200 line ETL or data script gets inline metadata, not a project scaffold — reserve src layout for packages and services:

# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx", "polars"]
# ///

uv add --script etl.py httpx manages the block; uv run etl.py resolves and runs it in an ephemeral environment — no pyproject, no venv ceremony. Terminate Result-style programs at the top level: match the final Result — Ok → sys.exit(0); Err(f) → log f.code and sys.exit(1).

Tooling Enforcement

The discipline is mostly enforceable via config — don't rely on review discipline alone. references/tooling.md is the canonical config: full pyproject.toml, complete select list with per-rule justification, targeted ignores, per-file overrides, pyright flag-by-flag, pre-commit and CI YAML. Load it when setting up a project, auditing a pyproject.toml, or resolving a lint-rule conflict. Lint config asset: copy assets/ruff.toml to the project root (standalone config), or merge its tables into pyproject.toml under [tool.ruff.*] — it is generated from references/tooling.md, which is canonical when they disagree.

ToolEnforces
uvDeterministic lockfile, no setup.py arbitrary code at install
ruffFunction shape (PLR0915 statements, PLR0913 args, PLR0912 branching, PLR1702 nesting), complexity (C901), mandatory annotations (ANN001/201/204), security (S family — S101 globally ignored, house assert rule), modern syntax (UP), plus SIM/PERF/TRY/RUF
basedpyright (strict, primary)End-to-end static types on pyright's engine, pip-installable (no Node). reportMatchNotExhaustive = "error" turns missing match cases into CI failures; reportAny = "error" turns Any leaks into CI failures. Prose references to "pyright" behavior elsewhere in this skill mean this checker.
mypy (strict, secondary)Belt-and-suspenders second opinion in CI.
pytest + pytest-asyncio + hypothesisasyncio_mode = "auto"; --strict-markers --strict-config turn typos into failures; property tests for pure functions
coverage (branch)fail_under = 80; excludes assert_never and if TYPE_CHECKING:
pre-commitRuns ruff + pyright before each commit

Any discipline is machine-enforced: basedpyright's reportAny = "error" flags every expression that resolves to Any. The narrow, justified uses (JSON parse funnels, kwargs forwarding) carry # pyright: ignore[reportAny] # reason — localized by the funnel pattern, so a file collecting more than 3 is telling you to extract a typed boundary. Ruff's ANN401 stays ignored: redundant once expressions are checked. The highest-leverage settings (mirroring references/tooling.md — the numbers live there, don't fork them):

[tool.basedpyright]
typeCheckingMode = "strict"
reportMatchNotExhaustive = "error"   # non-exhaustive match → CI failure
reportAny = "error"                  # Any leak → CI failure

[tool.ruff.lint.pylint]
max-args = 5          # "3-4 params" with slack for constructors
max-statements = 40   # matches "40 lines per function"

Ecosystem Libraries

Each topic has its own reference with full patterns, Result[T] boundary conversions, and anti-patterns. Load only what the current task needs.

TopicReferenceLoad when
HTTP clientreferences/httpx.mdWriting httpx code, pooling + timeouts, wrapping HTTP into Result[T], debugging pool exhaustion, MockTransport testing
Web framework + DIreferences/fastapi.mdFastAPI routes, Depends wiring, Result[T] → HTTP responses, Protocol-based DI, lifespan hooks
Database (PostgreSQL)references/asyncpg.mdasyncpg queries, transactions, pool config, Result[T] wrapping, parameterized SQL, bulk inserts
Structured loggingreferences/structlog.mdConfiguring structlog, trace-ID via contextvars, @traced decorator, guarding expensive debug args
Performance hot pathsreferences/performance.mdOptimizing after profiling, container choice (list/array/bytearray/numpy), orjson default= discipline, profiling workflow
Testingreferences/testing.mdpytest tests, testcontainers fixtures, hypothesis property tests, fake-over-mock, async testing
Jupyter notebooksreferences/notebooks.mdWriting or editing .ipynb teaching notebooks, cell vs sibling-module decisions, exercise cells, restart-and-run-all and output hygiene before commit
Pydantic frozen modelsreferences/frozen_models.mdDerived/computed fields on frozen models, validator modes, BaseSettings derivation, tempted by object.__setattr__

Sibling skills own adjacent territory — route, don't improvise:

  • numpy / polars / pandas / pyarrow / jaxtyping / parquet / tensor shapes → python-numerics-skill
  • torch / nn.Module / training loops / architectures → pytorch-skill (complements this skill — both load)
  • SQL style inside inline strings (lowercase keywords, snake_case) → sql-skill

Observability (logs + metrics + traces, zero-overhead)

Three pillars joined by one correlation id: every log line, metric window, and trace of the same request must pivot to each other through that id.

  • Logs: structlog with the correlation id bound via contextvars (references/structlog.md) — no handler logs before binding.
  • Metrics: prometheus_client counters/histograms at seams (request boundaries, external calls, error arms), exposed on its own listener port. Bounded label values only (channel, model, status) — per-user/per-request ids never become labels; identity lives in traces and logs.
  • LLM/agent traces: Langfuse SDK (@observe or explicit spans) around model calls, STT/TTS, tool runs. Adopt the service correlation id as the trace id by zero-padding 64→128 bit with a shared pad function — never create_trace_id(seed), which hashes and splits cross-service traces. Payload capture (prompts/completions) sits behind a config flag, default off.
  • Zero-overhead posture: exporters batch in background threads; unset env ⇒ a no-op stub (zero network, zero branching at call sites); observability misconfig warns and is skipped, never blocks startup or a request.
  • A feature is done when its seams carry spans + metrics — instrumented on landing, not retrofitted.

Before Committing (Self-Review)

Before committing or completing a review, load references/checklist.md for the full checklist. Quick top-of-mind checks:

  • Every variable has a type annotation; basedpyright (strict) passes; reportMatchNotExhaustive = "error"; reportAny = "error"
  • Guard clauses (match/early return) over .unwrap() — unwrap only in tests
  • Result[T] for all fallible ops; AppException only as narrow escape hatch (see references/app_exception.md)
  • Every match over a union has case _ as unreachable: assert_never(unreachable)
  • Non-trivial functions carry big-O comments (compute AND memory)
  • Timeouts on ALL external calls; bounded queues; asyncio.wait_for(...) wraps every coroutine
  • Tests verify state/behavior, not interactions; testcontainers for infra, fakes for Protocols
  • orjson for JSON (with narrow typed default=), slots=True on dataclasses
  • # type: ignore hygiene — every ignore needs a # reason comment. More than 3 in one file → extract a typed boundary function.
  • Re-run the CLI checkers yourself before presenting work — uv run basedpyright src tests, uv run mypy <file>, uv run ruff check src tests, uv run ruff format --check src tests, uv run pytest -q; paste the tail output, don't trust a subagent's or IDE's "clean" claim. Pay attention to reportPossiblyUnbound around Result[T] unwraps: pre-declared-then-match is correct only when every non-assigning case branch returns/raises. If CLI says clean but IDE flags "possibly unbound", that's an IDE false-positive — note it explicitly and keep the canonical pattern, don't refactor around it.
  • Trust but verify the formatter. After ruff format, run python -m py_compile on the touched files — formatters have shipped syntax-breaking regressions. If py_compile fails post-format, keep the valid pre-format state and file an upstream issue.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.