Astral — the uv, Ruff, and ty people — announced an experimental uv↔ty integration this week. The headline is a unified uv check command: uv prepares your project's environment, installs the checker, and runs ty against code that actually matches your declared dependencies. The quieter half is PEP 723 support: standalone scripts that declare their own Python version and dependencies inline can now be type-checked against those declarations, with uv preparing a separate environment per script.
That second half is the interesting claim. Anyone who has run a type checker over a folder of one-off scripts knows the failure mode: the checker uses whatever environment you happened to invoke it from, so half your diagnostics are really just "wrong venv." I built a small project with four deliberate errors and ran every path Astral describes. Here's what actually happens. (Versions: uv 0.13.5, ty 0.0.91, both newer than the blog's minimums of uv 0.12.3 / ty 0.0.76.)
The test project
uvtest/
pyproject.toml # requires-python >=3.12, no dependencies
src/uvtest/__init__.py # BROKEN: int = "not an int"
scripts/
script_py39.py # PEP 723: requires-python >=3.9, uses datetime.UTC (3.11+)
script_py311.py # PEP 723: requires-python >=3.11, same UTC code
script_dep.py # PEP 723: depends on requests; resp: int = requests.get(...)
Four seeded faults, each aimed at a different claim: one ordinary project type error, one script whose code is invalid for its own declared Python version, its identical twin declared for a newer Python, and one script whose real type error is only visible if requests is actually installed somewhere the checker can see.
Baseline: plain ty check is better than I expected — and still ambient
Plain ty, pointed at the whole directory, took 0.09s and found 3 diagnostics:
error[unresolved-import]: requests (script_dep.py)
error[unresolved-attribute]: datetime has no member UTC (script_py39.py)
error[invalid-assignment]: Literal["not an int"] is not assignable to int
Two surprises here. First, modern ty already reads PEP 723 metadata for the Python version: the UTC error came with a note saying "Python 3.9 was assumed when resolving imports because it was specified in script metadata," pointing at the requires-python line in the comment block. The identical code in script_py311.py passed. So per-script version targeting isn't the new part.
Second, the new part is visible in the failure. For script_dep.py, ty reported unresolved-import — and its search-path info listed the site-packages of the venv I happened to run ty from (the one containing uv and ty themselves), not an environment containing the script's declared requests. The checker didn't fail to understand the script; it checked it against an environment that has nothing to do with the script. That's the ambient-environment bug, and it's the thing the integration exists to kill.
With the integration: the diagnostic changes, not just the plumbing
Opting in from the ty side is an environment variable:
TY_UV=scripts ty check scripts/script_dep.py
Cold run: 3.17s — uv prepared an environment for the script, installed requests, and ty re-checked. The diagnostic flipped from the uninformative one to the real one:
error[invalid-assignment]: Object of type `Response` is not assignable to `int`
--> scripts/script_dep.py:7:13
7 | resp: int = requests.get("https://example.com")
Same file, same checker, same seeded bug — the only difference is that the environment now matches the script's own declarations, so the checker can see far enough to find the actual error. Warm runs of the same command took 0.39s. The version-split scripts behaved exactly as declared under the integration too: py39 still errors on datetime.UTC, py311 passes clean.
uv check: cold 5.5s, warm 0.71s, and it keeps receipts
The unified command, first run on the project:
$ uv check
warning: `uv check` is experimental and may change without warning.
Pass `--preview-features check-command` to disable this warning.
Using CPython 3.15.1
Creating virtual environment at: .venv
Building uvtest @ file:///.../uvtest
Installed 1 package in 1ms
Downloading ty v0.0.91 (13.5MiB)
Downloaded ty v0.0.91
error[invalid-assignment]: ... (the project error)
Found 1 diagnostic
real 5.53s
Three observations. It creates and syncs the project venv itself (here with a uv-managed CPython 3.15.1), downloads ty separately (13.5 MiB) rather than using the ty I already had installed, and — importantly — it reported only the project error. The scripts folder is not swept into a plain uv check; scripts are checked explicitly, as the blog says: uv check --script scripts/script_dep.py reproduced the Response error, --script on py39 gave the UTC error, and on py311, "All checks passed!". Exit codes are sane: 1 with diagnostics, 0 clean.
Warm uv check took 0.71s, versus 0.09s for bare ty on the same code. You're paying ~0.6s for environment assurance on every run. For a pre-commit hook that's a real cost to weigh; for CI, where the ambient environment is whatever the image builder had for breakfast, it's cheap insurance.
The lockfile claim checks out
The blog says uv check uses the ty version recorded in your project's lockfile, so a team shares one checker version without separate pins. I tested it by pinning an older ty as a dev dependency (uv add --dev "ty==0.0.80", lockfile updated) and re-running verbosely:
$ uv check -v
DEBUG Requirement already installed: ty==0.0.80
It used 0.0.80 from the project environment instead of fetching the latest 0.0.91. Claim verified — with the small print that the pin has to exist as a project (dev) dependency for the lockfile to record it; there's no separate "checker version" knob that I found.
What uv check does not do yet
I added two unused imports (import os, import sys) to the project module and re-ran. Diagnostics: still exactly 1 — the type error. No lint findings of any kind. That's consistent with the blog ("starting with support for ty… over time we plan to support Ruff"), but worth stating plainly because "unified checking command" invites the assumption: today, uv check is a type-check command. Your Ruff invocation stays separate.
Two more pieces of small print, one from the blog and one from the terminal. The blog warns that preparing a script environment can execute third-party Python code during dependency installation, which is why editor integration and TY_UV are opt-in; on the CLI, uv check enables it automatically — know that running it on an unfamiliar repo is not a read-only operation. And every CLI run prints the experimental warning unless you pass --preview-features check-command; budget one line of noise per invocation in your logs until it stabilizes.
My take
The honest summary is that the flashy half — one command instead of two — is the less important half. A 0.6s warm overhead and a 13.5 MiB download buy you that, fine. The real change is the baseline experiment above: the same checker, on the same file, returning unresolved-import or the actual type error depending on which venv happened to be ambient. Scripts are where this bites hardest, because a PEP 723 script's whole premise is "my environment is declared in this file, not in your shell." Checking it against your shell was always a category error; it just took an integrated toolchain to make the correct behavior the default path. Experimental warning and all, this is the first time I've seen a type checker treat that premise as true.
Sources: Astral, "Bringing uv and ty together" (astral.sh, Oct 20, 2026). All commands, timings, and diagnostics are my own runs with uv 0.13.5 / ty 0.0.91 on a 2 vCPU Linux VM; timings are single cold runs and short warm repeats, and cold times include package downloads over this VM's network.
Top comments (0)