Using pdb from Emacs

pdb Basics

Emacs’ pdb support is part of the Grand Unified Debugger framework.

Since everything is available out of the box, there’s nothing really to setup! Just run M-x pdb and Emacs will ask you how you want to invoke pdb

Run pdb (like this): python -m pdb example.py

Emacs will then reconfigure itself to display both the pdb prompt in an interactive window, alongside the code you are debugging.

M-x pdb |-1-|                                                                                                                 [85%] 18:58 18/03/26 ☰ Current directory is ~/Projects/alcarney/blog/content/                                                                                       > /var/home/alex/Projects/alcarney/blog/content/example.py(1)<module>()                                                                      -> import pathlib                                                                                                                            (Pdb)  -  *gud-pdb* 4:6      (Debugger:run Apheleia)                                                                                                 =>  1 import pathlib                                                                                                                            2      3      4 defmain():                                                                                                                               5 forfilein pathlib.Path(".").glob("*.rst"):                                                                                          6 print(file.name)                                                                                                                  7      8      9 if __name__ == "__main__":                                                                                                               10     main()                                                                                                                            -🖿 alcarney/blog example.py 1:0      (Python ws Apheleia© ElDoc)                                                                          

From here you can use pdb just as if you’d started it in your terminal. But of course Emacs offers a set of additional keybindings, which differ slightly depending on in you have your cursor in the source code view or the pdb buffer.

Command

pdb Equivalent

Keybind (Pdb)

Keybind (Code)

gud-break

break (b)

C-x C-a C-b

gud-step

step (s)

C-c C-s

C-x C-a C-s

gud-next

next (n)

C-c C-n

C-x C-a C-n

gud-print

C-c C-p

C-x C-a C-p

See (info "(emacs)Commands of GUD") for the full list.

Tip

Yes, those keybinds look pretty ugly, but if you enable repeat-mode following the first use the keymap below is activated and the listed commands are available with a single keypress!

;; from `gud.el`
(defvar gud-pdb-repeat-map
  (let ((map (make-sparse-keymap)))
    (pcase-dolist (`(,key . ,cmd) '(("n" . gud-next)
                                    ("s" . gud-step)
                                    ("c" . gud-cont)
                                    ("l" . gud-refresh)
                                    ("f" . gud-finish)
                                    ("<" . gud-up)
                                    (">" . gud-down)))
      (define-key map key cmd))
    map)
  "Keymap to repeat `pdb' stepping instructions \\`C-x C-a C-n n n'.
Used in `repeat-mode'.")

Startup Commands

My favourite thing about M-x pdb is that it will work with any python command that eventually results in a pdb session. For example:

  • Start a pdb session within the context of a pytest failure:

    pytest test_example.py --pdb
    
  • Use uv to install a particular version of Sphinx and debug a failing build:

    uv run --with sphinx==9.1.0 sphinx-build -b html -P docs/ docs/_build/
    
  • Select the hatch environment that uses Python 3.11 and Sphinx v8, run the named test and start pdb when it fails:

    hatch test -i py=3.11 -i sphinx=8 -- tests/example/test_example.py::test_this --pdb
    

The only commands that haven’t worked for me so far are those invoked via make (possibly because each command is executed in a separate shell?).

Tip

M-x pdb runs your given Python command from the default-directory of the current buffer. Running pdb via project-execute-extended-command (C-x p x for me) runs the given command from your project’s root.

Remote Debugging

If you’ve been keeping up with recent Python releases, you’ve likely already heard that pdb in Python 3.14 now supports attaching to existing Python processes! I’m finding this particuarly useful when working with Textualize/textual as the process’ own stdin and stdout channels are busy hosting the TUI.

To attach to a remote process, we “just” need to supply the process PID to the cli provided by the pdb module:

uv run --python 3.14 python -m pdb -p 1234

(I’m using uv here as my system Python is currently 3.13).

Up until now I’ve been using htop to lookup the PID and type it in, but come on, we’re using Emacs, surely we can do better than that!

Selecting PIDs using completing-read

If you didn’t know, Emacs has it’s own built-in htop style program - proced (because of course it does!).

This means all the necessary infrastructure for discovering the available processes should be there for us to reuse. And yes, after a quick search I find that the list-system-processes function returns a list of all the currently active PIDs

*** Welcome to IELM ***  Type (describe-mode) or press C-h m for help.
ELISP> (list-system-processes)
(1 2 3 4 5 6 7 8 10 13 15 16 17 18 19 20 21 22 23 24 25 27 28 ...)

Each PID can be passed to process-attributes to get some information about it:

ELISP> (process-attributes 42778)
((args . "python -m sphinx_agent") (pmem . 0.6490886804884154)
 (rss . 208580) (vsize . 733132) (thcount . 3) (nice . 0) (pri . 20)
 (pcpu . 0.5113255738694369) (etime 1 53556 420000 0)
 (start 27065 14522 350000 0) (ctime 0 0 0 0) (cstime 0 0 0 0)
 (cutime 0 0 0 0) (time 0 608 950000 0) (stime 0 29 470000 0)
 (utime 0 579 480000 0) (cmajflt . 0) (cminflt . 0) (majflt . 5)
 (minflt . 950190) (tpgid . -1) (ttname . "") (sess . 42771)
 (pgrp . 42771) (ppid . 42777) (state . "S") (comm . "python")
 (group . "alex") (egid . 1000) (user . "alex") (euid . 1000))

After a fair amount of head scratching I eventually arrived at the following elisp that will prompt me to select a process in minibuffer and return the corresponding PID:

lisp/alc-python.el
(defun alc-python--pid-to-candidate (pid)
  "Convert the given PID into a completion candidate for `completing-read'"
  (let* ((process (process-attributes pid))
         (cmd (alist-get 'args process)))
    (if (string= (user-login-name) (alist-get 'user process))
      (cons (format "[%s] %s" pid cmd) pid))))

(defun alc-python--select-process-pid ()
   "Select a process id via `completing-read'"
   (let* ((processes (mapcar 'alc-python--pid-to-candidate (list-system-processes)))
          (selection (completing-read "Select process: " processes nil t)))
     (cdr (assoc-string selection processes))))

I’d love to be able to pre-filter the list of processes to only include those that are debuggable by pdb (it must be possible as pdb itself gives a nice error message if you select an incompatible process), but I imagine that’s a non-trivial undertaking.

Anyway with that helper out of the way we’re only a defun away from implementing an “attach to process” command!

lisp/alc-python.el
(defun alc-python-pdb-attach-to-process ()
   "Attach a pdb session to a running Python process."
   (interactive)
   (when-let* ((pid (alc-python--select-process-pid))
               (prj (project-current nil))
               (default-directory (project-root prj)))
     (pdb (format "uv run --no-project --python 3.14 python -m pdb -p %s" pid))))