Emacs + Python + uv

Posted on Dec 1, 2024

Python has yet another packaging tool, uv [fn]. Here’s an opinionated configuration for uv based projects that plays well with Emacs. It uses a .dir-locals.el approach to make the config project specific, so you can drop it into a project without impacting any existing Python config.

The configuration and tooling includes:

Create a new project

Create a new Python project using uv, we’lll call it example and then cd into it:

uv init example
cd example

uv prepopulates the project with some basics:

tree
.
├── hello.py
├── pyproject.toml
├── README.md
└── uv.lock

ruff

ruff is astral’s linter; similar to uv it’s written in Rust and is the fastest in its class.

Add ruff to the dev dependencies, and give your project a first lint:

uv add --dev ruff
uv run ruff check

See the tutorial for next steps configuring ruff.

pytest

Add pytest to the project’s dev dependencies and run Python tests using the pytest runner:

uv add --dev pytest
uv run pytest

pytest integrates smoothly with Emacs via the python-pytest package. Configure it by setting the python-pytest-executable variable in .dir-locals.el (create the file under your project root if it doesn’t already exit):

(python-mode . ((python-pytest-executable . "uv run pytest")))

ipython

Add ipython to the project’s dev dependencies and start the ipython console by running:

uv add --dev ipython
uv run ipython

To have Emacs use this project’s IPython as the default interpreter, set the python-shell-interpreter and python-shell-interpreter-args vars using .dir-locals.el:

(nil . ((python-shell-interpreter . "uv")
        (python-shell-interpreter-args
         . "run ipython -i --simple-prompt --InteractiveShell.display_page=True")))

pyright

The pyright language server provides an LSP server.

uv add --dev pyright

You won’t typically start the pyright language server directly; eglot will start it before trying to connect. Here is the .dir-locals to configure eglot:

((python-mode
  . ((eval . (add-to-list
            'eglot-server-programs
            `((python-mode python-ts-mode) .
              ,(eglot-alternatives
                '(("uv" "run" "pyright-langserver" "--stdio")))))))))

Putting it all together

The .dir-locals.el should look like:

((python-mode
  . ((python-pytest-executable . "uv run pytest")
     (eval . (add-to-list
             'eglot-server-programs
             `((python-mode python-ts-mode) .
               ,(eglot-alternatives
                 '(("uv" "run" "pyright-langserver" "--stdio"))))))))
 (org-mode . ((org-babel-python-command . "uv run python")))
 (nil . ((python-shell-interpreter . "uv")
         (python-shell-interpreter-args
          . "run ipython -i --simple-prompt --InteractiveShell.display_page=True"))))