description: I found it surprisingly tricky to get good information on how to set up lsp-mode to work with a Python virtual environment. Here's my solution.


I found it surprisingly tricky to get good information on how to set up lsp-mode to work with a Python virtual environment. Here's my solution.

Global setup

Use pyvenv. This provides Emacs lisp code that can set up Emacs to use a particular virtualenv using the pyvenv-activate function. I do this in my init.el with use-package:

(use-package pyvenv
  :ensure t)

Of course, I also have lsp installed:

(use-package lsp
  :ensure t)

Per-project setup

Add a file named ".dir-locals.el" to the root of the project. For example, here's mine for the precovery project (note: see addendum below for an updated version):

((python-mode . ((pyvenv-activate . "~/code/b612/precovery/.precovery-venv/")
         (pyvenv-post-activate-hooks . (lsp)))))

The pyvenv-activate cell's value should be a path to a virtualenv directory. For example, that one was made with

cd ~/code/b612/precovery
python -m venv .precovery-venv

Within each per-project virtualenv, it's important to install the python-lsp-server package:

source .precovery-venv/bin/activate
pip install python-lsp-server

That's all there is to it.

The tricky bit here is really pyvenv-post-activate-hooks. The lsp function needs to be called after the Python virtual environment has been activated.

Addendum (2024-12-17):

A reader named Alexei emailed me with an improvement upon the above .dir-locals.el content.

He writes:

This works great for the first file opened in the workspace. The problem shows up with the subsequent files in the same workspace (or at least it did for me).

Since the given venv is already activated, new activations are not happening and pyvenv-post-activate-hooks is not called again. Thus 2nd and subsequent files don't have lsp enabled automatically. I had to manually M-x lsp them.

[...]

The small modification that worked for me was to change .dir-locals.el to:

((python-mode . ((eval . (pyvenv-activate "~/.virtualenvs/venv"))
                 (eval . (lsp)))))

Turns out there is a pyvenv-activate function in addition to the var by the same name. Eval executes things on the spot, so no more dealing with eventuality. And the lsp loading will be done for every file, even if venv has already been activated.

I hope you find this useful.

Thanks, Alexei.