`cache-dir / config-dir` in `pyproject.toml` so tools stop littering the project root

Problem

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:

[tool]
cache-dir = ".cache"
config-dir = "_configs"

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:

  1. Tool’s own CLI flag / env var — existing, highest priority
  2. Tool’s own [tool.<name>].cache_dir if explicitly set — existing
  3. New: shared [tool].cache-dir / [tool].config-dir from pyproject.toml
  4. 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
  • Easier “clean” / reset — rm -rf .cache covers every adopting tool

Prior art

  • mypy: cache_dir in [tool.mypy]
  • ruff: cache-dir in [tool.ruff]
  • pytest: cache_dir in [tool.pytest.ini_options]
  • Cargo: single target/ every subsystem writes into — the UX this is chasing

Ask

  1. Would tool maintainers (ruff, mypy, pytest, coverage) actually adopt this as a fallback?
  2. Any objection to config-dir being flat while cache-dir is tool-nested, or should both follow the same layout rule?

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.

What do you mean by “cleanup logic in CI”?

1 Like

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.

2 Likes

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)

2 Likes

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:

DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.pytest_cache|\.ruff_cache|\.tox|\.venv|__pypackages__|build|dist|venv)/"

Using [tool] itself as a global config location (config-dir, cache-dir) fixes that gap; instead of multiple sub-table entries.

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.

.config was my 1st approach, but the issue was that it will make the folder hidden.
Since config shouldn’t be hidden, many devs won’t agree to it.

And, even for other alternative names it will be hard to converge at single name.
Hence, this provides a flexibility to choose a name.

Related discussion : Alternative Directory Names · pi0/config-dir · Discussion #3 · GitHub