Local Debugging with Python

Debugging serverless-style applications can be challenging due to the way functions are triggered by events.

This guide will walk you through setting up local debugging for Nitric applications written in Python, using VSCode. We will use debugpy to connect a debugger to the service while it runs.

1. Add debugpy to our project

uv add debugpy

2. Modify the Python entry point

Add the following lines to the top of the service file (e.g., services/api.py). This starts a debug server that the IDE can attach to.

This code is for local development only and must not be included in production deployments.

import debugpy
host, port = debugpy.listen(("0.0.0.0", 52509)) # Static port for consistent debugging
print(f"✅ Debugpy is listening on {host}:{port}", flush=True)

A static port (52509) is used so the IDE knows which port to connect to. Update the launch.json configuration to match this port before starting the debugger.

3. Update start command

Modify the start command to include an auto-reloader and ensure Python does not use frozen modules, which can interfere with debugpy:

name: my-project
services:
- basedir: ''
match: services/*.py
runtime: python
start: uv run -- watchmedo auto-restart -p "*.py" --no-restart-on-command-exit -R -- python -Xfrozen_modules=off $SERVICE_PATH
batch-services: []
websites: []
runtimes:
python:
dockerfile: ./python.dockerfile
context: ''
args: {}

This configuration restarts the service on file changes and includes the necessary flags for debugging compatibility.

4. Configure VS Code

VS Code uses a .vscode/launch.json file to define how it should start or attach to a debugging session. In this case, the debugger doesn't launch the application itself—it attaches to the running service that was started manually from the terminal.

To create or update the launch.json file:

  • Open the Run and Debug panel in VS Code (Ctrl+Shift+D or from the sidebar).
  • Click "create a launch.json file" if one doesn't already exist.
  • Choose "Python" when prompted for the environment.
  • Replace the default configuration with the following:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Remote Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 52509
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
}
]
}

Ensure the port matches the value used in the debugpy.listen() call. If the port changes in the code, update it here as well.

5. Running the debugger

Start your nitric service with nitric start, in the Terminal, both the Nitric runtime output and the debugpy listener output will be visible, including the active debug port.

vscode

Now run the debugger and add breakpoints or watch variables.

vscode

6. Handling multiple services

Nitric applications typically have more than one service, however, debugpy only supports listening on a single (host, port) pair per service.

Your services will each have the following snippet with a unique port which will configure in our launch.json.

import debugpy
host, port = debugpy.listen(("0.0.0.0", 52509))
print(f"✅ Debugpy is listening on {host}:{port}", flush=True)

You can update your vscode configuration to use compounds, for example if you had two services:

{
"version": "0.2.0",
"compounds": [
{
"name": "Attach to Both Services",
"configurations": [
"Python Debugger: Service 1",
"Python Debugger: Service 2"
]
}
],
"configurations": [
{
"name": "Python Debugger: Service 1",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 52509
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
},
{
"name": "Python Debugger: Service 2",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 52510
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
}
]
}

Now in Run and Debug you can select Attach to both:

vscode

Last updated on Apr 1, 2025