Every dev tool invents its own cache/state directory and drops it straight in the project root: .pytest_cache/, .mypy_cache/, .ruff_cache/, .tox/, .hypothesis/, htmlcov/, .coverage. Each one needs its own .gitignore line and its own cleanup logic in CI. Some tools already let you relocate their own cache (mypy: cache_dir, ruff: cache-dir, pytest: cache_dir ini option) — but it’s N separate settings for N tools, with no shared default.
Proposal
Two well-known keys, directly under [tool], that any tool MAY read as a fallback before its own hardcoded default:
config-dir is flat — every tool’s config file lands directly inside it (e.g. _configs/mypy.ini, _configs/ruff.toml), one level, no nesting. cache-dir is a shared root that each tool nests its own subfolder under (e.g. .cache/mypy/, .cache/ruff/), since cache contents are tool-specific and shouldn’t collide.
Resolution order for a conforming tool:
Tool’s own CLI flag / env var — existing, highest priority
Tool’s own [tool.<name>].cache_dir if explicitly set — existing
New: shared [tool].cache-dir / [tool].config-dir from pyproject.toml
Tool’s current hardcoded default
Fully opt-in and backwards compatible — nothing changes for a tool until its maintainers add step 3.
Why not just point tools at XDG_CACHE_HOME?
That’s per-user/per-machine, not per-project, has no Windows equivalent, and doesn’t travel with the repo or work cleanly in CI without extra setup. This is project-local, checked in (or gitignored as one line), and consistent across machines.
Benefits
Cleaner root directory — one .cache/ and one _configs/ instead of a dozen scattered dotfolders
One .gitignore line (.cache/) instead of one per tool
Simpler CI cache config — a single path to cache between runs instead of enumerating every tool’s cache dir
These don’t all need .gitignore lines: the directories all contain their own .gitignore file created by the tool, containing *. This means Git already ignores them. So only the .coverage file needs its own entry.
Yeah, I’m not sure this is a great idea: there’s a good argument that Python tooling should in general try to sprawl less in the user’s development tree (even if it all gets ignored anyways), but I suspect that a cache-dir/config-dir setting that’s honored (conventionally) by tools would be a layering violation in terms of how pyproject.toml is currently laid out.
The “good” news is that technically this doesn’t really need a specification: pyproject.toml says that tool is an open-ended table, so you could establish a convention like:
[tool._meta]
cache-dir = "target/{tool}"
…where {tool} would be expanded to the tool’s name or other identifier.
(I picked _meta because it’s not a valid package name.)
However, I suspect most tools won’t want to adopt that for the reasons mentioned above, and it would be hard to force them to.
Alternatively (and only solving the config case), I’ve seen a growing trend of tools supporting .config: https://dot-config.github.io/
So perhaps one way out of your personal config hell is to work with maintainers on joining in the fun (and the more tools that support it, the stronger the case for other tools)
The core idea: declutter the root directory from tool clutter, which only gets worse with more tools.
Right now every tool has its own [tool.<name>] subtable, so there’s no shared place for something like “where should you put your stuff” — either you set it per tool, or nobody bothers and it all defaults to root.
That clutter is bad enough that tools already hardcode each other’s dirs to work around it — black’s exclude list has to name every other tool’s cache dir just to avoid formatting inside them:
Caching or clearing the cache, as and when required.
.gitignore works for git and some other tools, but not for all. Tools like black needs to maintain an exclusion list for each cache dir. This approach will also make tools more compatible.