Skip to content

Instantly share code, notes, and snippets.

@brechtm
Created September 12, 2025 13:17
Show Gist options
  • Select an option

  • Save brechtm/6b3838d367b0aaedf88370bf1d939df1 to your computer and use it in GitHub Desktop.

Select an option

Save brechtm/6b3838d367b0aaedf88370bf1d939df1 to your computer and use it in GitHub Desktop.
Python post-mortem debugging with Visual Studio Code
import os
import sys
from inspect import currentframe
from socket import gethostname
from traceback import print_stack
import debugpy
from rich.console import Console
from rich.traceback import Traceback
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
END = '\033[0m'
DEBUGPY_PORT = 56700
def _wait_for_client():
try:
debugpy.listen(('0.0.0.0', DEBUGPY_PORT))
except RuntimeError as e:
message, = e.args
if not message.endswith("has already been called on this process"):
raise
print(f"Attach the VSCode Python Debugger to port {gethostname()}:{DEBUGPY_PORT} (or Ctrl-C to continue)")
try:
debugpy.wait_for_client()
except KeyboardInterrupt:
pass
# https://github.com/microsoft/debugpy/discussions/1201#discussioncomment-10602573
def vscode_breakpointhook():
breakpoint_frame = currentframe().f_back
print(f"{BOLD}Hit breakpoint at{END}")
print_stack(breakpoint_frame, limit=1)
_wait_for_client()
debugpy.breakpoint()
# https://github.com/microsoft/debugpy/issues/723#issuecomment-919089064
def vscode_excepthook(type_, value, tb):
traceback_console = Console(file=sys.stderr)
traceback_console.print(Traceback.from_exception(type_, value, tb, width=120))
_wait_for_client()
import pydevd
import threading
from debugpy.server.api import _settrace
# handle case when debugging a module started with `python -m`
while (tb.tb_frame.f_code.co_filename == '<frozen runpy>'
or tb.tb_frame.f_code.co_filename.endswith('runpy.py')):
tb = tb.tb_next
# copied from debugpy.breakpoint()
# seems to be required to make the next section work reliably
pydb = pydevd.get_global_debugger()
stop_at_frame = tb.tb_frame
while (stop_at_frame is not None
and pydb.get_file_type(stop_at_frame) == pydb.PYDEV_FILE):
stop_at_frame = stop_at_frame.f_back
_settrace(suspend=True, trace_only_current_thread=True,
patch_multiprocessing=False, stop_at_frame=stop_at_frame)
# stop on the unhandled exception
thread = threading.current_thread()
additional_info = pydb.set_additional_thread_info(thread)
additional_info.is_tracing += 1
try:
arg = (type_, value, tb)
pydb.stop_on_unhandled_exception(pydb, thread, additional_info, arg)
finally:
additional_info.is_tracing -= 1
def ipdb_breakpointhook():
import ipdb
# https://github.com/gotcha/ipdb/issues/272
try:
return ipdb.set_trace(frame=sys._getframe().f_back)
finally:
sys.excepthook = ipdb_excepthook
def ipdb_excepthook(type_, value, tb):
import ipdb
traceback_console = Console(file=sys.stderr)
traceback_console.print(Traceback.from_exception(type_, value, tb, width=120))
ipdb.pm()
match os.getenv("PYDEBUG"):
case 'vscode' | None:
sys.breakpointhook = vscode_breakpointhook
sys.excepthook = vscode_excepthook
case 'ipdb':
sys.breakpointhook = ipdb_breakpointhook
sys.excepthook = ipdb_excepthook
@brechtm
Copy link
Author

brechtm commented Sep 12, 2025

Save this script as sitecustomize.py in your project root. These dependencies need to be installed in the project's virtualenv: debugpy, rich, and optionally ipdb.

When you run a python script or module from within the project root and an unhandled exception is encountered or breakpoint() is called, you'll have the option to connect to the Python process using VSCode's debugger. For this, create a "remote attach" launch configuration in launch.json:

        {
            "name": "Python Debugger: Remote Attach",
            "type": "debugpy",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": "56700",
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "."
                }
            ]
        },

The PYDEBUG environment variable can be set to adjust the script's behavior:

  • when not set, or set to vscode: wait for the Visual Studio Code debugger to attach
  • set to ipdb: attach the ipdb debugger
  • any other value: disable post-mortem debugging (for Python REPLs for example)

P.S. If several people are working on a shared machine, each user needs to be assigned a unique DEBUGPY_PORT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment