Support debugging Jupyter notebook cells
Positron Version:
Steps to reproduce the issue:
- Open a new Jupyter Notebook for Python
- Create the subtract function and its call inside of a cell
def subtract(a, b):
diff = a - b
return diff
subtract(1, 2)
- Click on the left of
diffline to insert a breakpoint - Try running the debugger
- Attempt to advance the line
- Receive error in Terminal
Debugger open in Notebook with breakpoint
Debugger options
What did you expect to happen?
The Python debugger should have displayed the call stack
Were there any error messages in the output or Developer Tools console?
(ds) (base) ➜ Positron-Python cd /Users/ronin/PositronVideos/Positron-Python ; /usr/bin/env /opt/
miniconda3/envs/ds/bin/python /Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-a
rm64/bundled/libs/debugpy/adapter/../../debugpy/launcher 52971 -- /Users/ronin/PositronVideos/Posit
ron-Python/jupyter-debugger-demo.ipynb
Traceback (most recent call last):
File "/opt/miniconda3/envs/ds/lib/python3.12/runpy.py", line 198, in _run_module_as_main
return _run_code(code, main_globals, None,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/miniconda3/envs/ds/lib/python3.12/runpy.py", line 88, in _run_code
exec(code, run_globals)
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 39, in <module>
cli.main()
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 430, in main
run()
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 284, in run_file
runpy.run_path(target, run_name="__main__")
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 321, in run_path
return _run_module_code(code, init_globals, run_name,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 135, in _run_module_code
_run_code(code, mod_globals, init_globals,
File "/Users/ronin/.positron/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 124, in _run_code
exec(code, run_globals)
File "/Users/ronin/PositronVideos/Positron-Python/jupyter-debugger-demo.ipynb", line 14, in <module>
"execution_count": null,
^^^^
NameError: name 'null' is not defined
Thanks for the report! We haven't yet hooked up the debugger for Jupyter notebook cells, we can use this issue to track that work.
I think what's happening in your case is that the Python debugger is trying to treat this .ipynb file as a .py file, which won't work. We'll need to setup debugging for Jupyter notebook cells specifically.
I'm going to track the overall progress for debugging Jupyter notebook cells in the epic: https://github.com/posit-dev/positron/issues/6338.
We can use this issue to track the initial feature-flagged implementation.
Once we address https://github.com/posit-dev/positron/issues/6339, we should be able to use vscode.debug.registerDebugAdapterDescriptorFactory and return a vscode.DebugAdapterInlineImplementation with a vscode.DebugAdapter that routes messages to/from the kernel.
This is at least how the Jupyter extension approaches it. They also convert locations in all messages between the kernel and debug adapter.
We might be able to use the same functionality to debug Python scripts in the console (with a kernel connection). That may, for example, let us view debug variables in the data viewer (https://github.com/posit-dev/positron/issues/5659).
There is a very rough and early branch: feature/debug-notebook-cells which I worked on for #5779.
It:
- Wires up the kernel debug messages in the
positron-supervisorextension and adds adebugmethod to sessions. - Adds a
positron.LanguageRuntimeSession.debugmethod that calls down to supervisordebug. - Registers a debug adapter in the Python extension (it only lives there atm for convenience but should be housed in a new extension, since it should interact with languages via Positron's runtime interfaces).
- The debug adapter routes messages between the language runtime and the debug server.
Next step would be to convert locations between "real" to "debugger" when the debug adapter routes messages. For example, see vscode-jupyter's kernelDebugAdapter.
Will probably also need to be lots of smaller refinements, like cleaning up when the session ends, on interrupt, etc.
I'm experiencing issues with debugging on the latest versions of Positron:
- Version 2025.06.0-91
- Version 2025.05.0-142
Operating system: Ubuntu 22.04.5 LTS
@seeM @nstrayer I put detailed notes here. I found a few bugs, which I'll be logging. Overall, I think it looks more or less in line with VS Code, with some problems when using the Positron Assistant together with Notebooks, like previously reported. Additionally, user can switch to another cell type while a cell is executing ("on the fly"), but this is also a problem within VS Code. cc @testlabauto
Verified Fixed
Positron Version(s) : 2025.09.0-102 (latest, and previous build)
OS Version : Sequoia 15.6
Test scenario(s)
I must first give a warning that this was probably the hardest verification I've had, because it covers a lot. For reference, please see the User Stories in the Positron: Debugging Notebooks Feature Spec document. I verified the scenarios below in both Positron and VS Code to make sure behaviors are similar. This list is by no means comprehensive and there are a lot more scenarios. I also tested with some more complex chemistry-related notebooks, and behaviors seem to be in line with VS Code.
I have grouped them in groups and sub-groups (which indicate cells) in case we need to run them again in the future or to split tasks for adding automation. Also, whenever I say BP, I'm referring to BreakPoint.
Group 0: Environment
# [0.1] - runtime info
import sys, platform # note: ruff doesn't like this, but it's ok :)
print("Python:", sys.version.split()[0], "| Impl:", platform.python_implementation())
# [0.2] - shared helpers we'll need later
DATA = [3, 1, 4, 1, 5, 9]
def slow_add(a, b, delay=0.3):
import time; time.sleep(delay)
return a + b
Group 1: Breakpoints
# [1.1] - toggle + pane metadata
x = 10 # BP-1
y = 20 # BP-2
z = slow_add(x, y) # BP-3
print("z:", z)
# [1.2] - Run ignores BPs; Debug stops
print("Before")
val = sum(DATA) # BP-RUN-IGNORE
print("After", val)
Group 2: Debug Cell
# [2.1] - first-stop at earliest BP
a = 7 # BP-DC-1
b = 8
c = a * b # BP-DC-2
print("c:", c)
Group 3: Debug All
# [3.1] - part 1
def f(n): # BP-DA-1
return n*n
# [3.2] - part 2
res = [f(i) for i in range(3)]
print("res:", res) # BP-DA-2
Group 4: Debugging Tools
# [4.1] - step across cells: def
def g(x): # BP-STEP-DEF
return x + 42
# [4.2] - step across cells: use
u = 5 # BP-STEP-1
v = g(u) # Step Into, then Out
w = v * 2 # Step Over
print("w:", w)
# [4.3] - scopes: Globals / Locals / Cell
GLOBAL_BOX = {"k": "G"} # global
def scoped(alpha):
beta = alpha * 3 # local
return beta
ALPHA = 11
BETA = scoped(ALPHA) # BP-VARS
print("BETA:", BETA)
# [4.4] - Debug Console eval
nums = [2, 7, 1] # BP-DCON
total = sum(nums)
print("total:", total)
# [4.5] - call stack navigation
def layer1(p):
return layer2(p + 1)
def layer2(q):
return layer3(q + 1)
def layer3(r):
s = r * 2 # BP-STACK
return s
out = layer1(3)
print("out:", out)
Group 5: UX Sharp Edges
# [5.1] - unexpected exit → error toast
import os
print("Exiting kernel abruptly...")
os._exit(1) # kills interpreter hard
# [5.2] - interrupt/shutdown/restart ends session silently
import time
for i in range(20): # interrupt while debugging
time.sleep(0.5)
print("tick", i)
Group 6: Unsupported interpreter, e.g., switch from Python to R
# [6.1] — placeholder; switch to R for a Python notebook to verify disabled/errored UI
import time
print("Switch interpreter manually to R, such that cell is actually Python code.")
Group 7: Multiple Notebooks
# Notebook A
from math import sqrt
def square_then_root(x): # BP-A
y = x*x
return sqrt(y)
print(square_then_root(9))
# Notebook B
def cube(x): # BP-B
return x*x*x
print(cube(3))
Group 8: Config (justMyCode)
# [8.1] - simulate app vs library; use notebook.debugging.justMyCode to see effect
import os, sys, textwrap, tempfile # ruff will get angry here, it's fine though
tmpdir = tempfile.mkdtemp()
libdir = os.path.join(tmpdir, "fakepkg")
os.makedirs(libdir, exist_ok=True)
with open(os.path.join(libdir, "__init__.py"), "w") as f: f.write("")
with open(os.path.join(libdir, "core.py"), "w") as f:
f.write(textwrap.dedent("""
def lib_func(n):
x = n + 5 # BP-LIB
return x * 2
"""))
sys.path.insert(0, tmpdir)
from fakepkg.core import lib_func
def app():
val = 10 # BP-APP
return lib_func(val)
res = app()
print("res:", res)
Group 9: Cross-cell state with stepping into (make sure the subgroups correspond to separate cells)
# [9.1]
STATE = {"count": 0} # BP-STATE-DEF
# [9.2]
def bump():
STATE["count"] += 1 # BP-STATE-MUT
return STATE["count"]
# [9.3]
vals = [bump() for _ in range(3)] # Step Into; watch STATE
print("vals:", vals, "| STATE:", STATE) # should yield a STATE with count=3
Group 10: Step Performance
# [10.1] - many quick steps
s = 0
for k in range(50): # BP-LOOP, change the 50 value to observe changes in response times too
s += k
print(s)