Emacs Eglot for Python with .dir-locals.el, python-lsp-server (Pylsp), and ruff

James Cherti

5 min read Original article ↗

Setting up a Python development environment in Emacs with Eglot and python-lsp-server (pylsp) requires telling the language server which plugins to run and which ones to ignore.

While you can set eglot-workspace-configuration globally, doing so forces every Python project you open using Emacs to use the same toolchain and formatting rules.

Placing the configuration in a .dir-locals.el file applies these Emacs buffer settings and Eglot workspace settings to files within the project's directory tree. The indentation settings and fill-column can be chosen to match the conventions configured in pyproject.toml, while Ruff reads its own configuration from that file when applicable.

The .dir-locals.el configuration for Ruff

Place the following .dir-locals.el file in the root of your Python project:

;;; Directory Local Variables for a Python project -*- no-byte-compile: t; -*-
;; Author: James Cherti
;; License: MIT
;; URL: https://www.jamescherti.com/emacs-eglot-python-dir-locals-el-and-pylsp/
;;
;; Requires:
;;   - python-lsp-server: https://github.com/python-lsp/python-lsp-server
;;   - Checkers:
;;       - ruff: https://github.com/astral-sh/ruff
;;       - pylint: https://github.com/pylint-dev/pylint
;;   - Pylsp Plugins:
;;     - python-lsp-ruff: https://github.com/python-lsp/python-lsp-ruff
;;     - pylsp-mypy: https://github.com/python-lsp/pylsp-mypy
;;
;; 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

((nil . ((sentence-end-double-space . nil)
         (require-final-newline . t)))
 (python-base-mode
  . ((indent-tabs-mode . nil)
     (tab-width . 4)
     (fill-column . 88)
     (python-indent-offset . 4)
     (python-indent-guess-indent-offset . nil)
     (eglot-workspace-configuration
      . (:pylsp
         (;; Formatter to use for reformatting signatures in docstrings.
          :signature
          (;; Signature
           :formatter "ruff"
           :include_docstring t
           :line_length 88)
          :plugins
          (:ruff
           (;; Ruff plugin (Requires: ruff and python-lsp-ruff)
            :enabled t
            :formatEnabled t
            :lineLength 88
            ;; I: Sort imports
            :extendSelect ["I"]
            :format ["I"])

           :pylsp_mypy
           (;; Type checking (Requires: pylsp-mypy)
            :enabled t

            ;; Disabling live mode prevents type-checking updates on every edit.
            ;; Save the file to refresh mypy diagnostics.
            :live_mode :json-false

            ;; :dmypy :json-false
            ;; :report_progress t
            )

           :pylint
           (;; Pylint
            :enabled t
            ;; :args []
            )

           :flake8 (:enabled :json-false)
           :mccabe (:enabled :json-false)
           :pyflakes (:enabled :json-false)
           :pycodestyle (:enabled :json-false)
           :pydocstyle (:enabled :json-false)
           :isort (:enabled :json-false)

           :autopep8 (:enabled :json-false)
           :yapf (:enabled :json-false)

           :preload (:enabled :json-false)
           :rope_autoimport (:enabled :json-false)
           :rope_completion (:enabled :json-false)

           :jedi_completion
           (;; Jedi code completion
            :enabled t

            ;; Adds class objects as a separate completion item.
            ;; :include_class_objects t

            ;; Adds function objects as a separate completion item.
            ;; :include_function_objects t

            ;; Auto-completes methods and classes with tabstops for each
            ;; parameter.
            ;; :include_params t

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

            ;; Resolve documentation and detail eagerly.
            ;; :eager :json-false

            ;; Disable fuzzy when requesting autocomplete.
            ;; :fuzzy :json-false
            ))))))))

Targeting python-base-mode in .dir-locals.el ensures the configuration applies automatically whether you use the standard python-mode or python-ts-mode.

Note: Ensure python-lsp-server and python-lsp-ruff are installed in your Python environment.

