Beautiful, correct documentation from a Python package, with no boilerplate in the package. Less humdrum, more automation, earlier at the pub.
Full documentation here, generated by epythet.
pip install epythet
epythet quickstart /path/to/project --ignore tests/ scrap/ examples/Open /path/to/project/docsrc/_build/html/index.html. You get:
- a landing page that is your README (badges, images, GitHub alerts and mermaid fences intact),
- a nested API tree built from your package layout, one page per module,
- RST field lists and Google/NumPy sections rendered side by side, types linked from annotations,
- a modern theme with light/dark mode and an accent colour derived from your package name,
- agent-facing twins:
llms.txt, a.mdtwin of every page, a flat<package>.md, andobjects.inv.
Every site also says where it came from. A small line at the bottom of the landing page reads built <UTC time> from <commit> (<branch>) · <package> <version> · about this build, so a reader can tell whether the docs match the repository and the installed package, and a maintainer can see whether the latest push has been published. The about-this-build page behind the link holds the full diagnosis (commit, tags, dirty flag, CI run, tool versions, resolved configuration, latest PyPI release and whether it matches, how to reproduce the build), and build_info.json at the site root holds the same for machines; epythet build-info DIR prints it. [tool.epythet] provenance = false turns it off, "minimal" keeps the line and the JSON without the page, and provenance_template points at your own page template.
Nothing has to be added to the package. Everything is read from pyproject.toml (or setup.cfg), the README and the docstrings.
epythet ships tooling for coding agents. If you are one, start here.
Skills (Agent Skills format), for any agent host. Install one with gh skill:
gh skill install i2mint/epythet epythet-setup --agent claude-code # or copilot, cursor, codex, gemini| Skill | Use it to |
|---|---|
epythet-agentic-readme |
make sure a repository's README documents its agentic aspects |
epythet-ai-artifacts |
find, install and document a repository's AI agent artifacts |
epythet-docstring-style |
write and improve Python docstrings that render correctly in epythet/Sphinx and that help both humans and AI agents |
epythet-pages |
diagnose and fix GitHub Pages publishing for Python documentation built with epythet (or any Sphinx site pushed to a gh-pages branch) |
epythet-repair-migrate |
the per-repository documentation sweep for packages documented with epythet |
epythet-setup |
set up documentation for a Python package with epythet |
epythet-theme |
choose and parametrize the Sphinx theme of an epythet documentation site |
epythet-validate |
check a Python package's docstrings for rendering artifacts and build problems with epythet validate |
The same skills are inside the wheel, under epythet/data/skills/.
Subagents: docs-migrator (runs the epythet documentation sweep on one repository end to end), docs-reviewer (reviews the rendered documentation of a Python package), in epythet/data/agents/. Copy one into your project's .claude/agents/ (or your host's equivalent).
Instruction files: .claude/CLAUDE.md (Claude Code).
The documentation, machine-readable: llms.txt indexes every page; epythet.md is the whole documentation in one file; every page has a .md twin; objects.inv maps symbols to URLs. The full list, with install lines, is on the site's For AI agents page.
If you would rather understand than delegate, the rest of this README is written for you, starting at What it fixes without touching your docstrings.
Docstrings in real packages mix reStructuredText, Google sections and Markdown habits, and a few recurring slips render wrongly, often silently. epythet rewrites those at build time (the normalizer), so the rendered site is right even when the source is not:
| you wrote | what happened before | what epythet renders |
|---|---|---|
a >>> block right after a sentence |
a paragraph starting with >>>; never run by sphinx.ext.doctest |
a doctest block |
```python fences |
the backticks printed literally | a highlighted code block |
Returns: the answer on one line |
a sentence | a Returns section |
## Heading |
a literal ## |
a heading |
*args / **kwargs in prose |
an "emphasis start-string without end-string" error | escaped, as written |
[text](url) |
printed literally | a link |
| a wrapped list item at the bullet's indentation | "bullet list ends without a blank line" | a list item |
Examples: followed by an unindented doctest |
a stray "Examples:" paragraph | an Examples rubric |
Single backticks render as code (default_role = "code"), matching the Markdown habit. Code inside doctests, literal blocks and fences is never touched. The rules are pure functions in epythet.normalizer; you can see what one docstring becomes with epythet.normalize_text(docstring).
On the dol package (48 modules, 23,000 lines of doctests) this took the build from 91 Sphinx warnings and 266 detected rendering artifacts to 25 and 55, with every doctest that autodoc documented still documented.
All optional. Omit the section and you get the defaults below.
[tool.epythet]
display_name = "Dol" # site title; default: the project name
copyright = "2024, Jane Doe" # footer; default: no copyright line at all
theme = "auto" # "auto" | "furo" | "shibuya" | "pydata" | "sphinxawesome" | "book" | "alabaster" | "rtd" | any installed theme
accent = "#3661ac" # default: derived from the package name (OKLCH, WCAG AA on white by construction)
mode = "auto" # "auto" | "light" | "dark" (where the theme supports forcing it)
ignore = ["tests/", "scrap/", "examples/"] # path substrings to skip; `--ignore` on the CLI overrides
api_generator = "auto" # "auto" (autosummary if the package imports, else autoapi) | "autosummary" | "autoapi"
agent_outputs = true # llms.txt, .md twins, <link rel="alternate"> relations
aggregates = ["md"] # flat single-document twins at the site root: "md", "pdf"
ai_artifacts = true # "For AI agents" page when the repo has skills, agents or CLAUDE.md
ai_artifacts_template = "" # project-relative file overriding that page's template
package_dir = "src/dol" # default: found by convention (<name>/ or src/<name>/)
docs_dir = "docsrc" # where the Sphinx sources are generated
[tool.epythet.theme_options] # verbatim passthrough into Sphinx's html_theme_options; always wins
announcement = "v2 is in beta"
[tool.epythet.readme] # pins the generated "For AI agents" README section (see "For agents"); wins over ~/.config/epythet
humor = true
agentic_first = truesetup.cfg projects put the same keys under [metadata] (display_name, copyright) or a [tool.epythet] section. When both files exist, pyproject.toml wins.
Themes. theme = "auto" (the default) hashes the package name into a curated pool (furo, shibuya, pydata-sphinx-theme, sphinxawesome-theme) so a fleet of packages gets variety while every package keeps the same look across rebuilds. The pool's themes are installed with epythet; sphinx-book-theme and sphinx_rtd_theme come with pip install "epythet[themes]". The accent is one hue per package, at a fixed perceptual lightness, so every possible colour clears WCAG AA against white and AAA on a dark background; an explicit accent is used as given in light mode and lifted to the same dark-mode lightness for dark mode.
API generator. autosummary (Sphinx built-in) imports your package, so aliases, functools.partial objects and other assigned names keep the docstring of what they point to. autoapi parses statically and needs no import. The default auto probes the import once and picks autosummary when it succeeds, autoapi otherwise (a missing optional dependency in CI then costs you the aliases, not the whole API section). Both give the nested tree; both run the normalizer; both skip __main__. Under autosummary, ignore keeps the ignored modules out of the tree, but Python still imports them once while discovering the package.
PDF aggregate. aggregates = ["md", "pdf"] renders <package>.pdf from the Markdown aggregate with Playwright (pip install "epythet[pdf]" && playwright install chromium) or WeasyPrint, whichever is installed. No LaTeX.
epythet quickstart (or epythet make-docsrc) writes a docsrc/ directory holding a two-line conf.py:
from epythet.sphinx_conf import * # noqa: F401,F403and an index.md that includes your README and a hidden toctree for the API pages. That is the whole scaffold; the API pages and the agent outputs are generated at build time. You do not need to commit docsrc/ (CI regenerates it), but if you do, the shim is the single source of truth: put project-specific Sphinx overrides below the import and they win over the generated values. A hand-written conf.py without the import is never overwritten.
epythet make PROJECT_DIR [html|doctest|markdown|github|clean] runs sphinx-build with the current interpreter; there is no Makefile. github builds HTML and copies it into PROJECT_DIR/docs. doctest is Sphinx's doctest builder, which runs examples without the module's namespace; for docstring doctests use pytest --doctest-modules.
Every site also serves, next to the HTML:
llms.txt: an index of every page with a one-line description,<page>.html.md: a fully rendered Markdown twin of every page, advertised from each page's<head>with<link rel="alternate" type="text/markdown">,<package>.md: the whole documentation as one Markdown file, linked from the landing page (and<package>.pdfwhen enabled),objects.inv: the Sphinx inventory, a machine-readable symbol-to-URL index (sphobjinv convert plain objects.inv -).
Set agent_outputs = false to skip the second (Markdown) build pass.
The README. epythet ai-readme-check PROJECT_DIR reports which of these a project has (skills, subagents, instruction files, the outputs above) and whether its README mentions each; --format json for machines, --fail-on warn for CI (the exit code is 0 otherwise). --draft prints a "For AI agents" README section rendered from text snippets; --write adds it between marker comments and updates it in place on later runs, wherever you moved it (the section at the top of this README is one). What to do about a missing section is a user-level policy in ~/.config/epythet/config.toml: [readme] agentic_aspects = "warn" (the packaged default) or "add", humor = true to draw the "for humans" line from a pool, agentic_first = true to put the section before every other; a project pins the keys that shape its committed text in [tool.epythet.readme], which wins. The wording is yours too: epythet snippets list | show NAME | init | diff resolve a snippet from ~/.config/epythet/snippets/ over the packaged default; init copies the defaults out once, with a header recording the epythet version, and never overwrites; diff shows how your copy differs from the current default after an upgrade. The epythet-agentic-readme skill walks an agent through the whole thing.
The "For AI agents" page. When the repository ships anything for agents, epythet adds an ai-agents page to the site listing it: skills (<pkg>/data/skills/*/SKILL.md, skills/*/SKILL.md, .claude/skills/*/SKILL.md) with their gh skill install lines and source folders, subagents (<pkg>/data/agents/*.md, .claude/agents/*.md), instruction files (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, .cursor/rules, .codex), and the outputs above with their URLs. Symlinks are followed and duplicates removed. epythet ai-artifacts PROJECT_DIR prints the same inventory (--format json for machines). Turn the page off with ai_artifacts = false (or, for a whole CI fleet, the environment variable EPYTHET_AI_ARTIFACTS=0), or replace its template with ai_artifacts_template = "path/to/template.md" (a str.format template; see epythet.ai_artifacts). A hand-written docsrc/ai-agents.md is left alone. A malformed SKILL.md never fails the build: the skill is listed by folder name.
from epythet import (
quickstart,
make_docsrc,
make,
load_config,
sphinx_settings,
normalize_text,
)
quickstart(
"/path/to/project", ignore=["tests/"]
) # scaffold + build; returns the html dir
cfg = load_config("/path/to/project") # the resolved DocsConfig
sphinx_settings(cfg) # the conf.py namespace as a dictDiagnosis and repair of docstring formatting in source files (missing blank lines before doctests) is unchanged: epythet.diagnose_doctest_code_blocks, epythet.repair_package.
Add a workflow such as .github/workflows/publish-docs.yml to your repo and adjust the trigger. The example below runs after the "Continuous Integration" workflow completes.
name: GitHub Pages
on:
workflow_run:
workflows: ["Continuous Integration"]
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: i2mint/epythet/actions/publish-github-pages@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
ignore: "tests/,scrap/,examples/"
python-version: "3.12"The action installs epythet, installs your project, runs epythet quickstart . --ignore ... and pushes ./docsrc/_build/html/ to the gh-pages branch.
After the CI runs and creates the gh-pages branch, you need to tell GitHub to actually serve it. There are two ways to do this:
Go to your repo's Settings > Pages, set the source branch to gh-pages and the folder to / (root), then click Save.
If you have the gh CLI installed:
# Check if Pages is set up correctly
epythet check-pages owner/repo
# Enable or fix Pages configuration
epythet configure-pages owner/repoOr from Python:
from epythet import check_pages_setup, enable_pages
# Diagnose
check_pages_setup("owner/repo")
# Fix
enable_pages("owner/repo")You can also point these at a local git checkout instead of owner/repo:
epythet check-pages .
epythet configure-pages /path/to/my/projectThese tools work with either the gh CLI (recommended) or a GITHUB_TOKEN
environment variable.
Under the hood, configure-pages is just the GitHub Pages REST API — the direct
gh equivalent of Settings > Pages → Branch gh-pages, folder / (root) →
Save is:
# POST creates the Pages site (when Pages is not yet enabled — GitHub's default);
# use -X PUT instead to change an already-enabled Pages config.
gh api repos/owner/repo/pages -X POST -f 'source[branch]=gh-pages' -f 'source[path]=/'See CI epythet troubleshooting.
epythet 0.2 keeps the contract the fleet depends on and changes what is behind it:
epythet quickstart DIR --ignore ...still writes HTML toDIR/docsrc/_build/html/; the--ignoreflag with no values still means "use the default".make_docsrc,make_autodocs,makeandquickstartare still importable fromepythet(and fromepythet.setup_docsrc/epythet.call_make);make_autodocsis now a no-op alias ofmake_docsrc, since API pages are generated at build time.epythet.config_parser.parse_configkeeps its 5-tuple(name, copyright, author, version, display_name), so a committed 0.1.xdocsrc/conf.pykeeps working. It now resolves the project directory whatever path it is given, sopyproject.tomlwins over a stalesetup.cfg(0.1.x silently preferredsetup.cfg).- A committed 0.1.x
docsrc/(templateconf.py,index.rst,table_of_contents.rst,module_docs/, Makefile) is recognised and replaced by the new scaffold on the nextquickstart. - Dropped: the
sphinx_rtd_themedefault (furo-class themes replace it),sphinx-toggleprompt(copybutton already strips prompts),commonmark, and theMakefile. Requires Python 3.11+, Sphinx 9, myst-parser 5.1; a project that pinssphinx<9ordocutils<0.22in its own dependencies will conflict with epythet 0.2 in the same environment. - URLs of API pages changed (
module_docs/<pkg>/<mod>.htmlis now_autosummary/<pkg>.<mod>.html);objects.invkeeps every symbol resolvable across sites.
The publish action pins epythet<0.2 until v2 is validated across the fleet; see the v2 decision record and the tracking issue.
epythet validate checks a package's documentation in tiers, epythet repair fixes the markup slips it finds in the source, and epythet sweep runs the checks across many packages. Install the extras you need: pip install 'epythet[validate]' (ruff, pydoclint, PyYAML), 'epythet[repair]' (LibCST, optional), 'epythet[migrate]' (docstring-parser), 'epythet[review]' (Playwright screenshots).
epythet validate . # lint + parse every docstring (no build), exit 0/10/11
epythet validate . --level 3 # + Sphinx build + rendered-output checks, exit 12/13
epythet repair . && epythet repair . --write # diff first, then apply
epythet sweep pkg1 pkg2 --manifest my_packages.pth # read-only, many packages--level |
Runs | What it reads | Exit code |
|---|---|---|---|
| 0 | lint | the docstring text: ruff D rules, pydoclint, and the coverage detectors (public objects without a docstring, entry points without an example, summaries that only restate the name, parameter descriptions that only restate the type) |
10 |
| 1 (default) | + parse | the docutils doctree of every docstring, in isolation, against the artifact ledger (a :param line glued to the summary, a doctest rendered as prose, a Markdown fence, *args opening an emphasis, ...) |
11 |
| 2 | + build | the Sphinx warning stream | 12 |
| 3 | + render | the built pages: -b xml for objects described with nothing and cross-references that rendered as plain code, -b html for dangling #idN anchors and missing images, -b text for snapshots |
13 |
| 4 | + review | writes a review packet (rendered text of the changed or sampled pages, the rubric, a strict JSON schema) for an in-session agent; never calls a model and never gates | 14, only with --fail-on-review |
Levels 0 to 3 gate on --fail-on error (default), warning or info; exit 20 means the ledger itself is broken. --format json and --format jsonl give the machine-readable report.
Text snapshots are opt-in: --update-snapshots writes the -b text render of every page under docsrc/_snapshots/text, and --snapshot diffs against it (a changed page is a level-2 error; the snapshot diff is the proof that a migration changed nothing). Level 4 writes its packet under the user data dir (~/.local/share/epythet/review/<package>/<run>/, EPYTHET_DATA_DIR overrides); a reviewer answers with a review.json that --review-reply ingests, and epythet ledger propose review.json turns its proposed rules into status: proposed ledger rules in an overlay (--ledger DIR uses them) for a human to promote.
Every finding names a rule from the ledger (epythet/ledger/rules/, one YAML per rule with a fixture that is also its regression test). Findings are appended, as observations, to ~/.local/share/epythet/ledger/observations.jsonl, never inside the repository.
The docs build already normalizes docstrings on the fly. epythet repair applies the same source-safe rewrites to the files: a blank line before a doctest, list or field list; a Markdown fence to a .. code-block:: (or a :: literal block with --fence-style literal); Returns: text to a real section; ## Heading to a rubric; [text](url) to an RST link; a short title underline padded.
epythet repair path/to/pkg # dry run: a unified diff, and what needs a hand
epythet repair path/to/pkg --write # apply, after verifyingOnly the docstring literals change; the rest of the file is copied byte for byte and the module's AST outside its docstrings must be identical or nothing is written. Doctest sources are never altered. Each rewritten docstring is re-validated at level 1 (a rewrite that would introduce a finding is dropped), and with --write the doctests of every touched file are run before and after, and a file whose failures went up is restored. What no rule can fix safely (a prose *args, unmatched backticks, a backslash in a non-raw docstring) is listed under "needs a hand". repair_package from epythet.tools keeps working and delegates here.
epythet migrate-style path --to google (or numpy) is the opt-in cousin: it rewrites an RST field list (:param x:, :returns:, :raises:) as a Google or NumPy section via docstring_parser, with the same guarantees, and leaves alone any docstring whose fields would not round-trip.
epythet sweep DIR... [--manifest FILE] validates every package at levels 0 and 0.5 without writing into any of them, prints how often each rule fires (findings, packages affected, rate per hundred public objects) and a queue of packages ranked by the work they hold, entry points first. It is what decides the severities in the ledger, and the shortest path to "which package should I document next".
