mrkeyoor.com_
Fri 07 Aug 20:52 UTC
PyPIUtilsupdated 07 Aug 2026

ty

ty is a Python type checker and language server written in Rust by Astral, the team behind uv and Ruff. It does the same job as mypy or Pyright: read your annotations, read typeshed and the stubs in your virtual environment, and tell you where the types do not line up. The difference is the implementation. It is a native binary with no Python startup cost, it reuses Ruff's parser, and it recomputes only the parts of the program a change actually affected, which is what makes the editor side feel instant on a large repository. The same binary serves both roles: ty check on the command line and ty server as an LSP with completions, hover, go-to-definition, auto-import and inlay hints. The type system it implements is deliberately built for codebases that are only partly annotated, with support for redeclarations, intersection types and narrowing that follows reachability. It is also, by the maintainers' own version policy, beta software on 0.0.x version numbers with no stable API.

Verdict

ty is already the fastest way to get type feedback in an editor, and the CLI is fast enough to change how often you run it. Treat it as a second opinion pinned to an exact version until it leaves 0.0.x, and keep mypy or Pyright as the gate if your project relies on plugins.

API stability2/5Self-declared beta on 0.0.x, with the README stating that breaking changes including diagnostics changes may occur between any two versions. Releases 0.0.65 through 0.0.69 all shipped between 29 July and 6 August 2026, so an unpinned install in CI is a moving target and rule names can change under you.
Docs4/5docs.astral.sh/ty covers installation, configuration precedence, exclusions, suppression comments, rule levels and a page specifically for people coming from mypy or Pyright, plus a generated per-rule reference. The gap is that the authoritative list of which typing features are implemented is an open GitHub issue, not a docs page.
Maintenance5/5Astral ships releases several times a week and the repository was pushed on 7 August 2026, the same week as 0.0.69. The 845 open issues plus 6 open PRs reflect the volume of feedback on a beta rather than neglect, and the near-empty PR queue is because the Rust source lives in the ruff repository.
Ecosystem3/519,410 stars and around 9.2M weekly downloads in roughly fifteen months since the repository was created in May 2025, with documented editor setups for VS Code, PyCharm and Neovim. Against that, there is no plugin API, so nothing from the mypy plugin ecosystem carries over and framework-heavy codebases lose type information ty cannot recover.

Use it if

  • Type checking is the slow step in CI or your editor stalls on a repository large enough that mypy's incremental cache stopped helping
  • You already standardise on uv and Ruff and want the same distribution story: uvx ty check with nothing installed, a standalone installer that needs no Python at all, and one version pinned in the lockfile
  • You want an open-source language server with completions and go-to-definition that is not tied to a specific editor, which Pyright's Pylance bundle is
  • You are adding types to a partly annotated codebase and want a checker whose defaults tolerate that, with per-directory overrides for tests and generated code
  • You need CI output in a native format: --output-format github, gitlab or junit are built in rather than bolted on with a wrapper
Skip it if

Setup reality

uvx ty check runs it with nothing installed, which is the fastest way to try it, but for a team you want uv add --dev ty so everyone gets the same build; there is also a standalone shell installer that needs no Python on the machine. The setting that actually decides whether ty is useful is environment discovery: it looks at VIRTUAL_ENV, then a .venv in the project root, then an activated Conda environment, then any python3 on PATH. In a CI job that installs dependencies without activating the venv it will happily fall back to the system interpreter and drown you in unresolved-import errors, so either activate the environment or pass --python .venv. Configuration goes in [tool.ty] inside pyproject.toml or in a ty.toml, and if both exist the ty.toml wins and the pyproject table is ignored completely rather than merged. A pyproject.toml with no tool.ty table is skipped and discovery walks further up the tree, which is a real surprise in a monorepo. Finally, the target Python version defaults to the lower bound of your requires-python, so a lax requires-python = ">=3.9" makes ty check your code as 3.9 and flag syntax that your actual runtime supports; set python-version explicitly if those two ever disagree.

Patterns

Try it on a project in one commandrun-without-installing

uvx ty check

# or check specific paths
uvx ty check src scripts/backfill.py

Paths passed as positional arguments are checked even when an exclude pattern or a gitignore entry would normally skip them, unless you also pass --force-exclude. Run this from inside the project so it finds the pyproject.toml and the virtual environment.

Pin ty as a dev dependencypin-in-project

uv add --dev ty
uv run ty check

# bump it deliberately
uv lock --upgrade-package ty

Pinning matters more here than for most tools: on 0.0.x, a new release can change which diagnostics fire, so an unpinned CI job can fail on a commit that touched nothing. uv run also sets VIRTUAL_ENV, which is how ty finds your installed packages.

Set rule severity in the project configconfigure-rule-levels

