For years, I have struggled to get elpy running with direnv - and have recently found the missing link, which was hiding in the setting elpy-rpc-virtualenv-path. If you set it to 'current, life is good.

direnv + pyenv

First of all, why would you want to use direnv for your development purposes? I primarily use it as a nice tool to help me manage virtual environments, and it also allows me to develop using a specific Python version.

For example, this is what I currently have in .envrc:

layout pyenv 3.10.3

This sets up a virtualenv using pyenv, containing a Python 3.10.3 interpreter so I am able to use spiffy language features like PEP-604’s writing union types as X | Y.

If you are on a Mac, direnv and pyenv can be installed using the usual homebrew commands. Follow the instructions on how to integrate it into your shell.

You will also need some configuration to use direnv in Emacs, for example, if you are using use-package, the following snippet should be enough to get you going:

(use-package exec-path-from-shell
  :config (progn (exec-path-from-shell-initialize)))
(use-package direnv
  :after exec-path-from-shell
  :config (direnv-mode))

Note that if you use direnv, every project has its own virtualenv that is no longer shared with the rest of the system. In this case, elpy needs the jedi package to be installed, which means you’ll need to install it in every virtualenv you want to use elpy in. It is easy though:

$ python -m pip install jedi
Collecting jedi
  Using cached jedi-0.18.1-py2.py3-none-any.whl (1.6 MB)
Collecting parso<0.9.0,>=0.8.0
  Using cached parso-0.8.3-py2.py3-none-any.whl (100 kB)
Installing collected packages: parso, jedi
Successfully installed jedi-0.18.1 parso-0.8.3


Now it is time to set up elpy ! Again, this is easy – once you know how. Just add something like the following to your Emacs configuration:

(use-package elpy
  (elpy-rpc-virtualenv-path 'current)  ;; this makes elpy use direnv venvs
  (elpy-modules '(elpy-module-company elpy-module-eldoc elpy-module-flymake elpy-module-pyvenv elpy-module-yasnippet elpy-module-django elpy-module-sane-defaults))  ;; removes elpy-highlight-indentation

The most important part is the elpy-rpc-virtualenv-path setting, which will make elpy use the current path, which in this case will have been set by the direnv integration in Emacs.

bonus contents: darker

black is a combination of a tool and a somewhat ossified coding style that aspires to improve the legibility of Python code. Unfortunately, it fails to reach that goal every now and then, and I prefer that black does not touch my artisanal code in those cases. Therefore, I have been using a wrapper called black-macchiato that makes it possible to let black selectively reformat a portion of the code.

However, there is something nicer out there that automates running black but prevents it from reformatting code that was not touched since the last merge: darker. This great piece of software can be installed in your virtualenv using pip:

$ python -m pip install darker[isort]
Collecting darker
  Using cached darker-1.4.2-py3-none-any.whl (84 kB)

Note that by supplying [isort] it will also automatically pull in dependencies to run isort from darker, which should help you to keep your import statements tidy.

I have the following snippet in my emacs-configuration to get darker to run automatically on save (most of the time) using reformatter:

(use-package reformatter
  :hook ((python-mode . darker-reformat-on-save-mode))
  (reformatter-define darker-reformat
    :program "darker"
    :stdin nil
    :stdout nil
    :args (list "-q" "-i" "-S" input-file))

  (:map python-mode-map
    ("<f2>" . darker-reformat-region)
    ("<M-f2>" . darker-reformat-on-save-mode)))  ;; to disable temporarily

Note that darker also supports pyproject.toml integration, so instead of putting arguments like "-i" and "-S" in the reformatter-configuration, you can also put it there and share it with other people working on this project.

line-length = 104  # let's compromise between black's 88 and a sensible 120 🤦‍♂️
target-version = ["py310"]
include = '\.pyi?$'
skip-string-normalization = 1
isort = 1

Unfortunately this duplicates bits of the configuration from black but that is a small price to pay – in the end, they’re both configured in the same file. Just be sure to keep them in sync.