Dependencies

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

Why is ruff is a better linter and formatter

Historically, maintaining code quality in a Python project required assembling and configuring a long list of single-purpose tools. Ruff replaces almost all of them with a single binary written in Rust.

Ruff is designed for high-performance Python linting and formatting, and it is generally much faster than traditional Python linters. This can improve the responsiveness of on-the-fly diagnostics and formatting in Emacs. Actual performance depends on project size and the other enabled language-server plugins.

When integrated into pylsp via python-lsp-ruff, Ruff replaces the following tools and plugins:

  • flake8, pyflakes, and pycodestyle: Syntax verification, unused variable detection, and PEP 8 style enforcement.
  • black, autopep8, and YAPF: Automated code formatting.
  • isort: Alphabetical and sectioned import sorting.
  • McCabe: Cyclomatic complexity analysis.
  • pydocstyle: PEP 257 docstring convention checks.

Consolidating these tools into Ruff also means you only need to maintain a single configuration block in your pyproject.toml file instead of scattering settings across .flake8, setup.cfg, and .isort.cfg...

Why keep Pylint enabled alongside ruff?

Ruff does not implement every Pylint check. Keeping Pylint enabled can provide additional diagnostics for issues that Ruff does not currently detect, although this comes with additional execution cost.

Forcing pylsp selection in Eglot

Eglot supports several Python language servers out of the box and will automatically connect to the first one it finds in your environment.

If multiple servers are installed, Eglot might pick an alternative server instead of your preferred choice. To ensure pylsp is the server that is selected over any alternatives, add the following to your Emacs init file:

(with-eval-after-load 'eglot
  (setf (alist-get 'python-mode eglot-server-programs) '("pylsp"))
  (setf (alist-get 'python-ts-mode eglot-server-programs) '("pylsp")))

Packages like flymake-ruff and ruff-format are now redundant

If your Eglot setup already runs Ruff, Emacs packages like flymake-ruff and ruff-format become redundant.

You can handle auto-formatting Python buffers on save directly through Eglot. Read: Configuring Emacs Eglot to auto format code on save.

Flycheck is also unnecessary, as Eglot integrates directly with built-in Flymake out of the box.

Running eglot, pylsp, python-lsp-ruff, and jedi (code completion and xref definitions) provides the cleanest setup.

Fallback .dir-locals.el for projects not using ruff

If you are working on an existing codebase that has not migrated to Ruff and relies on the traditional Python tooling stack, you can invert the plugin configuration.

The following .dir-locals.el disables Ruff and enables the standard suite of pylsp plugins:

;;; Directory Local Variables for a Python project -*- no-byte-compile: t; -*-
;; Author: James Cherti
;; License: MIT
;; URL: https://www.jamescherti.com/emacs-eglot-python-dir-locals-el-and-pylsp/
((nil . ((sentence-end-double-space . nil)
         (require-final-newline . t)))
 (python-base-mode
  . ((indent-tabs-mode . nil)
     (tab-width . 4)
     (python-indent-offset . 4)
     (fill-column . 79)
     (eglot-workspace-configuration
      . (:pylsp
         (:plugins
          (:ruff
           (;; Ruff plugin
            :enabled :json-false
            :formatEnabled :json-false)

           ;; Note: If you enable Flake8, disable mccabe, pyflakes, and pycodestyle.
           :flake8 (:enabled :json-false)
           :mccabe (:enabled t) 
           :pyflakes (:enabled t)
           :pycodestyle (:enabled t)

           :pylint (:enabled t)
           :pydocstyle (:enabled t)
           :isort (:enabled t)
           
           :autopep8 (:enabled t)
           :yapf (:enabled :json-false)

           :jedi_completion
           (;; Jedi code completion
            :enabled t))))))))

Note: The configuration above requires installing python-lsp-isort. Additionally, :yapf remains disabled so it does not conflict with :autopep8 during formatting.

Similar articles