# pyproject.toml
[tool.ty.rules]
possibly-missing-import = "error"
possibly-unresolved-reference = "warn"
redundant-cast = "ignore"

[tool.ty.terminal]
output-format = "concise"

If a ty.toml exists next to this file, the whole [tool.ty] table is ignored rather than merged, which is the most common reason a config change appears to do nothing. A pyproject.toml with no [tool.ty] table at all is skipped and discovery continues up the directory tree.

Silence one diagnostic without disabling the rulesuppress-inline

a = 10 + "test"  # ty: ignore[unsupported-operator]

sum_three("one", 5)  # ty: ignore[missing-argument, invalid-argument-type]

# combine with another checker in one comment
sum_three("one", 5, 2)  # type: ignore[arg-type, ty:invalid-argument-type]

In a plain type: ignore comment, ty only reads codes prefixed with ty: and ignores everything else, so a mypy code sitting there suppresses nothing on the ty side while a bare type: ignore suppresses every rule on that line. A file-level suppression is the same comment placed on its own line before any code.

Choose what gets checkedselect-files

# pyproject.toml
[tool.ty.src]
include = ["src", "tests"]
exclude = ["src/generated", "!**/build/"]
respect-ignore-files = true

Patterns are anchored to the project root, so exclude = ["src"] does not match tests/src; use **/src for that, at a cost in discovery speed. A leading ! re-includes something the built-in default exclusions dropped, which is how you get build/ checked again.

Different strictness for tests and generated coderelax-rules-per-directory

# pyproject.toml
[[tool.ty.overrides]]
include = ["tests/**", "**/test_*.py"]

[tool.ty.overrides.rules]
possibly-unresolved-reference = "warn"

[[tool.ty.overrides]]
include = ["generated/**"]
exclude = ["generated/important.py"]

[tool.ty.overrides.rules]
unresolved-attribute = "ignore"

Overrides are an array of tables, so the double brackets are load-bearing and a single-bracket [tool.ty.overrides] is a different, wrong shape. When several overrides match the same file, later entries win, and any override beats the global [tool.ty.rules] table.

Stop a dependency without stubs from flooding the outputhandle-untyped-dependencies

# pyproject.toml
[tool.ty.analysis]
allowed-unresolved-imports = ["legacy_vendor.**", "!legacy_vendor.core"]
replace-imports-with-any = ["some_untyped_sdk.**"]

The two settings do different things: allowed-unresolved-imports only silences the unresolved-import diagnostic, while replace-imports-with-any makes the module resolve to Any so downstream usage stops erroring too. Reach for the second one only when you accept losing all type information behind that import.

Check against the interpreter you actually deploy onpin-python-version

# pyproject.toml
[tool.ty.environment]
python-version = "3.12"
python-platform = "linux"
python = "./.venv"

Without python-version, ty takes the lower bound of project.requires-python, so a permissive ">=3.9" makes it reject match statements in a service that only ever runs on 3.12. python-platform decides what sys.platform narrows to, which changes whether platform-specific stdlib attributes exist.

Change severity for one runoverride-rules-on-cli

ty check \
  --error possibly-missing-attribute \
  --warn unused-ignore-comment \
  --ignore redundant-cast

# ratchet: everything is an error
ty check --error all

Later flags override earlier ones, and command-line settings always beat both config files. --error all is the honest way to see the true size of the backlog before you decide which rules to turn down in config.

Wire it into GitHub Actionsci-exit-codes

- run: uv sync --dev
- run: uv run ty check --output-format github --error-on-warning

# during migration, report but do not block
- run: uv run ty check --output-format concise --exit-zero

--error-on-warning and --exit-zero are mutually exclusive and ty rejects the combination rather than picking one. The github format emits workflow annotations so failures land inline on the diff; junit and gitlab are there for other runners.

Run continuously or as a language serverwatch-and-serve

ty check --watch      # recheck affected files on save
ty server             # LSP over stdio, for your editor config

Watch mode uses the same incremental engine as the language server, so after the first run it only recomputes what the edit reached, which is much faster than looping ty check. ty server takes no options; everything is configured through the editor client and the project config file.

Look up what a rule actually meansexplain-a-diagnostic

ty explain rule invalid-argument-type
ty explain rule unresolved-import

Worth doing before you add a suppression comment, because several ty rule names have no mypy counterpart and the fix is often a real bug rather than a checker disagreement. The full list is also generated into the rules reference on the docs site.

Alternatives

PackageRegistryPick it when
mypyPyPIYou need plugins, a decade of settled behaviour, or the strict mode your team already writes code against
pyrightPyPIYou want a mature checker with strong inference today and are happy running it through Node
pyreflyPyPIYou want the other fast Rust checker and would rather bet on Meta's implementation than Astral's