Skip to content

required-version cannot fire while exclude-newer = "14 days" is in the same [tool.uv] table, so old uv silently drops the cool-off #28

Description

@jlevy

Summary

template/pyproject.toml.jinja pairs two settings that defeat each other:

[tool.uv]
# Fail fast on old uv versions rather than misbehave. 0.9 is the floor where
# exclude-newer accepts a relative duration like "14 days".
required-version = ">=0.9"
exclude-newer = "14 days"

On uv < 0.9 the relative duration fails to deserialize, so uv discards the entire [tool.uv] table — including required-version — emits a warning, and continues with defaults. The guard adopted specifically to catch these versions is in the table that fails, so it never runs.

The consequence is not a loud failure. A generated project on old uv resolves and locks with no cool-off at all, and the only signal is a warning that scrolls past above a successful command.

Reproduction

Minimal project with the template's exact [tool.uv] block, run under uv 0.8.17:

$ uv lock
warning: Failed to parse `pyproject.toml` during settings discovery:
  TOML parse error at line 18, column 17
     |
  18 | exclude-newer = "14 days"
     |                 ^^^^^^^^^
  failed to parse year in date "14 days": failed to parse "14 d" as year ...

Using CPython 3.11.15 interpreter at: /usr/local/bin/python3
Resolved 1 package in 4ms

It resolves. uv sync and uv run behave the same way.

Isolating the cause

Same uv 0.8.17, varying only the [tool.uv] contents:

[tool.uv] contents result
required-version only error: Required uv version >=0.9 does not match the running version 0.8.17
required-version + exclude-newer = "2026-08-01T00:00:00Z" ✅ same clean error
required-version + exclude-newer = "14 days" warning only, uv lock succeeds, no cool-off

So the guard mechanism works. It is specifically the relative duration — the one setting required-version exists to protect — that suppresses it, because required-version can only be read if the table it lives in deserializes.

Which paths are affected

Not everything degrades silently, which is worth knowing when deciding how much to do:

path uv < 0.9 behavior cool-off applied
bare uv sync / lock / run / add (project default) warning, succeeds no
make install etc. (UV_EXCLUDE_NEWER env var) hard error aborts
documented uv add --exclude-newer "14 days" hard error aborts
generated-project CI unaffected — setup-uv pins 0.11.25 with a checksum yes

The Makefile and explicit-flag paths fail loudly. The silently-degrading path is bare uv commands — which is exactly the case [tool.uv] was added to cover. From research-uv-changes.md:

The same [tool.uv] table sets exclude-newer = "14 days" so direct uv commands, not only Makefile and CI paths, inherit the policy.

That intent is right; it just doesn't hold on the versions required-version was meant to exclude.

Suggested fixes

Roughly in order of how well they hold, and all of these are your call on the churn tradeoff:

  1. Absolute exclude-newer date. Verified above: required-version then fires correctly. Costs a periodic bump, though updating.md already defines a cadence and the template bumps pins each cycle anyway.
  2. A uv --version preflight in the generated Makefile, plus documenting the floor. Catches make entry points with an actionable message, but does not protect bare uv commands.
  3. Upstream fix in uv — evaluate required-version before rejecting unrelated keys in the same table, or degrade unparseable settings loudly rather than warning and continuing. This is arguably where it really belongs, since the trap is reachable by any project combining the two settings. Happy to file that separately if useful.

One approach worth ruling out

Moving required-version into a standalone uv.toml looks like it fixes it, and it does fire correctly on old uv — but on modern uv, the presence of uv.toml makes [tool.uv] be ignored entirely:

warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`.
The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file:
- required-version
- exclude-newer
Resolving despite existing lockfile due to removal of global exclude newer

That trades a silent failure on old uv for a silent failure on every version, so it is a step backwards.

Context

Found while debugging this in jlevy/fdu, which uses the same 14-day cool-off in a standalone uv.toml. That layout fails fatally rather than silently, so it presents differently: one stale uv breaks several unrelated-looking targets at once with a TOML date error that reads like a corrupt config. It cost a full investigation to attribute to the tool version. fdu's fix was option 2 — a uv-version preflight guarding the affected make targets, with the floor documented and cross-referenced to the CI pin (jlevy/fdu#21).

Verified against uv 0.8.17 (failing) and 0.12.4 (passing).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions