Guidance for AI coding assistants working on this project.
CLAUDE.md loads this file via the @AGENTS.md import.
- Package:
kaparoo/ - Python: 3.13+
- Kind: distributable library (
uv_buildbackend)
The Astral toolchain. Keep it unless there is a clear reason to change:
uv: environment, locking, runningruff: linting + formattingty: type checkingpytest: testingpytest-cov: coverage measurement and threshold gate
uv sync --group dev # create/refresh the environment
uv run ruff check . # lint
uv run ruff format . # format
uv run ty check # type-check
uv run pytest # run tests (coverage included by default)
uv run pytest --no-cov # skip coverage for quick iteration
uv run pytest -m "not slow" # skip tests marked slowCoverage is measured by pytest-cov against kaparoo/ with
branch tracking; the fail_under gate lives in pyproject.toml
(see tests/AGENTS.md).
Rules for one kind of task live in their own file. Read the guide before you start that task:
| Before you ... | Read |
|---|---|
| write or change a test | tests/AGENTS.md |
| commit (only when the user asks) | .agents/commits.md |
| add, remove, or upgrade a dependency | .agents/dependencies.md |
| change the supported Python versions | .agents/dependencies.md |
edit CHANGELOG.md |
.agents/changelog.md |
edit README.md or a sub-README |
.agents/readme.md |
add or remove a TODO.md item |
.agents/todo.md |
| record a design decision | .agents/decisions.md |
| cut a release (only when the user asks) | .agents/releases.md |
tests/AGENTS.md also loads on its own whenever you work in tests/.
A change is done when the gates pass. Before reporting it finished, run them in this order and fix what they find:
uv run ruff format .
uv run ruff check --fix .
uv run ty check
uv run pytestIf one cannot run here (a test needing hardware or a service this machine lacks), say which and why instead of skipping it silently. CI runs the same gates, but it should confirm a change, not discover that it is broken.
CI also builds the sdist and wheel, checks their metadata, and imports
the package from the wheel alone. When a change touches packaging
(pyproject.toml, the package layout, or files the package ships), run
uv build and uvx twine check dist/* locally too.
- Commit only when the user asks, then follow
.agents/commits.md. - Push only when the user asks; a release the user asked for includes its push and tag. Never force-push, rewrite pushed history, or move or delete a tag.
- Work on the current branch, usually
main. Create a branch only when the user asks for one. - Never skip git hooks (
--no-verify).
- No token, password, or private key in code, docs, commit messages, or logs, nor a test or example value that is real somewhere.
- Code reads a secret from an environment variable.
.envholds the local values and is never committed (.gitignorecovers it); the README's 📦 Installation names each variable the project needs, never its value. - If a secret has been committed, stop and tell the user before changing anything: removing it from history does not take it back. The credential has to be revoked and replaced.
Run uv run ruff format . and uv run ruff check --fix . rather than
styling by hand. ruff enforces the import groups and order, the blank
lines around imports and definitions, the 88-column limit (E501),
pathlib over os.path (PTH), exception messages built in a msg
variable (EM), and %-style logging arguments (G). The rules
below are the ones it cannot check. All Python code covers tests
too; Source code does not. tests/AGENTS.md says
what tests do differently.
A module runs, top to bottom:
from __future__ import annotations, except in an__init__.py.__all__as a tuple, where the module has one; in an__init__.pyit comes before the re-exports.- Imports, then an optional
if TYPE_CHECKING:block of type-only imports. - Constants, type aliases, functions, and classes.
- An
if __name__ == "__main__":guard, in a runnable script only.
-
One blank line between
__all__and the imports;ruffsets every other blank line between the parts above. -
Import by absolute path. A relative import is for the rare case an absolute one cannot work, with
# noqa: TID252and the reason. -
Define a private helper above its first caller, so the module reads top-down; private and public definitions are not kept apart.
-
ruff formatwraps code past 88 columns; shorten or rewrap a docstring, comment, or string yourself. -
A call that does not fit on one line takes one argument per line, with a trailing comma so the formatter keeps that shape. Assign a setup call to a local rather than nesting it in another call.
-
An import or
__all__that fits on one line stays on one line, with no trailing comma to hold it open. -
A comprehension takes one
for; write a loop for anything more. -
In a function body, a blank line separates logical groups; a body of one or two lines has none.
-
Use builtin generics (
list,dict),X | None, and PEP 695 syntax (type Alias = ...,def f[T](...),class C[T]:), neverTypeVar,TypeAlias,Optional, orUnion. -
Fix a
ruffortyfinding rather than suppressing it. A justified suppression names the code and gives the reason in a comment:# noqa: <code>, or# ty: ignore[<error-name>]withty's own error names, never a bare one. -
In a long module, group related definitions under a boxed banner: a centred title-case title, 30 columns wide, two blank lines on each side:
# ========================== # # Reading # # ========================== #
-
Docstrings, comments, and messages use no em dash and no
--. -
In docstrings and comments, single backticks mark identifiers, values, and expressions alike (
path,"merge",(H, W)); double backticks only when the content holds a backtick. -
A standalone runnable script carries PEP 723 inline metadata. Manage it with
uv add --script; edit it by hand only when asked.
Modules and classes:
- A module takes no docstring unless the user asks for one.
- A module-level constant is
UPPER_SNAKEand annotatedFinal. - A class body opens with its docstring, then
__slots__and class attributes, then__init__(or__post_init__) and__repr__;__enter__and__exit__close it. Order the methods between for readability and consistency: related methods together, sibling classes in the same order. - Mark every override with
@override, an implementedabstractmethodincluded; a self-evident dunder (__len__) needs none. - An
@abstractmethodtakes a docstring and a bareraise NotImplementedError; aProtocolmember keeps.... - A value type is
@dataclass(frozen=True, slots=True); a plain@dataclassis for mutable configuration. - A closed set of options is a
Literalalias, not anEnum. - Keep code fully typed (
tyruns witherror-on-warning), with standard typing only:tyhas no plugins, so use PEP 681dataclass_transformor.pyistubs.
Errors and messages:
- Raise built-in exceptions. Add a custom one only when callers need to
catch it apart; name it
*Errorand subclass one built-in. - Write the
msgon one line. When it would pass 88 columns, shorten the prose, hoist a value to a local, or use{value=}; never split the string. - A message starts lower-case and ends without a period, unless it
begins with an identifier. Show a value with
!r, not hand-written quotes, and put a path at the end. - Name the valid set or the fix, not just the failure ("unsupported node type 'X': expected File, Directory, ...").
Docstrings and comments:
- Docstrings are optional. Write one where it clarifies intent and
contracts, not mechanism: never restate the code, and never explain
a base class through its subclasses (a closed hierarchy's base may
name them as a family map). A public method callers consume still
gets one.
- The summary is one line ending in a period: a noun phrase for a
class ("An ordered, read-only view over ..."), a property ("The
reporting unit ..."), and a boolean method ("Whether
pathexists."); an imperative verb phrase for every other function and method ("Yield successive windows fromitems."). - A consumed public method is self-explainable from its own docstring and signature, never an inherited one. An abstract method documents the generic contract and names no subclass.
- Say what the signature cannot: invariants, edge cases, what subclasses must override, trade-offs. Document an edge case a family shares once, on the class.
- Use Google style
sections (
Args:,Returns:,Yields:,Raises:,Type Parameters:), without types inArgs:. Custom sections (Example:) are welcome when they clarify a real pitfall. Add a section only for what the summary and signature do not convey; often the summary is the whole docstring. - Document constructor arguments in the class docstring's
Args:and leave__init__bare. An@overloadstub takes no docstring.
- The summary is one line ending in a period: a noun phrase for a
class ("An ordered, read-only view over ..."), a property ("The
reporting unit ..."), and a boolean method ("Whether
- Comments explain why, not what: a constraint, a reason, a rejected alternative. Keep a comment to one line, three at most; longer belongs in a docstring or a commit message. An inline comment is for a short qualifier, such as a unit.
- Fill docstring and comment prose toward 88 columns: a line was
wrapped early if the next line's first word would still have fit.
Nothing checks this, so rewrap and read the result. Never split a
backticked span or an
*emphasis*across lines.
Whoever changes the code keeps the documents current, in the same
commit as the change. A docs-only fix (wording, a stale example) is
its own 📝 commit.
| When a change ... | Update |
|---|---|
| alters user-visible behavior, requirements, or dependencies | CHANGELOG.md → [Unreleased] |
| adds, removes, or renames a public module | README.md → 🧩 Modules |
adds, removes, or changes a command in [project.scripts] |
README.md → ⌨️ CLI |
| changes how the package is installed | README.md → 📦 Installation |
| changes code that a README example uses | that README → the example |
| moves, renames, or deletes a file a README links to | that README → the link |
| adds, removes, renames, or reorders a sub-README section | that README → ## Contents, if present |
| defers work, or uncovers a problem it does not fix | TODO.md → add an item |
resolves a TODO.md item |
TODO.md → remove the item |
| settles a design choice later work should not reopen | docs/decisions/ → add a record |
| settles a convention later sessions must follow | AGENTS.md → below the marker |
Each document has one audience and one tense:
README.mddescribes the project as it is now: what it does and how to install and use it. No history, no "new in vX.Y".CHANGELOG.mdrecords what changed for a user of the library: external behavior only, never internal refactoring.TODO.mdlists work that is not done yet.docs/decisions/records why the code is shaped as it is: one decision per file, written once.AGENTS.mdand its guides hold rules for whoever works on the code next.
Every document wraps prose at 72 columns; tables, code blocks,
headings, and a line holding a long URL or command are exempt. Write
a new document in English; one already written in another language
keeps that language. No document uses an em dash or -- in its
prose: recast the sentence with a colon, a comma, parentheses, or two
sentences.
- When a project-specific convention, command, or pitfall is settled
(the user states a rule, or a workflow is agreed on), record it
below the
<!-- Add project-specific guidance below. -->marker at the end of this file. - Leave everything above the marker as it is, and the guides in
.agents/andtests/AGENTS.mdtoo:copier updaterefreshes them from the template, so local edits turn into merge conflicts. If a template rule does not fit, add an overriding note below the marker and tell the user.
Generated from a copier template. .copier-answers.yml records the
answers; run copier update --UNSAFE to pull later template changes.
The project supports Python 3.13 and newer (requires-python in
pyproject.toml), one version below the python_version: '3.14'
recorded in .copier-answers.yml, which keeps .python-version at
3.14 for development. Two places override the template by hand, and
copier update flags both as conflicts: the Python: line under
Project above, and "3.13" in the python-version matrix of
.github/workflows/ci.yml. Keep the project's side of both.
The module docstrings in kaparoo/ predate the Source code rule
that a module takes none. They stay for now, pending a review that
TODO.md tracks. A new module still takes no docstring, and an
existing one is not extended.