Skip to content

How Astral Uses Its Own Tools

April 23, 2026ยทTim Hopper

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:

.github/workflows/ci.yaml
- 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:

pyproject.toml
[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 = true

The uv repo keeps its ruff config in a four-line ruff.toml:

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:

pyproject.toml
[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-format pass 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:

pyproject.toml
[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-keys in [tool.uv] whenever your Python build depends on files uv cannot see by default.

Learn More

Last updated on

Please submit corrections and feedback...