Emacs + Python + Hatch

Posted on May 5, 2024

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