Setting up Emacs Python LSP with PyEnv and stuff


There are many documents describing how to set up Python language servers for autocompletion in Emacs. The ones I read however were all missing the following features:

  • Switching between multiple Python versions
  • Handling Python virtual environments I have a pretty simple workflow that I am now happy with, so I figured I would share it with all of you!

Setting up the Environment

The tools I'm using:

  • pyenv: manage multiple python versions
  • direnv: handle project specific enviornment configuration
  • venv: switching between Python virtual environments

Setting up pyenv:

There is an official autoinstaller. I sort of hate the idea because of the security risks but it's not like I am going to be checking the install script anyways:

curl | bash

For those of you who are more security conscious, follow the other installation instructions.

Now, I'm using zsh as my shell. Let's set up the pyenv configuration in the ~/.zshenv:

export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

Note that I'm doing this in ~/.zshenv instead of ~/.zshrc because ~/.zshrc is for interactive shells. ~/.zprofile will work as well. You can read about the different roles of zsh startup files in this great stackoverflow post.

Now, if we open a new terminal and type whereis pyenv you should get something similar to what I get below:

kota@kota-ThinkPad-P1> whereis pyenv                                                                       ~
pyenv: /home/kota/.pyenv/bin/pyenv


Installing direnv

Wel, direnv is also easy to install as it is prepackaged for many Linux distributions. On Ubuntu:

sudo apt install direnv

Then, we want to install the hooks into your shell. In my case, this is zsh, so in my .zshrc:

eval "$(direnv hook zsh)"

Let's also add the pyenv and venv configuration to ~/.config/direnv/direnvrc (stolen from the direnv documentation):

use_pyenv() {
  # Because each python version is prepended to the PATH, add them in reverse order
  for ((j = $#; j >= 1; j--)); do
    local python_version=${!j}
    local pyenv_python=$(pyenv root)/versions/${python_version}/bin/python
    if [[ ! -x "$pyenv_python" ]]; then
      log_error "Error: $pyenv_python can't be executed."
      return 1

    unset PYTHONHOME
    local ve=$($pyenv_python -c "import pkgutil; print('venv' if pkgutil.find_loader('venv') else ('virtualenv' if pkgutil.find_loader('virtualenv') else ''))")

    case $ve in
        export VIRTUAL_ENV
        if [[ ! -d $VIRTUAL_ENV ]]; then
          $pyenv_python -m venv "$VIRTUAL_ENV"
        PATH_add "$VIRTUAL_ENV"/bin
        layout_python "$pyenv_python"
        log_error "Error: neither venv nor virtualenv are available to ${pyenv_python}."
        return 1

    # e.g. Given "use pyenv 3.6.9 2.7.16", PYENV_VERSION becomes "3.6.9:2.7.16"
    [[ -z "$PYENV_VERSION" ]] && PYENV_VERSION=$python_version || PYENV_VERSION="${python_version}:$PYENV_VERSION"


Setting up the Project

I created a very simple project structured as follows:

├── .envrc
├── foo
│   ├──
│   ├──
│   └──

All this application does is find the length of vector (1, 2, 3):

import numpy as np

def run():
    arr = np.array([1, 2, 3])

The key thing here is it uses an external dependency.

Let's also take a look at the

from setuptools import setup, find_packages
from foo import __version__
    author="Kota Weaver",
        'dev': [
        'test': [
            'pytest', 'pyflakes'

Note that I have the Python LSP server listed in the dev dependencies.

Now let's also set up the Python development enviornment! First my .envrc:

export SIMENV_PYTHON=3.8.1

use pyenv $SIMENV_PYTHON

Now, if we go into the project directory, and do a direnv allow, we should be able to install the correct version of Python using:

pyenv install $SIMENV_PYTHON

(NOTE: you may need to install some of the following dependencies: libffi-dev libssl-dev libreadline-dev libsqlite3-dev libbz2-dev)

Then, leave the directory and enter again, and install the dependencies using:

pip install -e .['dev','test']

(depending on your shell you may need to escape the [ and ] with \)

Make sure the which python shows the correct location! This allows us to run the program using:

kota@kota-ThinkPad-P1> python -m foo                                                                   ~/foo


Setting up Emacs

Now we get into the meat of it all… Let's configure our Emacs to do smart things with this!

I'm using use-package, which I bootstrap if it is not installed. I have this set to be a very simple ~/.emacs.d/init.el so you can take what you want:

(setq package-archives '(("gnu" . "")
                         ("marmalade" . "")
                         ("melpa" . "")))

(when (not (package-installed-p 'use-package)) (package-refresh-contents) (package-install 'use-package))
(require 'use-package)

; direnv mode allows automatic loading of direnv variables
(use-package direnv
  :ensure t

; setup Emacs path from our ~/.zshenv
(use-package exec-path-from-shell
  :ensure t
  (when (memq window-system '(mac ns x))

					; we also should make sure we have flycheck installed
(use-package flycheck
  :ensure t)

; Let's set up company! perhaps not necessary but this is what i like to use
(use-package company
  :ensure t
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1))

; install lsp mode
(use-package lsp-mode
  :ensure t
  :hook (python-mode . lsp-deferred)
  :commands (lsp lsp-deferred))

; let's add the lsp company backend
(use-package company-lsp
  :ensure t
  (push 'company-lsp company-backends))

; also installs lsp as a dependency
(use-package lsp-ui
  :ensure t
  :hook (lsp-mode . lsp-ui-mode))

And the necessary screenshot from my machine (this is my normal setup, rather than the one above, but the same functionality should be there):