Python Development in Emacs with Direnv
Jul 18, 2021
Introduction
The Python tooling, especially when it comes to package management, can be incredibly annoying to deal with. It's been one of my primary gripes with Python development for years. Recently I have been working on a relatively major Python project in my spare time and have really tried to setup a proper working environment for myself.
Managing Multiple Pythons
Often times you don't want to be on the latest Python
available. There's a variety of reasons for this but usually the
main one is due to package requirements. There are different ways to
manage multiple Python versions. The AUR
contains a few older
versions of python. Or if you prefer, Nix and Guix can be pinned to
a commit containing a specific Python version. But perhaps the
easiest method is to use pyenv.
In addition to being able to install various versions of Python,
pyenv
has shims that allow your shell to determine which version to
use depending on which project you're in. This is a perfectly
acceptable way of doing it but I only use pyenv
to install different
versions. For actually choosing which version of Python to use based
on which project I'm in I use direnv
which I will cover a little
later.
Managing Python Virtual Environments
Modern Python (3.3+) has a built-in way of creating virtual
envrionments. For older Pythons you'll need to use Virtualenv. In
addition there are tools that look to improve the workflow
surrounding virtual environment development in Python such as Pipenv
and Poetry. And as always you can leverage the power of Nix or Guix
to bypass using Python-specific virtual environments entirely. My
personal preference these days is using Poetry
.
Using direnv
direnv is a phenomenal tool that I have been aware of for a while but never actually used until recently. Basically it can automatically enable/disable environment variables depending on the current directory (and subdirectories). This allows you to very quickly have your environment set up per project just by ~cd~ing into a directory.
This is why I'm not using the pyenv
shim, direnv
handles that for me
so it's 1 less command and setup to worry about and in addition
direnv
is language-agnostic. I can use it for almost anything.
There is a global configuration file for direnv
where we'll define
how we want it to interface with our virtual environment. This
example is designed for using with poetry
, but it's fairly
straightforward to use with other setups. In fact there is an
extensive Community Wiki that covers almost every setup you could
want. And creating your own is relatively simple once you understand
how it works. Modern direnv
supports pyenv
out of the box so we
don't even need to set anything up for that.
In $XDG_CONFIG_HOME/direnv/direnvrc
add the following code:
layout_poetry() { if [[ ! -f pyproject.toml ]]; then log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.' exit 2 fi # create venv if it doesn't exist poetry run true export VIRTUAL_ENV=$(poetry env info --path) export POETRY_ACTIVE=1 PATH_add "$VIRTUAL_ENV/bin" }
This is taken straight from the Community Wiki and since it's a
basic bash function, it should be easy enough to follow. Essentially
we first confirm that this is poetry
based project after which we
set the appropriate environment variables and add the location of
the virtual environment that poetry
created in our PATH.
Now that direnv
knows how to interface properly with poetry
we can
set up our individual projects. Go to the root directory of a poetry
based project and create a file called .envrc
. In this file add the
following 2 lines:
layout pyenv 3.9.6 layout poetry
The first line uses the built-in pyenv
integration to set Python
3.9.6 as the version of Python for our project. Make sure you had
previously installed that version using pyenv install 3.9.6
.
The second line utilizes the custom function we defined earlier to
setup our environment to use the virtual environment that poetry
is
managing.
That's it. Two simple files that make life so easy.
Integrating with Emacs
Of course it's all well and good that our shell fully understands
how our Python project is laid out. The next step is to have our
editor understand it as well. This is a big part of why I went with
direnv
. There is really good integration with Emacs. There are Emacs
extensions for interacting with poetry
and pyenv
but that's already
2 more extensions, not to mention I would need to find other well
working extensions for other languages and ecosystems if I wasn't
relying on direnv
.
envrc
There are 2 Emacs extensions I know of for direnv
. There's direnv.el
and envrc. Both are good and well-supported, but envrc
sets
environment variables buffer-locally which I prefer. It's really
just a conceptual difference. I would recommend reading the
documentation for whichever package you go with. For envrc
the only
real configuration after installing it is to enable
(envrc-global-mode)
which will the set the Emacs environment to
match the direnv
one for the current file. It's recommend to enable
that mode as late as possible so that it's mode is prepended to
various hooks and as such is enabled first before other important
hooks that look at the Emacs environment.
Language Server Protocol (LSP)
Now to truly leverage this environment I would recommend using a Python language server coupled with an lsp client for Emacs.
By virtue of being such a popular language, there are quite a few
Python language servers available. As far as I'm aware there are 3
that are actively developed. Personally I'm using Pyright which is
the open source core of Microsoft's Python language server called
Pylance
. In my experience Pyright
works best out of the available
Python language servers and Pylance
itself is the default in VSCode
anyway.
In terms of the lsp client, like all for all things Emacs, there
are competing solutions. lsp-mode and Eglot. Both are quite good
and well maintained and I've used both in the past to great
success. These days I'm using lsp-mode
so that's what I'll
discuss. There's actually pretty nice documentation on their
website for setting up both lsp-mode
and using it with
pyright
. Essentially the basic lsp-mode
package is easily installed
after which you would also have to install and require
lsp-pyright.
One thing to note is that you want lsp-mode
to activate after envrc
has finished setting up the Emacs environment. So you should add
the hook for lsp-pyright
before you add the (envrc-global-mode)
config line.
Extras
At this point you can setup nice additional features to mimic a more IDE-like development setup. The following is a non-exhaustive list:
- Company-mode
- Flymake/Flycheck
- YASnippets
- Magit
Conclusion
So far I'm really loving this system. Not just for Python but the
combination of direnv
and lsp
works beautifully with practically
every programming language and environment without needing to
install and configure a lot of very language-specific Emacs
extensions. At most you'll just need to install the appropriate
language server and then (sometimes) the specific lsp library for
that language server like we did for pyright
.