Trying Ty for my LSP in Emacs

3 min read

Astral announced Ty Beta today, the new Rust-based Python type checker. I’ve been using Ruff and uv for a while now, and they’ve consistently delivered on the “rewrite Python tooling in Rust to make it absurdly fast” promise. So when they claimed Ty is 10-80x faster than Pyright for incremental checking, I had to try it.

I’ve been using basedpyright with Eglot and it works, but I’d be lying if I said the lag didn’t bother me. When you’re editing and the type checker is half a second behind, you stop trusting the feedback. The squiggly lines show up after you’ve already moved on to the next thought.

The Dual Setup

Since Ty just hit Beta, I wasn’t ready to completely abandon basedpyright. Instead, I set up a toggle so I can switch between them. Here’s my configuration:

  ;; Python LSP Server Selection
  (defvar my/python-lsp-server 'ty
    "Which Python language server to use: 'basedpyright or 'ty")

  (defun my/python-lsp-command ()
    "Return the LSP command based on selected server."
    (pcase my/python-lsp-server
      ('ty '("ty" "server"))
      ('basedpyright '("basedpyright-langserver" "--stdio"
                       :initializationOptions (:basedpyright (:plugins (
                         :ruff (:enabled t
                               :lineLength 88
                               :exclude ["E501"]
                               :select ["E" "F" "I" "UP"])
                         :pycodestyle (:enabled nil)
                         :pyflakes (:enabled nil)
                         :pylint (:enabled nil)
                         :rope_completion (:enabled t)
                         :autopep8 (:enabled nil))))))))

  (defun my/switch-python-lsp ()
    "Toggle between Ty and basedpyright, restart Eglot."
    (interactive)
    (setq my/python-lsp-server
          (if (eq my/python-lsp-server 'ty) 'basedpyright 'ty))
    (when (eglot-managed-p)
      (ignore-errors (eglot-shutdown (eglot-current-server)))
      (sleep-for 0.5)
      (eglot-ensure))
    (message "Switched to %s" my/python-lsp-server))

  The Eglot configuration uses a lambda to dynamically evaluate which server to start:

  (use-package eglot
    :ensure t
    :defer t
    :commands (eglot eglot-ensure)
    :config
    (setq eglot-server-programs
          '((python-ts-mode . (lambda (&rest _) (my/python-lsp-command)))
            ((js-ts-mode typescript-ts-mode tsx-ts-mode) .
             ("typescript-language-server" "--stdio"))))
    :hook ((python-ts-mode . eglot-ensure)))

That lambda matters. Without it, Eglot evaluates my/python-lsp-command once when your config loads and hardcodes the result. The lambda forces fresh evaluation each time Eglot starts a server, which is what makes toggling work.

The speed difference is very noticeable. Fix a type error and the diagnostic disappears immediately instead of lingering for that half-second that makes you wonder if it registered. It’s the kind of improvement that changes whether you pay attention to type feedback or ignore it. Completions and jumping to definitions are instant now.

I hit one issue. Ty doesn’t handle the LSP shutdown request cleanly, throwing a JSON parsing error. Wrapping the shutdown in ignore-errors fixed it, but it’s the kind of rough edge you expect from a December 2024 Beta release.

Installation

Install Ty globally

uv tool install ty@latest

Keep basedpyright around

pip install basedpyright ruff

Then M-x my/switch-python-lsp toggles between them.

Is It Worth It?

If you’re already using Ruff and uv, yes. The speed improvement is real. The dual setup gives you a fallback when if I hit issues, which I expect to happen occasionally given the Beta status.

My emacs config is on Github for reference.

Comments from Mastodon

No comments yet. Start the conversation!

Comment on Mastodon