Configuring Eglot for Python Development in Emacs: Integrating python-lsp-server (pylsp) with Linters and Formatters

Eglot provides built-in LSP support in modern Emacs, giving developers a native interface for autocompletion, linting, and formatting. When paired with python-lsp-server (pylsp), creating a Python development environment comes down to managing the LSP server configuration properly.

Historically, setting up this toolchain required wiring together a stack of individual utilities such as flake8 and isort. Now, ruff stands entirely apart from that legacy ecosystem by consolidating all of those separate checks into a single, high-performance Rust binary. This article demonstrates how to configure Eglot to detect and use Ruff dynamically when it is present on your system, while retaining a clean fallback to the older stack of tools like Flake8, pycodestyle, and pydocstyle when Ruff is missing.

The configuration strategy

The configuration provided in this article begins by checking for the presence of ruff and flake8 in your system path. Based on these checks:

  1. If Ruff is installed, the configuration relies on the python-lsp-ruff plugin. Ruff consolidates linting, formatting, and import sorting.
  2. If Ruff is missing but Flake8 is present, Flake8 takes over linting duties alongside Pylint.
  3. If neither is installed, the system falls back to the individual pylsp plugins (pyflakes, pycodestyle, mccabe, etc.).

Dependencies

The dependencies can be installed locally pip or directly via your system's package manager.

The complete configuration

Below is the complete Emacs Lisp code to achieve this dynamic configuration. You can place this snippet in one of your init files:

;; Use ruff when it is available because it is fast (written in Rust). When ruff
;; is not available, fall back to flake8 and its individual underlying tools.
;;
;; URL: https://www.jamescherti.com/emacs-python-dev-using-eglot-pylsp-ruff-pylint-flake8/
;;
;; To sup up:
;; - When ruff is available: Ruff, and Pylint.
;; - When Ruff is not available: Flake8, isort, and Pylint.
;;
;; Documentation:
;; https://github.com/python-lsp/python-lsp-server/blob/develop/CONFIGURATION.md
;; https://github.com/python-lsp/python-lsp-ruff
;; https://github.com/chantera/python-lsp-isort
(let* ((has-ruff (executable-find "ruff"))
       (has-flake8 (executable-find "flake8")))
  ;; Target ONLY the 'pylsp key in the global configuration alist safely
  (setf (alist-get 'pylsp (default-value 'eglot-workspace-configuration))
        `(:pylsp
          (:plugins
           (;; Plugin: https://github.com/python-lsp/python-lsp-ruff
            :ruff (;; Ruff configuration
                   :enabled ,(if has-ruff t :json-false)

                   :formatEnabled ,(if has-ruff t :json-false)

                   ;; Add 'W' (pycodestyle warnings), 'UP' (pyupgrade),
                   ;; and 'D' (pydocstyle).
                   :extendSelect ["W" "UP" "D"]

                   ;; Ignore specific rules
                   ;;   D213: Multi-line docstring summary should start on
                   ;;         the second line.
                   ;;   D202: No blank lines allowed after function
                   ;;         docstring.
                   ;; :ignore ["D213" "D202"]
                   )

            ;; Pylint remains enabled regardless of whether Ruff
            ;; or Flake8 is active because it serves
            ;; complementary role.
            :pylint (:enabled t)

            ;; Flake8 is a wrapper tool that bundles pyflakes,
            ;; pycodestyle, and mccabe.
            :flake8 (:enabled ,(if (and (not has-ruff) has-flake8)
                                   t
                                 :json-false))

            ;; When Flake8 or Ruff runs, they execute these under
            ;; the hood. If we enable either, we must explicitly
            ;; disable the individual pylsp plugins for them,
            ;; otherwise the language server will run the exact
            ;; same checks twice and duplicate all editor
            ;; diagnostics.
            :mccabe (:enabled ,(if (or has-ruff has-flake8)
                                   :json-false
                                 t))
            :pyflakes (;; pyflakes catches logical errors
                       ;; (unused imports, undefined names...)
                       :enabled ,(if (or has-ruff has-flake8)
                                     :json-false
                                   t))

            :pycodestyle (;; pycodestyle catches style/formatting
                          ;; violations (PEP 8)
                          :enabled ,(if (or has-ruff has-flake8)
                                        :json-false
                                      t)

                          ;; Ignore specific rules
                          ;; :ignore ["W293"]
                          )

            :pydocstyle (;; pydocstyle enforces PEP 257 docstring
                         ;; conventions
                         :enabled ,(if (or has-ruff has-flake8)
                                       ;; Use flake8-docstrings
                                       ;; https://github.com/pycqa/flake8-docstrings
                                       :json-false
                                     t)

                         ;; Ignore specific rules
                         ;;   D213 Multi-line docstring summary should start on
                         ;;        the second line.
                         ;;   D202 No blank lines allowed after function
                         ;;        docstring.
                         ;; :ignore ["D213" "D202"]
                         )

            ;; Formatting: isort
            ;; https://github.com/chantera/python-lsp-isort
            :isort (:enabled ,(if has-ruff :json-false t))

            ;; Formatting: autopep8
            :autopep8 (:enabled ,(if has-ruff :json-false t))
            :yapf (:enabled :json-false)

            ;; Code completion
            :jedi_completion (;; jedi configuration
                              :enabled t

                              ;; Disable resolving documentation details eagerly
                              ;; :eager t

                              ;; Add class objects as a separate completion item
                              ;; :include_class_objects t

                              ;; Add function objects as a separate completion item
                              ;; :include_function_objects t

                              ;; Auto-complete methods and classes for each parameter
                              ;; :include_params t

                              ;; Fuzzy matching for typos/abbreviations
                              ;; :fuzzy t

                              ;; Modules for which labels and snippets should be cached.
                              ;; :cache_for ["pandas", "numpy", "tensorflow", "matplotlib"]

                              ;; How many labels and snippets should be resolved?
                              ;; :resolve_at_most 25
                              ))))))

This Eglot configuration prioritizes Ruff to handle linting and formatting when it is available in the environment. If Ruff is missing, it falls back to Flake8 and the default pylsp plugins. This keeps Emacs Eglot adaptable across different machines, guaranteeing consistent diagnostics and autocompletion without demanding a strict set of dependencies on every system you use.

Leave a Reply

Your email address will not be published. Required fields are marked *