Emacs + Python + uv
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:
- Test runner (pytest)
- Linting (ruff)
- Syntax / type-checking (Eglot + pyright)
- Python shell (IPython)
- Org babel
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"))))