Advanced Python Development Workflow in Emacs
Introduction
Why Emacs?
Emacs is not just an editor — it’s an extensible platform for crafting
highly personalized development environments. With tools like company
,
yasnippet,
lsp-mode
and dap-mode
, Emacs transforms into a feature-rich
Python IDE capable of competing with PyCharm or VS Code.
This guide explores how to evolve Emacs into a robust Python IDE. Whether you’re writing simple scripts or managing large-scale projects, you’ll find a setup that aligns with your engineering needs and reflects the power of Emacs customization.
Goals and Scope
The purpose of this guide is to meticulously explore how Emacs can be transformed into a advanced Python IDE. This is not a universal manual intended for everyone — it is deeply tailored to my specific needs, preferences, and engineering rigor. Throughout this document, I aim to combine practical usability with transparent configuration, balancing power and simplicity.
This guide reflects my personal journey in creating an IDE that meets my own standards of excellence. Could Emacs be made “better”? Absolutely, because the concept of “better” is always contextual. Could someone else choose different tools than I did? Certainly. The tools I selected represent the choices I made based on my criteria and understanding, which I will detail throughout the guide.
At its core, this is a collection of my thoughts, experiments, and findings — a snapshot of my current understanding of how to make Emacs the ultimate Python development environment. While it may not suit everyone, I hope it inspires others to refine their workflows and explore the potential of Emacs as a highly customizable IDE.
A note on some conscious exclusions: I do not use use-package
in my
personal Emacs configuration and this guide. While it may be a popular
choice in the Emacs community, I have not found it particularly
valuable for my workflow. I manage my configuration manually, and this
approach works perfectly for me. If you are a use-package
user, you
might need to adapt discussed configuration to fit your own setup — I
leave this as an exercise for you.
Similarly, you will not find anything related to evil-mode
or its
keybindings in this guide. If I want to use Vim, I simply use Vim. I
see no reason to mimic Vim within Emacs, as I prefer to embrace Emacs
for what it is.
Overview of the Configuration Stack
Modern IDEs go beyond text editing — they’re ecosystems that support coding, debugging, and managing environments seamlessly. Transforming Emacs into an IDE tailored for Python requires a stack of modular tools, each addressing a specific aspect of the development workflow:
- Code Intelligence: Autocompletion, navigation, and type checking enhance productivity and help catch errors early.
- Linting and Static Analysis: Real-time feedback ensures code adheres to best practices and avoids common pitfalls.
- Debugging: Integrated debugging tools streamline the process of finding and fixing issues.
- Environment Management: Seamless management of Python environments and dependencies simplifies setup and reduces context switching.
By strategically combining these tools, we can craft a Python IDE that adapts to diverse project demands — from quick scripts to complex applications.
Foundations of Key Protocols
Before delving deeper into the tools and their configurations, we have to understand some foundational concepts and protocols that underpin the technologies used in this guide. This section provides a brief overview of two critical protocols: Language Server Protocol and Debug Adapter Protocol. These protocols form the backbone of modern IDEs, enabling modularity, flexibility, and seamless integration across various tools.
Language Server Protocol
The Language Server Protocol (LSP)1 was introduced by Microsoft in 2016 as a way to standardize communication between code editors (or IDEs) and language-specific tooling. Prior to LSP, every editor had its own method of integrating language support, leading to fragmentation, duplicated effort, and slower progress in tooling development. The following image summarizes the functionality of the LSP:
LSP solves these issues by decoupling the editor and the language tooling into two distinct components:
- Editor (Client): The interface that the developer interacts with (e.g., Emacs, Visual Studio Code, Neovim).
- Language Server: A standalone program that provides features such as autocompletion, type checking, navigation, and refactoring for a specific programming language.
These components communicate using JSON-RPC over a network or local connection. For example, the client might request “Provide autocompletions at line X, column Y”, and the server responds with suggestions.
In the context of Emacs and Python:
- Language Server: Pyright2 (written in TypeScript) — the one of existing implementations of Language Servers for Python, — provides functionality like autocompletion, type checking, diagnostics, refactoring and code navigation. It works independently of the editors, communicating with clients via the standard LSP protocol.
- Client: Packages such as
lsp-mode
3 implement the client side of the LSP protocol in Emacs.lsp-mode
facilitate communication with the server (e.g., “find the definitions of the function”) and handling responses from the server (e.g., “here are the function definitions in file X, line Y”).lsp-mode
also rendering output (e.g., error highlights, documentation tooltips). - Wrapper: In this guide, I will focus on the Pyright language server,
with
lsp-pyright
4 as its wrapper. This package acts as an intermediary layer betweenlsp-mode
and the Pyright server. It configures and launches the Pyright Node.js package as a language server. Additionally,lsp-pyright
provides Python-specific configurations, such as setting up virtual environment paths, types, and modules. Essentially, it serves as a bridge between Emacs and Pyright.
In general, we get the following architecture:
- Emacs (
lsp-mode
): The client-side component. Sends requests to the server and displays the results. - Emacs (
lsp-pyright
): The auxiliary layer. Handles the setup and configuration of Pyright. - Node.js (Pyright): The server-side component. Processes requests, analyzes Python code, and returns the results to the client.
Let’s consider a small example that demonstrates how this architecture works:
- The user edits a Python file in Emacs.
lsp-mode
sends a request throughlsp-pyright
to the Pyright server: “What is the type of variable X?”- Pyright analyzes the code and returns the type of the variable.
lsp-mode
displays the result to the user in the minibuffer or on hover.
In this architecture LSP servers are editor-independent. Pyright can
be used not only in Emacs but also in VS Code or Neovim. In other
hand, lsp-pyright
allows you to easily switch to another server (e.g.,
pylsp
) without altering the client logic. And at the end, the code
analysis is offloaded from the editor, as the server runs in a
separate process.
Debug Adapter Protocol
Before Debug Adapter Protocol (DAP)5 was introduced, developers and teams creating IDEs and editors faced significant challenges in implementing debugging support. Each IDE had to individually integrate debugging for every programming language, leading to substantial duplication of effort and inefficiencies.
For instance, to support languages like Python, C++, and Java, IDE
developers needed to integrate distinct debuggers such as gdb
for C++
or pdb
for Python. This required an understanding of the internal
protocols for each debugger, resulting in:
- Slow adoption of new languages in editors.
- Lack of standardization: Each IDE or tool implemented its own debugging solutions.
- Integration challenges with existing tools and ecosystems.
The following image visualizes the issue:
To address these problems, Microsoft introduced DAP while working on Visual Studio Code (VS Code). The concept was to provide a unified protocol for communication between IDEs and debuggers via a standardized API.
DAP was designed to solve several key issues:
- Reducing effort for debugging support across languages:
- IDE developers need to implement support for DAP, not individual debuggers.
- Language developers create a Debug Adapter to interface with their debugger, avoiding the need to support multiple IDEs.
- Standardization and universality:
- Debuggers for any programming language can connect to any IDE supporting DAP.
- Modularity:
- IDEs and debuggers can evolve independently. Updates to a debugger do not require IDE code changes.
- Remote debugging support:
- DAP is designed to work both locally and remotely, simplifying integration with cloud and distributed environments.
The following image visualizes the solution:
DAP defines a JSON-based protocol for communication between two parties:
- Editor (Client): For example, VS Code or Emacs sends commands such as “set a breakpoint” or “start execution”.
- Server: A Debug Adapter for a specific debugger (e.g.,
debugpyfor Python) processes the commands and responds.
In the Emacs-Python ecosystem, the tools play the following roles:
- Server: A Python debugger, such as
debugpy
6, acts as the server-side component. It processes requests from the DAP client (e.g., “start code execution”) and returns results (e.g., “current line of execution: 15, value of variable X: 42”). The server manages the entire debugging process, including:- Setting breakpoints.
- Stepping through code.
- Displaying the program’s current state.
- Client:
dap-mode
7 serves as the client implementation of DAP in Emacs. It configures and manages debugging sessions, sends requests to the server (e.g., “add a breakpoint” or “execute the next step”), and presents information (call stacks, variable values, current execution line) to the user.dap-mode
provides an interface for working with various language debuggers using the standardized DAP protocol. - Wrapper:
dap-python
(part ofdap-mode
package) acts as an intermediary layer betweendap-mode
and specific debuggers likedebugpy
. It simplifies Python-specific configurations such as:- Selecting the interpreter.
- Passing script arguments.
- Configuring source paths. This makes launching Python debuggers straightforward while adhering to the DAP standard.
In general, we get the following architecture:
- Emacs (
dap-mode
): Client-side component. Manages the debugging interface and sends requests to the server. - Emacs (
dap-python
): Auxiliary layer. Configures the debugger server (e.g.,debugpy
) for Python. - Debugger (
debugpy
): Server-side component. Executes debugging commands and provides data to the client.
Let’s consider a small example that demonstrates how this architecture works:
- The user initiates a debugging session for a Python script in Emacs
via
dap-mode
. dap-mode
, throughdap-python
, launches the debugger server (debugpy
) with the specified configuration.dap-mode
sends requests to the server:- “Set a breakpoint at line 10”.
- “Run the script until the next breakpoint”.
debugpy
processes the requests:- Sets the breakpoint.
- Runs the script, stopping at line 10.
- Returns the current call stack and variable values.
dap-mode
displays this information to the user within Emacs.
In this architecture DAP servers are independent of editors. For
example, debugpy
can be used with VS Code, Emacs, or Neovim. In other
hand, dap-python
allows seamless switching between debuggers (e.g.,
using PyCharm’s debugger instead of debugpy
). Debugging via DAP
integrates naturally with other Emacs capabilities, such as lsp-mode
,
creating a cohesive development environment.
Integrating Components into a Unified Workflow
With a solid understanding of the foundational protocols (LSP and DAP), we can now explore how to integrate these tools into a cohesive development environment in Emacs. While each component serves a specific purpose, the true power of this setup lies in their seamless collaboration.
The following sections will demonstrate how these components work together to create an efficient Python IDE:
- Code Intelligence (LSP):
lsp-mode
acts as the backbone of our configuration, providing diagnostics, navigation, and refactoring capabilities via Pyright, our chosen Python Language Server. - Real-time Feedback:
flymake
integrates withlsp-mode
to ensure real-time linting and error highlighting during development. - Autocompletion:
company
leverages the capabilities oflsp-mode
andyasnippet
to provide intelligent code suggestions and template expansions. - Debugging (DAP):
dap-mode
anddap-python
enable interactive debugging sessions, allowing you to set breakpoints, inspect variables, and step through code—all within Emacs. - Environment Management:
direnv
andenvrc
automate the activation of virtual environments, ensuring your Python projects remain consistent and reproducible.
By strategically combining these tools, we can achieve a powerful and flexible development setup that adapts to the demands of both small scripts and large-scale Python projects.
Each of these tools will be explained in the sections below, detailing their role in the workflow, their purpose within our configuration, and the minimal setup required to achieve a cohesive, functional result. However, this guide does not aim to provide an exhaustive configuration for each package. For deeper customization tailored to your specific needs, I strongly encourage referring to the official documentation of each tool.
Additionally, it is important to note that the tools discussed here are not always the only options available in their respective domains. Often, you can substitute one tool for another based on your preferences or project requirements. This guide should not be viewed as a rigid framework but rather as a practical and quick way to achieve a comprehensive Python IDE setup in Emacs.
Environment Setup
Installing Dependencies
Before diving into Emacs-specific configuration, ensure that your
system is ready to support a robust Python development
workflow. Fortunately, lsp-mode
simplifies the process of setting up a
Python Language Server like pyright
. If the server isn’t already
installed, lsp-mode
automatically detects this and offers to install
it for you.
Here’s what happens when you open a Python project without an installed server:
lsp-mode
displays a message in the minibuffer:You’ll be presented with a list of compatible servers to choose from.Unable to find installed server supporting this file. The following servers could be installed automatically:
- After selecting a server, in our case
pyright
, and pressing Enter,lsp-mode
initiates the installation. Forpyright
, it runs something like this:You can track progress in the buffer named like/usr/bin/npm -g \ --prefix ~/.emacs.d/.cache/lsp/npm/pyright \ install pyright
*lsp-install: /usr/bin/npm*
. Once complete, the server connects to your project, andlsp-mode
starts providing its features. - In the
*Messages*
buffer, you’ll see something like this:LSP :: Download pyright started. Comint finished LSP :: Server pyright downloaded, auto-starting in 1 buffers. LSP :: Connected to [pyright:106136/starting ~/sampleproject]. LSP :: pyright:106136 initialized successfully in folders: (~/sampleproject)
In rare cases where lsp-mode
doesn’t offer to install pyright
, or if
the automatic process fails, you can install it manually. According to
the official documentation, there are two main ways to install
pyright
:
- Install the Node.js package in the user’s home directory: This is
the most official and feature-complete method. It provides a CLI
application that seamlessly integrates with
lsp-mode
. - Install the Python package: While this method might appeal to
Python purists, I see several drawbacks:
- If installed locally within a virtual environment, it adds yet another dependency to every project, which may not be ideal for workflows where dependencies are frequently removed and reinstalled for testing reproducibility.
- Installing it in the user’s home directory feels redundant when a more official, Node.js-based alternative exists.
Given these considerations, I propose the installation in the user’s home directory via Node.js. Here’s how you can do the same:
# Install pyright in the user's home directory using npm
NPM_CONFIG_PREFIX="$HOME/.local/" npm install -g pyright
Don’t forget to add "$HOME/.local/bin"
to your $PATH
to make it easier
for Emacs to find pyright
. Without modifying your $PATH
, it will still
work, but you’ll need to do additional configuration in your LSP setup
to specify the search path. For NPM_CONFIG_PREFIX
usage trick during
the installation with using -g
see corresponding documentation
page8.
I also suspect that in some Linux distributions or macOS, pyright
might be available as a package through the system’s package
manager. This could also work well, depending on your preferred
setup.
Managing Multiple Pythons
Managing multiple Python versions is a necessity for many developers. Often, you don’t want to use the latest Python release, primarily due to package requirements or compatibility constraints. There are several approaches to managing multiple Python versions, and the choice largely depends on your environment and preferences.
For example, if you’re using Gentoo, Portage provides access to
several older Python versions. Similarly, tools like Nix or Guix can
be pinned to specific commits containing the desired Python
version. However, for most users, the simplest and most flexible
method is to use pyenv
9.
pyenv
allows you to install and manage multiple Python versions
seamlessly. It also provides shims, which enable your shell to
automatically select the appropriate Python version based on the
current project directory. This functionality makes it a convenient
tool for managing Python installations.
That said, in my workflow, I only use pyenv for installing and managing Python versions. For selecting the Python version on a per-project basis, I prefer to rely on direnv10, which I’ll describe next.
Using direnv
direnv
is a language-agnostic tool that automatically enables or
disables environment variables depending on the current directory (and
its subdirectories). This approach eliminates the need to manually
manage Python versions for each project and streamlines the setup
process.
With direnv
, you can configure a project’s Python environment simply
by navigating into the corresponding directory. Unlike pyenv
shims,
which require additional configuration, direnv
handles everything for
you in a way that works not just for Python but for virtually any
programming environment.
In fact there is an extensive Community Wiki11 that covers almost
every setup you could want. I will not focus on the deep customization
of direnv
and show a basic example of how to set up direnv
for a
Python project:
Here’s a basic example of how to set up direnv
for a Python project:
-
At the root of your project, create a file named
.envrc
:export VIRTUAL_ENV=.venv layout python3
-
Enable
direnv
for this directory by executing the following:direnv allow
From this point on, commands like command -v python
or type -a python
will point to the local Python version specified in your project.
The beauty of this workflow lies in its simplicity and flexibility:
pyenv
ensures you can easily install any Python version you need.direnv
takes care of dynamically selecting the correct environment as you switch between projects, reducing the need for repetitive manual configuration.direnv
is language-agnostic, meaning it can be used not only for Python but for virtually any environment management scenario.
The example provided above is intentionally minimal and sufficient for
the scope of this guide. However, direnv
offers extensive
customization options, including advanced configurations that
integrate with pyenv
shims. For more complex workflows, I highly
recommend exploring the official documentation and experimenting with
the examples available in the community wiki.
By combining pyenv
and direnv
, you gain a robust and scalable approach
to managing multiple Python versions without the overhead of manual
setup.
Integrating direnv with Emacs
There are 2 Emacs extensions I know of for direnv
. There’s
envrc
12 and emacs-direnv
13. Both are good and
well-supported, but envrc sets environment variables
buffer-locally. It automatically applies environment variables
buffer-locally, ensuring project-specific configurations don’t leak
into other buffers. This means I can open files in different
projects, each with their own virtual env, and emacs will see
different PATH etc. For each file I have open. This means I can run
correctly isolated LSP sessions for each project.
Add this snippet to your Emacs configuration at the very bottom of your configuration:
(when (executable-find "direnv")
(add-hook 'after-init-hook #'envrc-global-mode))
This activates envrc-mode
for all programming modes, automatically
aligning Emacs with the active environment. Please note,
envrc-global-mode
should be enabled after other global minor modes,
since each prepends itself to various hooks.
IDE Setup
Autocompletion with company and lsp-mode
Autocompletion is a cornerstone of modern development workflows,
saving countless keystrokes and providing real-time insights into your
code. In this guide, autocompletion is achieved through the powerful
combination of company
14 and lsp-mode
.
The setup for basic autocompletion is remarkably simple:
- Install
company
. - Profit.
That’s it. No, seriously — this is not a joke. For the purposes of
this guide (and honestly, for most use cases), you don’t even need to
include (require 'company)
in your Emacs configuration. By simply
installing the package, autocompletion will work out of the box when
combined with lsp-mode
.
Diagnostics with flymake and flycheck
Emacs offers two prominent options for integrating diagnostics:
flycheck
15 and flymake
16. Both packages excel in their
respective domains, and choosing between them often depends on your
specific needs and workflow. Both tools provide an interface for
diagnostics suitable for our needs.
flycheck
specializes in external linters, offering a rich ecosystem of
integrations. Each language requires specifying the linters to use and
configuring them as needed. flycheck
maintains a comprehensive list of
checkers (flycheck-checkers
), making it good enough for workflows that
heavily rely on external tools like Flake8, Mypy, or PyLint.
flymake
focuses on modern language servers (LSP) and native integration
with the Emacs ecosystem. It provides a unified API for connecting
linters, particularly those compatible with LSP or standard formats
like JSON or XML. flymake
has been asynchronous since Emacs 26, making
it more suitable for real-time feedback.
In practice, both tools perform exceptionally well with Python projects. I’ve personally tested each in various setups and found them reliable, efficient, and capable of handling complex diagnostics seamlessly. Neither is “better” than the other; instead, they complement different workflows.
That said, if you prefer flymake
or don’t want to install any
additional packages, lsp-mode
supports both and will automatically
default to whichever is available. The behavior of diagnostics is
controlled by the lsp-diagnostics-provider
variable, which is set to
:auto
by default. This means lsp-mode
will prefer flycheck
if it’s
installed, falling back to flymake
otherwise. This behavior is
seamless and requires no additional configuration unless you
explicitly disable lsp-auto-configure
.
flycheck
is straightforward to set up, especially for Python
development. Begin by installing it via your preferred package
manager. To enable it globally, add the following to your
configuration:
(add-hook 'after-init-hook #'global-flycheck-mode)
For the purposes of this guide, global mode is unnecessary. Instead, I
delegate lsp-mode
to enable flycheck
itself where needed, in buffers
where lsp-mode
is enabled.
You can navigate to the next/previous erroneous regions using
flycheck-next-error
and flycheck-previous-error
or by using the
corresponding keyboard bindings C-c ! n
and C-c ! p
.
flymake
comes bundled with Emacs, requiring minimal setup. If you’re
using lsp-mode
, it will automatically enable flymake
unless you
explicitly configure it otherwise. For manual activation, add the
following hook:
(add-hook 'python-mode-hook #'flymake-mode)
You can navigate to the next/previous erroneous regions using
flymake-goto-next-error
and flymake-goto-prev-error
commands. It might
be a good idea to map them to M-n
and M-p
in flymake-mode
, by adding
to your init file:
(with-eval-after-load 'flymake
(define-key flymake-mode-map (kbd "M-n") 'flymake-goto-next-error)
(define-key flymake-mode-map (kbd "M-p") 'flymake-goto-prev-error))
Flymake’s tight integration with LSP makes it an excellent choice for those who prefer a native Emacs experience. Recent improvements ensure smooth, asynchronous diagnostics without impacting performance.
Enhancing Autocompletion with yasnippet
To complement the power of lsp-mode
and company
, yasnippet
17
provides ability to use predefined code snippets and templates for
repetitive structures. This tool can save time and reduce boilerplate
by allowing you to expand small triggers into full code blocks.
In this section, I’ll guide you through the minimal configuration
necessary to get started with yasnippet
. As always, I recommend
exploring its capabilities further to tailor it to your specific
needs, but here we’ll focus on the essentials.
Integrating yasnippet with company
After installing yasnippet
, there’s no need to clutter your
configuration with unnecessary options. Instead, let’s focus on a few
key points. In the previous section, I mentioned how company
works
right out of the box. But frankly, we probably want autocomplete to
use our snippets, don’t we?
Integrating yasnippet
with company
is straightforward. This would
allow snippet expansion to be part of the autocompletion
pipeline. Here’s how such an integration might look:
(with-eval-after-load 'company
(add-to-list 'company-backends '(company-capf :with company-yasnippet)))
While the concept of company-backends
is a bit more intricate (it’s
essentially a list of functions implementing a specific interface),
this simple addition suffices for our needs. This setup appends
company-yasnippet
to the company-capf
backend, enabling snippet
completions alongside LSP-driven suggestions. However, this
modification globally affects all buffers where company
is active,
which might not be ideal for workflows requiring specific
configurations for different modes.
For now, let’s defer configuring yasnippet
as part of
company-backends
. Instead, we’ll revisit this integration in a later
section where we craft a Python-specific setup. This approach allows
us to maintain a modular configuration that can be tailored precisely
to Python development needs.
Enabling yasnippet Globally?
Whether to enable yasnippet
globally depends on your workflow. To
enable it globally, simply call:
(with-eval-after-load 'yasnippet
(yas-global-mode 1))
For the purposes of this guide, global mode is unnecessary. Instead,
you can opt for a more targeted approach by reloading snippet tables
and enabling yasnippet
selectively in specific modes:
(with-eval-after-load 'yasnippet
(yas-reload-all))
To enable yasnippet
only in python-mode
, lets define a custom function
and add it to the relevant hook. We’ll reuse this hook a bit later for
other python-related setiings.
(defun setup-python-environment ()
"Custom configurations for Python mode."
;; Enable YASnippet mode
(yas-minor-mode 1))
;; Configure hooks after `python-mode` is loaded.
(add-hook 'python-mode-hook #'setup-python-environment)
This approach keeps your configuration modular and avoids loading unnecessary modes globally.
Creating a Sample Snippet
Now that we have yasnippet
set up, let’s create a simple snippet to
see it in action. Create a file at ~/.emacs.d/snippets/python-mode/def
with the following content:
# -*- mode: snippet -*-
# name: def
# key: def
# --
def ${1:function_name}(${2:args}):
${3:pass}
At this stage, I won’t delve into the syntax of snippets. You can
experiment with it later to create more complex templates. For now,
type def
in a Python buffer and press TAB
to expand it using
yas-expand
. Alternatively, you can type the first few characters, wait
for lsp-mode
to suggest def through company
, and select it with
TAB
. It’s that simple.
The goal of this configuration is to get you started
quickly. yasnippet
integrates seamlessly with lsp-mode
and company
,
and the minimal setup above is sufficient to demonstrate its
potential. As you grow more familiar with it, you can explore more
advanced snippet features and refine your workflow. For now, enjoy the
simplicity of having code snippets readily available at your
fingertips.
Set up LSP
This section covers one of the most exciting parts of the guide: configuring LSP for Python development in Emacs. With this setup, you’ll unlock roughly 70% of the features this guide aims to deliver, turning Emacs into a highly capable Python IDE.
Getting Started
To begin, ensure you have the following packages installed: lsp-mode
,
lsp-pyright
, and lsp-ui
18. These three packages form the backbone of our
LSP configuration.
Configuring the Keymap Prefix
One of the first and most helpful configurations is setting a keymap
prefix for lsp-mode
. If you struggle with remembering multiple
keybindings or simply prefer an accessible overview of available
commands, I recommend install and use the which-key
19 package.
which-key
is a minor mode that displays keybinding suggestions in a
popup as you type a prefix. When integrated with lsp-mode, it provides
a quick reference for all LSP-related commands.
By default, the lsp-mode
keymap prefix is s-l
(Super + L), which often
conflicts with system-wide shortcuts, such as screen locking on many
Linux desktop environments. To avoid this, I recommend changing it to
C-c l
, even though it conflicts with org-store-link
in
org-mode
. Personally, I find the excessive use of C-c <key>
bindings
for so many commands in org-mode
to be unjustified.
As stated in the manual:
A small number of keys are reserved for user-defined bindings, and should not be used by modes, so key bindings using those keys are safer in this regard. The reserved key sequences are those consisting of
C-c
followed by a letter (either upper or lower case), and function keysF5
throughF9
without modifiers.
Here’s how you can remap the prefix and reassign org-store-link
:
; Reassign org-store-link
(global-set-key (kbd "C-c o l") #'org-store-link)
(setopt lsp-keymap-prefix "C-c l")
Selecting the Correct Python Version
lsp-pyright
automatically attempts to locate the appropriate Python
executable for the current project using a series of search
functions. The default behavior includes:
- Searching for a virtual environment named
.venv
orvenv
in the parent directories. - Looking for a Python executable in your system’s
PATH
.
However, you can extend or customize this behavior. Earlier in this
guide, we configured direnv
to set the VIRTUAL_ENV
environment
variable. Let’s leverage this by adding a custom search function:
(defun my-locate-python-virtualenv ()
"Find the Python executable based on the VIRTUAL_ENV environment variable."
(when-let ((venv (getenv "VIRTUAL_ENV")))
(let ((python-path (expand-file-name "bin/python" venv)))
(when (file-executable-p python-path)
python-path))))
(with-eval-after-load 'lsp-pyright
(add-to-list 'lsp-pyright-python-search-functions
#'my-locate-python-virtualenv))
This addition ensures lsp-pyright
prioritizes the Python executable
specified by VIRTUAL_ENV
, speeding up its decision-making
process. This environemt was set by direnv
and proxied to Emacs by
envrc
mode. While optional, this example demonstrates the simplicity
and flexibility of extending lsp-pyright
. We will not use this
hook function in our example, it was just an example of how to extend
lsp-pyright-python-search-functions
.
At this point, you might think about explicitly setting variables such
as python-shell-interpreter
or others provided by different packages,
pointing them to the Python executable. Technically, you could add
something like this to your hook function:
(defun setup-python-environment ()
"Custom configurations for Python mode."
;; Enable YASnippet mode
(yas-minor-mode 1)
;; Set the `python-shell-interpreter' to the python in PATH.
;; At this moment `envrc' should successfully configure environment.
(setq-local python-shell-interpreter (executable-find "python")))
And this might even work. However, from my perspective, it’s better
not to touch the Python paths — neither here nor in any other variable
within this hook function — and instead leave them as they are,
relative. Tools like envrc
and others (discussed later) excel at
determining the base directory and environment upon switching to a
Python project.
The simplest way to verify that everything is working as expected is
to open a Python project with a virtual environment managed by
direnv
. Then, invoke the command M-x run-python
, and in the resulting
Python interpreter buffer, execute the following code:
>>> import sys
>>> print(sys.executable)
You should see output resembling this:
~/sampleproject/.venv/bin/python
Now, if you close the interpreter buffer, switch to a new Python
project with a virtual environment managed by direnv
, and repeat M-x run-python
and the code above, you will notice that the path to Python
changes. This dynamic adjustment indicates that everything is
configured correctly.
By avoiding manual intervention, you keep your configuration minimal
and less error-prone, while still allowing lsp-pyright
and related
tools to do their job seamlessly. In this spirit, I recommend
refraining from setting python-shell-interpreter
or any similar
variable from other packages manually and letting the ecosystem handle
it automatically.
Finally, if you want the LSP server to shut down after you close all
buffers associated with the server, set lsp-keep-workspace-alive
to
nil
.
Configuring LSP Completion
In Emacs, the current trend for completion logic is to leverage the
completion-at-point-functions
(CAPF) framework. This is a standardized
approach to completion that integrates with many major modes and
external tools, including those that support the LSP.
CAPF serves as the backbone for modern Emacs completion systems. It
provides a unified interface for completion suggestions and works
seamlessly with LSP-based tools. company-capf
acts as a bridge between
the company-mode
autocompletion system and the CAPF framework, making
it a popular and highly recommended backend for autocompletion.
By default, the company-backends
variable is configured as follows:
(company-bbdb company-semantic company-cmake company-capf company-clang company-files
(company-dabbrev-code company-gtags company-etags company-keywords)
company-oddmuse company-dabbrev)
This setup includes a wide range of backends, with company-capf
already present. However, this default configuration may introduce
redundant or less relevant backends for your use case, especially when
working with LSP.
To streamline the autocompletion process, you can customize company-backends to prioritize CAPF and reduce clutter from other backends. Here’s a minimal configuration example that will work for LSP-based workflows:
(setopt company-backends '((company-capf company-dabbrev-code)))
This setup is almost sufficient for many major modes that implement
CAPF support, including Python via lsp-mode
. But lets a bit improve
this configuration by adding yasnippet
support. To improve performance
and maintain clarity, we’ll define a modular and reusable approach to
backend configuration using a macro. Here’s how:
(defmacro company-backend-for-hook (hook backends)
`(add-hook ,hook (lambda ()
(set (make-local-variable 'company-backends)
,backends))))
This macro dynamically configures company-backends for any mode by attaching it to a specified hook. Using this approach ensures reusability and consistency across different languages and modes.
Now, we impove our setup-python-environment
hook function:
(defun setup-python-environment ()
"Custom configurations for Python mode."
;; Enable YASnippet mode
(yas-minor-mode 1)
;; Customize company backends for `python-mode'.
(company-backend-for-hook 'lsp-completion-mode-hook
'((company-capf :with company-yasnippet)
company-dabbrev-code)))
Here’s explanation:
lsp-completion-mode-hook
: Hook run after enteringlsp-completion-mode
company-capf
: Handles LSP-driven suggestions via CAPF.company-yasnippet
: Seamlessly integrates snippet expansion.company-dabbrev-code
: Acts as a fallback for suggestions based on buffer content.:with
helps to make sure the results from mode agnostic backends (such ascompany-yasnippet
) are returned without preventing results from context-aware backends (such ascompany-capf
).
CAPF support is enabled by default in lsp-mode
. The
lsp-completion-provider
is already set to :capf
, ensuring seamless
integration with company-capf
. No additional configuration is required
for this functionality.
At the end, we need to enable LSP support in Python buffers, by
enbling lsp-mode
as well as lsp-pyright
. As stated in documentation
all we need is to require lsp-pyright
and call lsp
(or
lsp-deferred
)function. Let’s extend our hook function for python-mode
:
(defun setup-python-environment ()
"Custom configurations for Pyton mode."
;; Enable YASnippet mode
(yas-minor-mode 1)
;; Setup active backends for `python-mode'.
(company-backend-for-hook 'lsp-completion-mode-hook
'((company-capf :with company-yasnippet)
company-dabbrev-code))
;; Enable LSP support in Python buffers.
(require 'lsp-pyright)
(lsp-deferred))
The difference between lsp
and lsp-deferred
function is that
lsp-deferred
will start the server when you switch to that buffer (on
demand).
Enhancing the UI
Modern development workflows benefit from visual cues that provide
context about the current file, project, and code structure. In Emacs,
this is achieved through breadcrumb navigation and other enhancements
provided by lsp-mode
and lsp-ui
.
Breadcrumb navigation in the headerline helps track the current file’s
location within a project, along with symbols at the current
point. This feature is part of lsp-mode
and is enabled via the
lsp-headerline-breadcrumb-mode
. You can customize the breadcrumb
segments displayed by modifying the lsp-headerline-breadcrumb-segments
variable.
For instance:
(setopt lsp-headerline-breadcrumb-segments
'(path-up-to-project ; Include directories up to the project root
file ; Include the open file name
symbols)) ; Include document symbols, if supported
To activate breadcrumb navigation, no additional steps are required
unless you explicitly disable lsp-auto-configure
. Otherwise you have
to enable it as follows:
(add-hook 'lsp-mode-hook #'lsp-headerline-breadcrumb-mode)
The lsp-ui
package augments the LSP experience with additional UI
modules, such as:
- Sideline Diagnostics: Display inline diagnostics for the current line.
- Peek Definitions: A dedicated cross-references popup.
- Doc Frames: Show documentation in a floating frame near the cursor.
By default, lsp-mode
automatically activates lsp-ui
unless
lsp-auto-configure
is set to nil
. All you need to enable it is to
install it.
You can customize lsp-ui
features20 to match your preferences. All of
this is optional and secondary of course. I suggest playing around
with these parameters to determine what works best for you.
One highly recommended adjustment is remapping xref-find-definitions
and xref-find-references
, which are bound to M-.
and M-?
by
default. These commands open definitions or references in a new
buffer, which can interrupt your workflow. With lsp-ui-peek
, you can
view definitions and references in a popup, allowing you to navigate
cross-references without leaving your current context. Here’s how to
remap the keys:
(with-eval-after-load 'lsp-ui
;; Remap `xref-find-definitions' (bound to M-. by default)
(define-key lsp-ui-mode-map
[remap xref-find-definitions]
#'lsp-ui-peek-find-definitions)
;; Remap `xref-find-references' (bound to M-? by default)
(define-key lsp-ui-mode-map
[remap xref-find-references]
#'lsp-ui-peek-find-references))
Integrating which-key with lsp-mode
To further enhance the usability of lsp-mode
, enable its built-in
integration with which-key
:
(with-eval-after-load 'lsp-mode
(add-hook 'lsp-mode-hook #'lsp-enable-which-key-integration))
This integration ensures that pressing the LSP keymap prefix (C-c l
in
our case) provides a helpful list of available commands. Of course,
you need to have the which-key
package installed to take full
advantage of this configuration. However, this setup is optional, and
you can skip it if you’re not a which-key
user or don’t understand its
purpose. Strictly speaking, this configuration is not a defining
feature of an IDE.
LSP Sessions and Worspaces
One of the final note for LSP core setup is regarding the variable
lsp-keep-workspace-alive
, which controls whether the LSP server
remains active after the last workspace buffer is closed. By default,
this variable is set to t
, meaning the server continues running even
after you close the last buffer associated with the server. You can
verify this by running the following command after closing all
project-related buffers:
ps -eo args | grep '[p]yright-langserver'
While I understand the cases where keeping the server alive might be beneficial, I find it inconvenient for my typical workflows. For this reason, I prefer to disable this behavior. This is not a mandatory configuration but rather a reflection of my preferences and habits. So, for the purposes of this guide, however, I’ll simply disable it:
;; Shut down LSP server after close all buffers associated with the server.
(setopt lsp-keep-workspace-alive nil)
One of the more intriguing capabilities of the LSP is its support for Multi-Root sessions. This feature allows a single language server instance to handle multiple projects (workspace folders) within the same session. It’s particularly useful in scenarios like working with monorepos or related projects where shared context between folders can enhance development efficiency.
In the case of lsp-pyright
, the variable lsp-pyright-multi-root
determines whether Pyright is permitted to serve multiple root folders
within a single server instance. This behavior is enabled by default,
meaning Pyright starts in Multi-Root mode and adds all relevant
projects to the current session.
While Multi-Root mode has clear advantages, it also introduces potential downsides:
- Pyright will load all specified directories in the session, even if you’re actively working on only one project. For large projects, this can significantly increase the initialization time of the LSP process, leading to slower startup and responsiveness.
- The server may index directories unrelated to the current task, consuming resources like CPU and memory without adding immediate value.
- Common configurations, such as the Python virtual environment, are
automatically applied across related projects. This means if you
open two Python projects, each with its own virtual environment, the
LSP server will use only one of them for both projects. This can
lead to:
- Tools like
flycheck
orflymake
might show errors or warnings based on the wrong set of dependencies or library versions. - Projects using different versions of the same library may cause the server to analyze the wrong version, leading to false positives or missed issues.
- Tools like
In my typical Python workflow, I rarely work with monorepos or tightly coupled projects that benefit from shared contexts. Instead, I prefer to open separate instances of Emacs for each project, allowing each one to spawn its own LSP server. My workstation has enough resources (ample memory and a powerful CPU) to handle multiple Python projects and LSP server instances simultaneously without noticeable performance degradation.
To disable Multi-Root mode in lsp-pyright
, you need to configure it
before the package is loaded:
;; This must be set before the package is loaded.
(setq lsp-pyright-multi-root nil)
;; Enable LSP support in Python buffers.
(require 'lsp-pyright)
(lsp-deferred)
Once you disable Multi-Root mode, it’s essential to clean up the
session data to ensure the changes take effect. Remove the file
~/.emacs.d/.lsp-session-v1
(is set by lsp-session-file
) and restart
Emacs or LSP mode. After this, you can verify the behavior by opening
two separate Python projects and running the command M-x lsp-describe-session
. This will display:
- You should see that only the relevant directory for each project is listed.
- Each project should have its own server process, identifiable by its unique Process ID (PID).
This setup ensures that each project operates independently, with its own configuration, dependencies, and diagnostics, aligning perfectly with a workflow where isolation between projects is essential condition.
While Multi-Root mode has its merits in specific use cases, such as monorepos, it doesn’t suit every workflow, in particular my own. By disabling it, you gain finer control over server behavior and avoid unnecessary cross-project interference. If you’re curious to explore its benefits or adjust your setup for a different workflow, I encourage you to delve deeper into the LSP documentation on Workspaces and Sessions to make an informed decision.
LSP Extensions
This section highlights optional extensions to the standard LSP setup, reflecting my preferences and demonstrating how you can enhance your workflow. Everything described here is entirely optional and depends on your habits and needs. Nothing is mandatory.
For users of consult
, the consult-lsp
21 package bridges the gap
with lsp-mode
, providing functionality similar to what Helm and Ivy
users enjoy. Below are a couple of features I’ve found particularly
useful, which you can use as inspiration for further exploration.
By default, lsp-mode
provides the lsp-treemacs-errors-list
command for
querying diagnostics. While functional, I find consult-lsp-diagnostics
more intuitive and aligned with the consult
workflow. Here’s how to
remap the diagnostic list command:
(with-eval-after-load 'lsp-mode
(define-key lsp-mode-map
[remap lsp-treemacs-errors-list]
#'consult-lsp-diagnostics))
Now, pressing C-c l g e
will bring up diagnostics in the familiar
consult
format, while lsp-treemacs-errors-list
remains accessible for
manual invocation if needed. Also, if you are flymake
user the
commands flymake-show-buffer-diagnostics
and
flymake-show-project-diagnostics
are designed for the same
purposes.
Another powerful feature of consult-lsp
is consult-lsp-symbols
, which
I find more natural than xref-find-apropos
. It provides an intuitive
interface for searching all meaningful symbols that match a given
pattern:
(with-eval-after-load 'lsp-mode
(define-key lsp-mode-map
[remap xref-find-apropos]
#'consult-lsp-symbols))
Now, pressing C-c l g a
allows you to find symbols effortlessly in the
consult
interface. Again, xref-find-apropos
remains accessible for
manual invocation if needed.
These are just two examples of how consult-lsp
can integrate into your
workflow. The package offers many more commands and features worth
exploring. I encourage you to dive deeper and experiment with its
capabilities to see what works best for you.
Debugging with dap-mode
Debugging in Emacs is made seamless with dap-mode
, a powerful
extension that integrates the DAP into your workflow. This section
provides a gentle introduction to setting up dap-mode
for Python
projects, showcasing how minimal configuration can get you started
quickly.
Setting Up dap-mode
To begin, you’ll need to install and enable dap-mode
. For a
straightforward setup, you can delegate most of the configuration to
the package itself by enabling the auto-configuration mode:
(setopt dap-auto-configure-mode t)
Or if you want to enable only specific modes instead:
(require 'dap-mode)
;; The modes below are optional
;; Displaying DAP visuals.
(dap-ui-mode 1)
;; Enables mouse hover support
(dap-tooltip-mode 1)
;; Use tooltips for mouse hover if it is not enabled `dap-mode' will
;; use the minibuffer.
(tooltip-mode 1)
;; Displays floating panel with debug buttons requies emacs 26+.
(dap-ui-controls-mode 1)
Configuring dap-python
After enabling dap-mode
on Emacs side we need to tune up the Python
specific settings, in our case dap-python
. First of all you need to
install the debugging tool that dap-python
will use. For Python, two
common options are debugpy
22 and the deprecated ptvsd
23. I
recommend using debugpy
as it is more advanced and actively
maintained. Install it in your project’s virtual environment with:
pip install debugpy
and to install ptvsd
instead of debugpy us the following command:
pip install "ptvsd>=4.2"
Note, ptvsd
is depracated, and as of 8/10/2022, ptvsd
caused dap to
break when it hits a breakpoint. For more details, refer to the
related discussion in dap-mode
issues tracker24.
To enable debugging for Python buffers, you’ll need to load the
dap-python
module and if you’re using debugpy
, make sure to set it as
the debugger explicitly.
One important detail: for dap-mode
to function correctly, ensure that
lsp-mode
is active in the buffer you’re trying to debug. Without it,
some necessary variables won’t be initialized, causing issues when
launching the debugger.
Let’s extend our hook function for python-mode
:
(defun setup-python-environment ()
"Custom configurations for Pyton mode."
;; Enable YASnippet mode
(yas-minor-mode 1)
;; Setup active backends for `python-mode'.
(company-backend-for-hook 'lsp-completion-mode-hook
'((company-capf :with company-yasnippet)
company-dabbrev-code))
;; Enable LSP support in Python buffers.
(require 'lsp-pyright)
(lsp-deferred)
;; Enable DAP support in Python buffers.
(require 'dap-python)
(setq-local dap-python-debugger 'debugpy)
(dap-mode 1))
This configuration adds Python-specific debugging templates to
dap-debug
, making it easy to get started.
Using dap-mode
Once your setup is complete, open a Python project, then any python
file and call the dap-debug
command. You’ll be prompted to choose a
debugger configuration template. For Python, the following templates
are provided by default (via dap-python
):
- Run file (buffer): Executes the currently opened buffer.
- Run pytest (buffer): Runs all tests in the current buffer.
- Run pytest (at point): Runs the test at point.
- Attach to running process: Attaches to an already running Python process.
- Run file from project directory: Executes a file from the project root.
A common use case is selecting Python :: Run file (buffer) to run the script in the current buffer. Note that this configuration doesn’t handle arguments, environment variables, or complex setups like executing a setuptools-based script. For such needs, you’ll have to create a custom template. To verify your setup, choose Python :: Run file (buffer) and launch the debugger. The script will execute, and you’ll notice additional windows appear in Emacs, indicating the debugger is active. However, since no breakpoints are set by default, the debugger will likely finish execution immediately. Even so, this confirms that the integration is working as intended.
Creating a Custom Debug Template
While the default templates in dap-python
cover common use cases,
there are scenarios where you need a tailored setup to handle
arguments, environment variables, or specific entry points. Creating a
custom debug template allows you to address these needs effectively.
Start by creating an Emacs Lisp file to hold your custom template. For
consistency and collaboration, I recommend create debug.el
and storing
this file in your project repository. This ensures that your teammates
can benefit from the same templates. Let’s assume you’re working on a
Python project with the following structure:
.
├── .envrc
├── .venv/
├── myapp
│ ├── cli.py
│ ├── __init__.py
│ └── __main__.py
└── tests/
In the root of this project (on the same level as tests
and myapp
),
create a file named debug.el
with the following content:
(dap-register-debug-template
"My Project :: Console App"
(list :type "python"
:request "launch"
:name "My Project :: Console App"
:program "${workspaceFolder}/myapp/__main__.py"
:cwd "${workspaceFolder}"
:args '("--some-arg" "value" "some-positional-arg")
:env '(("DEBUG" . "1") ("LOG_LEVEL" . "debug"))))
Here’s a breakdown of the used fields:
:type
: Specifies the DAP adapter type.:request
: Indicates the request typelaunch
orattach
.:name
: The name of the template, as it will appear in thedap-debug
command.:program
: Absolute path to the entry point of your application.:cwd
: Sets the working directory to the project root.:args
: Example of command line arguments passed to the entrypoint.:env
: Example of environment variables defined as a key value pair.
This is not a full list of the fields. Just a possible working
example. For a detailed description of possible fields refer to
documentation for used debugger. For debugpy
there is a Wiki
page25.
To use the custom template the buffer content using M-x eval-buffer
. Your template is now registered and can be selected when
invoking dap-debug
.
To verify your template:
- Open your Python project in Emacs.
- Run
dap-debug
and select “My Project :: Console App” from the list of templates. - The debugger should start, and you’ll see additional windows such as breakpoints, locals, and output.
While manually evaluating the file works, automating the process
ensures that the template is loaded whenever the project is
opened. You could write a hook function that checks for the existence
of debug.el
in the project root and evaluates it automatically. For
now, I leave this as an exercise for you.
By following these steps, you can create highly specific debug
configurations tailored to your project’s needs. Custom templates like
this not only streamline your debugging workflow but also make it
easier for teammates to get started with consistent
settings. Debugging doesn’t have to be a chore — let dap-mode
and a
bit of Emacs Lisp handle the heavy lifting for you.
Setting and Managing Breakpoints
To add breakpoints, use dap-breakpoint-add
or
dap-breakpoint-toggle
. These commands are intuitive: simply place your
cursor on the desired line and invoke the function. Once added, a
small dot will appear, indicating the breakpoint. To quickly re-run
the debugger with the last configuration, use dap-debug-last
. This
command saves you from re-selecting the configuration. If a breakpoint
is hit, the debugger will pause execution and open additional windows:
- Breakpoints: A list of all active breakpoints, with options to manage them.
- Locals: Displays variables local to the current stack frame.
- Expressions: Allows you to monitor custom expressions.
- Debug Sessions: Tracks active debug sessions.
- Run file (buffer): Displays output and interaction for the current session.
These windows provide a comprehensive view of the program’s state. You
can even navigate the call stack using dap-switch-stack-frame
in
addition to interact with specific frames through the stack window.
To terminate the debugging session, invoke dap-disconnect
.
Essential Commands
Here’s a quick reference to the most commonly used commands:
dap-debug
: Start a debug session.dap-debug-last
: Restart the last debug session.dap-breakpoint-toggle
: Add or remove a breakpoint.dap-breakpoint-delete
: Remove all breakpoints.dap-step-in
,dap-step-out
,dap-next
: Control execution flow.dap-switch-stack-frame
: Switch between stack frames.dap-disconnect
: End the debugging session.
This introduction demonstrates how easy it is to integrate dap-mode
into Emacs for Python debugging. While I haven’t delved into creating
complex custom templates or advanced debugging scenarios, I encourage
you to explore the dap-mode
documentation to unlock its full
potential.
My goal here was to show that setting up and integrating dap-mode
is
straightforward and doesn’t require deep expertise upfront. With
minimal effort, you can start experimenting and gradually build a
debugging workflow tailored to your needs.
Bringing It All Together
Here’s the complete setup:
(with-eval-after-load 'yasnippet
(yas-reload-all))
;; Only if you use `flymake-mode'. For `flycheck-mode' replace commands
;; by `flycheck-next-error' and `flycheck-previous-error'.
(with-eval-after-load 'flymake
(define-key flymake-mode-map (kbd "M-n") 'flymake-goto-next-error)
(define-key flymake-mode-map (kbd "M-p") 'flymake-goto-prev-error))
;; Set LSP keymap prefix.
(setopt lsp-keymap-prefix "C-c l")
;; Shut down LSP server after close all buffers associated with the server.
(setopt lsp-keep-workspace-alive nil)
;; Configure LSP UI enhancements.
(setopt lsp-headerline-breadcrumb-segments
'(path-up-to-project
file
symbols))
(with-eval-after-load 'lsp-ui
;; Remap `xref-find-definitions' (bound to M-. by default).
(define-key lsp-ui-mode-map
[remap xref-find-definitions]
#'lsp-ui-peek-find-definitions)
;; Remap `xref-find-references' (bound to M-? by default).
(define-key lsp-ui-mode-map
[remap xref-find-references]
#'lsp-ui-peek-find-references))
;; Configure LSP mode for enhanced experience.
(with-eval-after-load 'lsp-mode
;; Remap `lsp-treemacs-errors-list' (bound to C-c l g e).
(define-key lsp-mode-map
[remap lsp-treemacs-errors-list]
#'consult-lsp-diagnostics)
;; Remap `xref-find-apropos' (bound to C-c l g a).
(define-key lsp-mode-map
[remap xref-find-apropos]
#'consult-lsp-symbols)
;; Enable `which-key-mode' integration for LSP.
(add-hook 'lsp-mode-hook #'lsp-enable-which-key-integration))
;; Auto configure dap minor mode.
(setopt dap-auto-configure-mode t)
(defmacro company-backend-for-hook (hook backends)
`(add-hook ,hook (lambda ()
(set (make-local-variable 'company-backends)
,backends))))
(defun setup-python-environment ()
"Custom configurations for `python-mode'."
;; Enable YASnippet mode.
(yas-minor-mode 1)
;; Setup active backends for `python-mode'.
(company-backend-for-hook 'lsp-completion-mode-hook
'((company-capf :with company-yasnippet)
company-dabbrev-code))
;; Prevent `lsp-pyright' start in multi-root mode.
;; This must be set before the package is loaded.
(setq-local lsp-pyright-multi-root nil)
;; Enable LSP support in Python buffers.
(require 'lsp-pyright)
(lsp-deferred)
;; Enable DAP support in Python buffers.
(require 'dap-python)
(setq-local dap-python-debugger 'debugpy)
(dap-mode 1))
;; Configure hooks after `python-mode' is loaded.
(add-hook 'python-mode-hook #'setup-python-environment)
;; Setup buffer-local direnv integration for Emacs.
(when (executable-find "direnv")
;; `envrc-global-mode' should be enabled after other global minor modes,
;; since each prepends itself to various hooks.
(add-hook 'after-init-hook #'envrc-global-mode))
With this configuration, company
, yasnippet,
lsp-mode
and dap-mode
work seamlessly together to provide an enhanced development
experience. You now have powerful tools for Python development at your
disposal, with minimal effort and plenty of room for customization.
Future Enhancements
For those who prefer minimal configurations, Eglot
26 offers a
lightweight alternative to lsp-mode
. It integrates seamlessly with
Emacs’s native features and emphasizes simplicity over
extensibility. Eglot
is particularly suitable for users who value a
straightforward setup and are willing to trade some advanced features
for ease of use. Its minimalistic design makes it an appealing choice
for new users or those with modest LSP needs.
If performance is your top priority, consider lsp-bridge
27. It
aims to be the fastest LSP client in the Emacs ecosystem by leveraging
multi-threading technology. lsp-bridge
follows a plug-and-play
philosophy, reducing setup time and effort. This client might appeal
to users working on large codebases where performance bottlenecks in
other clients become apparent.
For Python developers, the Jedi Language Server28 is a focused alternative. Built exclusively on the Jedi library, it provides completions, definitions, references, and other essential features. If your Python workflow already relies on Jedi, this server integrates seamlessly and offers a lightweight option compared to more comprehensive language servers.
Another Python-specific option is the Python LSP Server29. This
community-maintained fork of the python-language-server
(pylsp
) project is
backed by the Spyder IDE team and the broader Python community. It
supports Python 3.8+ and provides robust LSP features like
completions, hover, and references. By default, it uses Jedi but can
be extended with plugins for additional functionality.
I discovered uv
30 too late in my journey, but I can confidently
say it has the potential to make some of the tools described earlier
unnecessary. uv
is a versatile, extremely fast Python package and
project manager, written in Rust. It combines and enhances the
functionalities of tools like pip
, pip-tools
, pipx
, pyenv
, and
virtualenv
into a single utility. With uv
, you can install and manage
Python versions, manage dependencies, create virtual environments, and
even run Python scripts with inline dependency metadata. It also
supports building and publishing Python projects with an efficient
universal lockfile.
Conclusion
So, we’ve reached the end of this guide. If you’ve followed along and set everything up, your system should now include the following components (organized by their location):
Component | Location |
---|---|
pyenv |
System-wide or User Home Directory. |
direnv |
System-wide or User Home Directory. |
pyright |
User Emacs Directory or User Home Directory. |
envrc |
Emacs Package. |
company |
Emacs Package. |
lsp-mode |
Emacs Package. |
lsp-pyright |
Emacs Package. |
lsp-ui |
Emacs Package. |
yasnippet |
Emacs Package. |
dap-mode |
Emacs Package. |
flycheck |
Emacs Package (optional, you can use flymake ). |
which-key |
Emacs Package (optional). |
consult-lsp |
Emacs Package (optional, only if consult is installed). |
debugpy |
Project Virtual Environment. |
The optional components are just that — optional. You can skip them and still achieve 99% of what’s described in this guide. The rest, however, are highly recommended. Are these tools unique or the absolute best? It’s hard to say, but they’re undeniably excellent, and their combination transforms Emacs into a fully-fledged Python IDE.
Should you stop here? Absolutely not. There are countless other tools and configurations that can make your Python development workflow even smoother and more enjoyable. I haven’t covered all of them because this article was never intended to be an encyclopedia.
That said, I genuinely enjoy this setup. Not only does it work
wonderfully for Python, but the combination of direnv
and lsp-mode
integrates seamlessly with virtually every programming language and
environment. What’s particularly satisfying is how little effort is
required to adapt it to new languages — no need to bloat your Emacs
configuration with language-specific extensions.
In most cases, all you need to do is install the appropriate language
server and, if necessary, a lightweight integration library like we
did with lsp-pyright
. This modular and universal approach highlights
the true power of Emacs: flexibility, simplicity, and the ability to
evolve alongside your workflow. Whether you’re working with Python,
JavaScript, C++, or even niche languages, this setup delivers a clean
and powerful development experience.
References
-
Language Server Protocol homepage. https://microsoft.github.io/language-server-protocol/ ↩︎
-
Pyright homepage. https://microsoft.github.io/pyright/#/ ↩︎
-
Emacs client/library for the Language Server Protocol. https://emacs-lsp.github.io/lsp-mode/ ↩︎
-
lsp-mode client leveraging pyright and basedpyright Language Servers. https://emacs-lsp.github.io/lsp-pyright/ ↩︎
-
Debug Adapter Protocol homepage. https://microsoft.github.io/debug-adapter-protocol/ ↩︎
-
An implementation of the Debug Adapter Protocol for Python. https://github.com/microsoft/debugpy ↩︎
-
Emacs client library for Debug Adapter Protocol. https://github.com/emacs-lsp/dap-mode ↩︎
-
Resolving EACCES permissions errors when installing packages globally. https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally ↩︎
-
Simple Python version management. https://github.com/pyenv/pyenv ↩︎
-
direnv home page. https://direnv.net/ ↩︎
-
direnv Community Wiki. https://github.com/direnv/direnv/wiki/Python ↩︎
-
Emacs support for direnv which operates buffer-locally. https://github.com/purcell/envrc ↩︎
-
Emacs support for direnv. https://github.com/wbolster/emacs-direnv ↩︎
-
Modular in-buffer completion framework for Emacs. https://company-mode.github.io/ ↩︎
-
Syntax checking for GNU Emacs. https://www.flycheck.org/en/latest/ ↩︎
-
GNU Flymake. https://www.gnu.org/software/emacs/manual/html_node/flymake/index.html ↩︎
-
A template system for Emacs. https://github.com/joaotavora/yasnippet ↩︎
-
UI integrations for lsp-mode. https://emacs-lsp.github.io/lsp-ui/ ↩︎
-
Display available keybindings in popup. https://elpa.gnu.org/packages/which-key.html ↩︎
-
lsp-ui features and configuration. https://emacs-lsp.github.io/lsp-ui/#intro ↩︎
-
lsp-mode and consult helping each other. https://github.com/gagbo/consult-lsp ↩︎
-
An implementation of the Debug Adapter Protocol for Python. https://github.com/microsoft/debugpy ↩︎
-
Python debugger package for use with Visual Studio and Visual Studio Code. https://github.com/microsoft/ptvsd ↩︎
-
dap-mode Issues #625. https://github.com/emacs-lsp/dap-mode/issues/625#issuecomment-1128961454 ↩︎
-
debugpy launch/attach configuration settings. https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings ↩︎
-
Eglot - A client for Language Server Protocol servers. https://joaotavora.github.io/eglot/ ↩︎
-
A fast LSP client for Emacs. https://github.com/manateelazycat/lsp-bridge ↩︎
-
A Python language server exclusively for Jedi. https://github.com/pappasam/jedi-language-server ↩︎
-
Fork of the python-language-server project. https://github.com/python-lsp/python-lsp-server ↩︎
-
An extremely fast Python package and project manager, written in Rust. https://github.com/astral-sh/uv ↩︎