How Astral Uses Its Own Tools
Astral runs ruff, uv, and ty inside the repos that build them. Their pyproject.toml, .pre-commit-config.yaml, and CI files are public, and a walk through the three shows how the people making ruff, ty, and uv wire their own toolchain into the projects that produce it.
Build ty from source and type-check Python with it
The ruff repository ships ty as part of its crate tree. Three CI steps build ty from source and run it against the repo’s own Python:
- name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
- name: Dogfood ty on the scripts directory
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
- name: Dogfood ty on ty_benchmark
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark“Dogfood” is a literal step name in the workflow file. Each step builds ty from the current commit and runs it on ruff’s own Python: uv run activates the project’s Python environment, and cargo run -p ty check compiles ty and checks that project. CI triggers on every PR and push to main, so a ty regression that breaks the ruff repo shows up on the PR that introduces it.
Tune ruff differently in each repo
All three repos use ruff for their own Python files, but the configurations diverge.
The ruff repo selects many rule categories and enforces isort-style imports:
[tool.ruff]
target-version = "py38"
[tool.ruff.lint]
select = [
"E", "F", "B", "B9", "C4", "SIM", "I", "UP",
"PIE", "PGH", "PYI", "RUF", "S602",
]
ignore = ["B011", "E501"]
[tool.ruff.lint.isort]
required-imports = ["from __future__ import annotations"]
combine-as-imports = trueThe uv repo keeps its ruff config in a four-line ruff.toml:
target-version = "py312"
[lint]
extend-select = ["I", "B"]
[lint.per-file-ignores]
"__init__.py" = ["F403", "F405"]The ty repo scopes ruff around the vendored ruff crate and bumps the target version for its helper scripts:
[tool.ruff]
extend-exclude = ["ruff"]
per-file-target-version = { "scripts/**" = "py313" }Order pre-commit hooks explicitly
The ruff and ty repos use priority: in .pre-commit-config.yaml to control run order. The pattern across both is the same:
- Priority 0: read-only hooks (
check-merge-conflict,typos,validate-pyproject,zizmor,shellcheck) and first-pass formatters (ruff-format,prettier,mdformat). - Priority 1: fixers that edit files (
ruff-check --fix,markdownlint-fix). - Priority 2: second-pass formatters that reformat content the fixers touched (a second
ruff-formatpass over the Markdown-embedded mdtest snippets).
The ordering matters because ruff-check --fix can rewrite imports or collapse comprehensions, which then need reformatting. Running the formatter before the fixer reduces what the fixer has to touch, and running it again after catches whatever the fixer reshuffled.
For the full setup pattern, see how to set up pre-commit hooks for a Python project.
Invalidate the uv cache when Rust changes
The ty repo is a Python package whose build depends on Rust source. Its pyproject.toml tells uv to treat Rust files as part of the cache key:
[tool.uv]
cache-keys = [
{ file = "pyproject.toml" },
{ file = "dist-workspace.toml" },
{ file = "ruff/Cargo.toml" },
{ file = "ruff/Cargo.lock" },
{ file = "**/*.rs" },
]Without the **/*.rs cache key, uv sync would keep serving a stale built artifact after a Rust change. The glob turns every edit to a .rs file into a cache miss, so the next uv sync rebuilds the wheel. This is a Rust-specific setting that most Python projects never need, but it illustrates how uv exposes cache invalidation as data rather than code.
Pin ruff-pre-commit differently in each repo
Every Astral repo uses the astral-sh/ruff-pre-commit mirror, but none share a pinned version:
| Repo | ruff-pre-commit rev |
|---|---|
astral-sh/ruff |
v0.15.10 |
astral-sh/ty |
v0.15.10 |
astral-sh/uv |
v0.14.14 |
Teams that want to keep local and CI behavior in lockstep can use a tool like sync-with-uv to automate the bump between uv.lock and .pre-commit-config.yaml. That isn’t an option in Astral’s repos, where ruff is produced rather than pulled in as a dependency.
Patterns worth copying
- Split ruff configs by sub-project target and rule set. The three TOML snippets above each describe a different Python stack.
- Set explicit priorities on pre-commit hooks when you have formatters that reshape content and fixers that rewrite it.
- Add
cache-keysin[tool.uv]whenever your Python build depends on files uv cannot see by default.