Emacs + Python + Hatch
Python has yet another packaging tool, Hatch. Here’s an opinionated
configuration for Hatch 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 sections below walk through the configuration and tooling for a Python project. Each section works standalone to add some particular functionality, and the full configuration is at the bottom of this post.
The configuration and tooling includes:
Use hatch new
to create a fresh project and then cd
in from your
shell of choice:
hatch new PROJECT_NAME
cd PROJECT_NAME
Using pytest as the test runner
Hatch projects are pre-configured to use the excellent pytest
runner. 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 . "hatch run pytest")))
Setting up a Python language server (pyright) and client (eglot)
Eglot can start up and connect to a language server. Running the language server from within the hatch project gives it access to the project’s dependencies and tooling
(python-mode
. ((eval . (add-to-list
'eglot-server-programs
`((python-mode python-ts-mode) .
,(eglot-alternatives
'(("hatch" "run" "pyright-langserver" "--stdio"))))))))
Hatch also puts in the work to pre-configure the project for MyPy type-checking. This guide uses pyright as the language server because it’s snappy and comes with a first class language server implementation, but I’d still recommend using MyPy as the Guido-blessed gold standard type-checker for your project. MyPy is slow to run on edit, so its a better fit for the project’s testing pipeline (and, much love to Hatch for setting up MyPy and pytest). Run MyPy typechecking from your shell by calling:
hatch run types:check
Using IPython as the Emacs Python shell
To use IPython as your project’s shell first add it to the project’s dependencies:
[tool.hatch.envs.default]
dependencies = [
"ipython>=8.0",
...
]
Invoke IPython by calling hatch run ipython
. To have Emacs use this
project’s IPython as the default interpreter, set
python-shell-interpreter
and python-shell-interpreter-args
using
.dir-locals.el
:
(nil . ((python-shell-interpreter . "hatch")
(python-shell-interpreter-args
. "run ipython -i --simple-prompt --InteractiveShell.display_page=True")))
Running Org Babel Python blocks
For data analysis I set up an org notebook running against some Python dependencies. To run an org babel block within a project’s environment:
(org-mode . ((org-babel-python-command . "hatch run python")))
Putting it all together
To put it all together, drop this file into your new project’s root as
.dir-locals.el
:
((python-mode
. ((python-pytest-executable . "hatch run pytest")
(eval . (add-to-list
'eglot-server-programs
`((python-mode python-ts-mode) .
,(eglot-alternatives
'(("hatch" "run" "pyright-langserver" "--stdio"))))))))
(org-mode . ((org-babel-python-command . "hatch run python")))
(nil . ((python-shell-interpreter . "hatch")
(python-shell-interpreter-args
. "run ipython -i --simple-prompt --InteractiveShell.display_page=True"))))
And, add IPython to the Hatch project’s dependencies (feel free to adjust the version pin):
[tool.hatch.envs.default]
dependencies = [
"ipython>=8.0",
...
]
Feedback welcome!
I’ll update this with any further best practices so please do reach out to me with any tips and feedback.
Some missing elements:
- Auto-formatting (black or ruff)
- Jupyter notebooks