ty: A Fast Python Type Checker and LSP for Emacs
Contents
Introduction
Recently Astral↗ (the team behind Ruff↗ and uv↗) announced the beta release of ty↗, an extremely fast Python type checker and language server written in Rust.
The performance numbers are impressive. Without caching, ty is consistently 10-60x faster than mypy and Pyright. In an editor context, the gap is even more dramatic: after editing a load-bearing file in the PyTorch repository, ty recomputes diagnostics in 4.7ms, which is 80x faster than Pyright (386ms) and 500x faster than Pyrefly (2.38s).
Beyond raw speed, ty includes some sophisticated type system features like first-class intersection types, advanced type narrowing, and best-in-class diagnostic messages inspired by Rust’s compiler. The diagnostics can pull context from multiple files to explain not just what’s wrong, but why.
Setting Up ty with Eglot
Since ty implements the Language Server Protocol, it’s straightforward to use with Emacs. Here’s my configuration using Eglot, the built-in LSP client:
(defun my-project-find-python-project (dir)
(when-let ((root (locate-dominating-file dir "pyproject.toml")))
(cons 'python-project root)))
(with-eval-after-load "project"
(cl-defmethod project-root ((project (head python-project)))
(cdr project))
(add-hook 'project-find-functions #'my-project-find-python-project))
(add-to-list 'auto-mode-alist '("/uv\\.lock\\'" . toml-ts-mode))
(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))
(add-to-list 'eglot-server-programs
'((python-ts-mode python-mode)
. ("ty" "server")))
(add-hook 'python-ts-mode-hook #'eglot-ensure)
The project-find-functions hook ensures that Eglot starts ty from the correct
directory in monorepos, picking up the right paths for code references. This is
particularly important when you have multiple Python projects with different
virtual environments.
If you’re using uv to manage your tools, you can use uvx instead:
(add-to-list 'eglot-server-programs
'((python-ts-mode python-mode)
. ("uvx" "ty" "server")))
The Multi-LSP Question
A question that sometimes comes up in the Emacs community is whether multi-LSP support is needed. For Python specifically, you might want both a type checker (ty, Pyright) and a linter/formatter (Ruff) running simultaneously on the same file.
How Other Editors Handle This
VSCode handles this transparently. Multiple language servers can attach to the same file type, and the editor merges their capabilities. This is largely invisible to users.
Neovim also has built-in support for multiple LSP clients per buffer. You can attach as many language servers as you want to a buffer, and Neovim will aggregate their diagnostics, completions, and other features.
Zed has built-in support for multiple language servers per language. You can enable or disable servers in your settings using a simple array syntax, and Zed already includes ty as a built-in option alongside basedpyright, Pyright, Ruff, and PyLSP.
lsp-mode (for Emacs) supports running multiple servers for the same file
type. You can register a server with the :add-on? t flag to start it in
parallel with other servers.
OpenCode (the AI coding agent) also handles multiple LSP servers natively. When a file is opened, OpenCode checks the file extension against all enabled LSP servers and starts each applicable one. Diagnostics from all servers are aggregated into a single collection, which is then provided to the LLM as context. The implementation is straightforward: each server runs in its own process, and diagnostics are merged by file path.
Eglot’s Approach
Eglot currently doesn’t support multiple language servers per buffer out of the box. The primary solution being developed by Eglot’s author is rassumfrassum↗, an external LSP multiplexer that presents multiple LSP servers as a single server to the client.
rassumfrassum works by spawning multiple LSP server subprocesses and merging their capabilities, diagnostics, and responses. For Python with ty and Ruff, you can run it directly from the command line:
rass -- ty server -- ruff server
Then configure Eglot to use that command:
(add-to-list 'eglot-server-programs
'((python-ts-mode python-mode)
. ("rass" "--" "ty" "server" "--" "ruff" "server")))
For the ty + Ruff use case specifically: these tools are complementary, not overlapping. ty is a type checker (replacing mypy/Pyright), while Ruff is a linter and formatter (replacing Flake8/Black/isort). If you want both type checking and linting/formatting, you’d need both servers running. That said, ty does include some features that overlap with linting, like dead code elimination and unused dependency detection, so the boundary may blur somewhat as ty matures.
Aside - Is an External Multiplexer Needed?
It’s worth asking whether an external tool like rassumfrassum is the right approach when most other editors have built-in multi-LSP support.
While this may keep Eglot’s codebase simpler and promote reusability, it’s also very much not aligned with what other popular editors are doing, as we’ve shown earlier.
One could argue that rass makes it easier to customize server combinations
per-project via its Python preset files. But this is really just trading one
config format for another - Eglot already supports per-project configuration via
.dir-locals.el. I also like the competing idea of autodetecting what’s needed:
my emacs-shared↗ configuration has
this, albeit just for JS/TS projects currently.
Further pain points with rass:
- Extra complexity for users (another tool to install and configure)
- Potential performance overhead from the proxy layer, especially given the implementation in Python
- I’d expect multi-LSP to “just work” like it does in other editors
- lsp-mode already handles this natively
- The documentation emphasizes Python config files (
.pypresets) over command-line arguments, which makes the tool feel more complex than it needs to be for simple use cases
One caveat: the approach of splitting up multiple LSP servers using a --
separator seems reasonably ergonomic to use with eglot-server-programs, but I
wish that it were brought forward a bit more in the docs.
I think I have to conclude that if rass continues to be the way forward with
Eglot, I may well end up switching to lsp-mode in the future. This may
especially be the case for JS/TS projects, which will likely have fun
combinations of eslint, oxlint, and biome for a while due to the evolving
nature of those tools and the performance vs functionality trade-offs.
Current Status
ty is in beta, so some LSP features may not be fully implemented yet. The official docs↗ list supported capabilities including Go to Definition, Symbol Rename, Auto-Complete, Auto-Import, Semantic Syntax Highlighting, and Inlay Hints.
A patch to add ty to Eglot’s default server list has already been submitted by Steve Purcell (the MELPA maintainer), so it should be included in a future Emacs release.
If you’re doing Python development in Emacs, ty is worth trying out. The speed improvements alone make it compelling, and the diagnostic quality is excellent.