Python Programming in Emacs

Making a lightweight and fast Python IDE in Emacs.

Page content

Python is one of my programming languages I’m using for both work and home life – no doubt, I’d want to use Emacs as an IDE for Python programming. There is a certain set of features we’d expect to have in any IDE: 1) code completion, 2) code navigation, 3) error checking.

Thanks to the fact that both eglot, the Emacs client for the Language Server Protocol (LSP), and tree-sitter, a powerful parsing library, are now – since version 29 – the parts of Emacs, It should not be too hard to construct a lightweight and fast Python IDE.

Choosing a Language Server

There are a number of the language servers for Python available these days – pylsp, pyls, pyright, jedi-language-server. After a series of tests, I’ve settled on pyright, an open syntax and type checker from Microsoft (sic!) used in Visual Studio Code, the fast and reliable one, as for me.

The Pyright documentation – a bit weird – claims that Pyright is just static type checker for Python. However, as we’ll see later, Pyright can do much more than just type checks.

A note on the Tree-Sitter package

The tree-sitter package is optional – I’m using it for pretty-looking source code fontification mainly. However, if you want to use it you have to pay attention to the following quote from the Mickey Petersen’s ‘Mastering Emacs’ book:

And in Emacs 29, support for tree-sitter is built in. Sort of. It’s an optional extra, so you must compile Emacs from source, or hope that someone else will do it for you.

Neither tree-sitter nor Emacs come installed with language grammars. Just like kids' toys and batteries, they’re sold separately. So you’re required to download and compile the sources for each language grammar you want to use.

– Mastering Emacs by Mickey Petersen.

If you are – like me – on Linux, the best way to setup language grammars is to compile them from the source code; you can find all grammars available on the tree-sitter repository. More details at How to Get Started with Tree-Sitter.

Python prerequisites

First of all, we need to install the following Python packages – pyright (the language server) and jsonrpc (the transport layer for the LSP).

[p3] $ pip install pyright
[p3] $ pip install jsonrpc

I’m running Python in the virtual environment, “p3” in my case.

Error checking with Pyright

Eglot automatically finds a suitable LSP – Pyright in our case – when you are opening a Python file. I’m using the default Pyright settings, no extra tuning required.

Pyright LSP with Flymake

Flymake is a minor Emacs mode performing on-the-fly syntax checks. I’m using the flymake built-in Emacs package as an Eglot front-end for syntax checks.

(require 'flymake)

My custom my/flymake-toggle-diagnostics-buffer function toggles the Flymake diagnostics buffer window.

If there is the Flymake diagnostics buffer associated with a file in the current buffer, it shows the diagnostics buffer in the new window and switches to it. If the current buffer is the Flymake diagnostics buffer, it closes it. Otherwise it just shows the error message.

(defun my/flymake-toggle-diagnostics-buffer ()
  (interactive)
  ;; Check if we are in the diagnostics buffer.
  (if (string-search "*Flymake diagnostics" (buffer-name))
      (delete-window)
    (progn
      ;; Activate the Flymake diagnostics buffer.
      ;; and switch to it
      (flymake-show-buffer-diagnostics)
      (let ((name (flymake--diagnostics-buffer-name)))
        (if (get-buffer name)
            (switch-to-buffer-other-window name)
          (error "No Flymake diagnostics buffer found")
          )))))

I’ve bound this function to the F7 key globally since I’m using F7 in other programming modes as well.

(global-set-key [(f7)] #'my/flymake-toggle-diagnostics-buffer)

;; Additional bindings.
(global-set-key (kbd "C-c f b") #'flymake-show-buffer-diagnostics)
(global-set-key (kbd "C-c f p") #'flymake-show-project-diagnostics)

The screenshot below shows what happens if you press F7 while editing a Python file managed by Eglot + Flymake.

Click or tap to view the full-size picture.

As you can see, Pyright can do much more than just type checks.

Code completion

I’m using the company package, a modular in-buffer completion framework for Emacs, as an Eglot front-end for code completion; it integrates with Eglot seamlessly.

;;;; `COMPANY'
(use-package company
  :ensure t
  :config
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 2)
  (setq company-show-numbers t)
  ;; To prevent default down-casing.
  ;; https://emacs.stackexchange.com/questions/10837/how-to-make-company-mode-be-case-sensitive-on-plain-text
  (setq company-dabbrev-downcase nil)
  ;; 2023-01-13 From a Reddit post on mixed case issue.
  (setq company-dabbrev-ignore-case nil)
  (setq company-dabbrev-code-ignore-case nil))

;; Use `company' everywhere.
(add-hook 'after-init-hook 'global-company-mode)

Click or tap to view the full-size picture.

Code navigation

I’m using the default key bindings for

xref-find-definitions
M-. to find the definition of the identifier at point.
xref-go-back
M-, to return back to where you invoked the xref-find-definitions command.
xref-find-references
M-? to find references to the identifier at point.

These functions are now powered by Eglot.

Final notes

I can recommend to force using Eglot for Python explicitly –

(require 'eglot)
(add-hook 'python-mode-hook 'eglot-ensure)

A note for Tree-Sitter users

If you are using Tree-Sitter for Python, it’s recommended to remap the python-mode to the Tree-Sitter specific python-ts-mode.

(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))

If this is the case, do not forget to force using Eglot for this mode as well.

(add-hook 'python-ts-mode-hook 'eglot-ensure)

Happy emacsing!