Python & Emacs, 2025
2025-08-27
Here we go with installment four of the unofficial series (first, second, third). I've decided I'll start putting the year in the title.
Back in 2022 python-base-mode was
added
to Emacs such that (a) python-mode and python-ts-mode can inherit
a lot of shared functionality, and (b) users don't have to account for
customizing both modes (when their configuration needs to be used in
both tree-sitter and non-tree-sitter enabled Emacs builds). It's a
good idea to use the generic major mode when possible.
These days I'm on the Astral train, which really
means ruff and uv (and ty, sort of, since they're not done yet).
Unfortunately ruff isn't able to run both format and the import
sorting mechanism of the linter (ruff check) in the same command. So
I have a couple of reformatters defined to handle both steps:
(use-package reformatter
:ensure t
:config
(reformatter-define dd/ruff-format
:program "uvx"
:args `("ruff" "format" "--stdin-filename" ,buffer-file-name "-"))
(reformatter-define dd/ruff-sort
:program "uvx"
:args `("ruff" "check" "--select" "I" "--fix" "--stdin-filename" ,buffer-file-name "-")))
The default virtual environment path for a project managed with uv
is just .venv. Combining that behavior with my reformatters, I get a
simple "init" function for all of my Python buffers. The function
automatically finds .venv in the project and calls pyvenv-activate
(from the pyvenv package);
it also enables the reformatters:
(defun dd/python-init ()
(let* ((project (project-current))
(project-root (when project (project-root project)))
(venv-path (when project-root
(expand-file-name ".venv" project-root))))
(when (and venv-path (file-directory-p venv-path))
(make-local-variable 'pyvenv-virtual-env)
(pyvenv-activate venv-path))
(dd/ruff-format-on-save-mode +1)
(dd/ruff-sort-on-save-mode +1)))
(add-hook 'python-base-mode-hook #'dd/python-init)
The reason I need to "activate" the virtual environment in Emacs is
because I typically install whatever language servers I want to play
with in the virtual environment of a project. Then, invoking M-x
eglot will give us choices based on whatever eglot finds
(through its executable-find usage in eglot-alternatives calls
within eglot-server-programs). My Python incantation:
(add-to-list 'eglot-server-programs
`(python-base-mode
. ,(eglot-alternatives '(("basedpyright-langserver" "--stdio")
("ty" "server")
("pyright-langserver" "--stdio")))))
You'll notice that I cycle through using basedpyright, ty, and pyright.
I'm hopeful that sometime in the near future my eglot-alternatives
list can be redefined to
(add-to-list 'eglot-server-programs
`(python-base-mode
. ,(eglot-alternatives '(("uv" "run" "basedpyright-langserver" "--stdio")
("uv" "run" "ty" "server")
("uv" "run" "pyright-langserver" "--stdio")))))
such that I can rely on uv's automatic virtual environment
discovery/usage, removing the need to have code that activates a
virtual environment. The current eglot-alternatives implementation
in Emacs uses the first entry of the list you provide to pass along to
completing-read, and completing-read ignores duplicates (and all
entries are duplicates in the list above). I filed
79290; maybe
it'll go somewhere eventually, but it's not a big deal.