Environment
- debugpy: 1.8.20 (bundled in
ms-python.debugpy 2026.6.0, linux-x64)
- pydevd: 3.2.3
- Python: 3.14.7 (
[Clang 22.1.3], uv-managed CPython, virtualenv-created venv)
- OS: Linux 6.6.114.1-microsoft-standard-WSL2
- Client: VS Code (Remote-WSL)
"justMyCode": true, "subProcess": true
Summary
With Raised Exceptions checked in the Breakpoints panel, every debug session pauses during
debugpy's own startup on an AttributeError that debugpy raises and immediately catches itself.
debugpy/common/log.py deliberately probes sys.real_prefix — an attribute stock CPython has
never defined and that virtualenv ≥ 20 no longer sets — and handles its absence:
# debugpy/common/log.py:295-301
if not callable(get_paths):
expr = get_paths
get_paths = lambda: util.evaluate(expr)
try:
paths = get_paths()
except AttributeError:
report("{0}<missing>\n", prefix)
return
debugpy/common/util.py explicitly tries to keep the debugger from stopping there:
# debugpy/common/util.py:9-16
def evaluate(code, path=__file__, mode="eval"):
# Setting file path here to avoid breaking here if users have set
# "break on exception raised" setting. This code can potentially run
# in user process and is indistinguishable if the path is not set.
# We use the path internally to skip exception inside the debugger.
expr = compile(code, path, "eval")
return eval(expr, {}, sys.modules)
That suppression does not work. debugpy/common/util.py is not registered anywhere in
pydevd's skip tables, so pydevd classifies it as user code and stops on the exception.
Steps to reproduce
- Python 3.14.7 venv.
- Any
launch.json config of "type": "debugpy", "request": "launch", "justMyCode": true.
- In the Breakpoints panel, check Raised Exceptions.
- Press F5.
Actual
The session pauses immediately, before any user code runs, with:
Exception has occurred: AttributeError
module 'sys' has no attribute 'real_prefix'
File ".../debugpy/common/util.py", line 1, in <module>
File ".../debugpy/common/util.py", line 16, in evaluate
File ".../debugpy/common/log.py", line 297, in <lambda>
File ".../debugpy/common/log.py", line 299, in report_paths
File ".../debugpy/common/log.py", line 328, in get_environment_description
File ".../debugpy/common/log.py", line 372, in describe_environment
File ".../debugpy/server/api.py", line 56, in ensure_logging
File ".../debugpy/server/cli.py", line 526, in main
File ".../debugpy/__main__.py", line 71, in <module>
The frame shown is the compiled expression, reported as util.py line 1 — i.e. the path=__file__
trick puts a debugger-internal filename in the user's call stack instead of hiding it.
Expected
No pause. The exception is internal to debugpy, is caught by debugpy, and util.evaluate's comment
states the intent to skip it.
Root cause
For <...>/debugpy/common/util.py, PyDB.get_file_type() (pydevd.py:1043) returns None,
which the docstring defines as "a regular user file which should be traced." Every branch that
could classify it as PYDEV_FILE misses:
| Check |
Location |
Result for debugpy/common/util.py |
basename.startswith(IGNORE_BASENAMES_STARTING_WITH) |
pydevd.py:1002 |
No — prefixes are ('<frozen ', '<builtin', '<attrs', '<__array_function__') |
DONT_TRACE[basename] |
pydevd.py:1006 |
Miss — util.py and log.py are absent from the table |
DONT_TRACE_DIRS[dirname] |
pydevd.py:1019-1023 |
Miss — see below |
dont_trace_external_files(abs_path) |
pydevd.py:1109 |
False — the default at pydevd.py:1026-1041 always returns False |
DONT_TRACE_DIRS covers only pydevd's own packages; nothing under debugpy/:
['_pydev_bundle', '_pydev_runfiles', '_pydevd_bundle', '_pydevd_frame_eval',
'_pydevd_sys_monitoring', 'pydev_ipython', 'pydev_sitecustomize',
'pydevd_attach_to_process', 'pydevd_concurrency_analyser', 'pydevd_plugins',
'test_pydevd_reload']
Verified directly:
$ python -c "from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, DONT_TRACE_DIRS; \
print(DONT_TRACE.get('util.py','NOT PRESENT'), DONT_TRACE.get('log.py','NOT PRESENT')); \
print([k for k in DONT_TRACE_DIRS if 'debugpy' in k] or 'NONE')"
NOT PRESENT NOT PRESENT
NONE
The only mechanism that would set dont_trace_external_files to something non-trivial is
PyDevdAPI.set_dont_trace_start_end_patterns (pydevd_api.py:953-976), driven by the
setDebuggerProperty DAP request — which VS Code does not send for debugpy's own install root.
So the path passed to compile() never reaches a skip list, and the comment's promise
("We use the path internally to skip exception inside the debugger") is not backed by any
registration.
Why users can't work around it
setExceptionBreakpoints cannot narrow this. The adapter does not advertise
supportsExceptionFilterOptions (pydevd_process_net_command_json.py:232-257), so VS Code offers
no condition UI on the filter, and the handler hardcodes:
# pydevd_process_net_command_json.py:900-902
# Can't set these in the DAP.
condition = None
expression = None
supportsExceptionOptions=True is advertised, but it selects by exception name, not location —
so the only expressible workaround is muting AttributeError globally, which is worse than the bug.
That leaves users a binary choice: turn off "Raised Exceptions" entirely, or hit this on every F5.
Suggested fixes
-
Don't raise in the first place (smallest, fixes the reported symptom):
in log.get_environment_description, probe with getattr(sys, "real_prefix", None) rather than
depending on an AttributeError propagating out of eval.
-
Make the suppression real (fixes the whole class of leaks):
register debugpy's own package directories — debugpy/common, debugpy/server,
debugpy/adapter, debugpy/launcher — as PYDEV_FILE in DONT_TRACE_DIRS, or have debugpy
install a dont_trace_external_files covering its install root at startup. Today pydevd hides
its own internals but not the debugpy layer wrapping it.
Notes
- The
AttributeError itself is harmless — report_paths catches it and prints
sys.real_prefix: <missing>. This is purely a spurious debugger stop.
- The classification logic above is Python-version independent; I observed the stop on 3.14.7 after
migrating a project from 3.8.16 and have not verified behaviour on older interpreters, so I
can't claim this as a 3.14-specific regression.
- Setting
sys.real_prefix via a .pth in site-packages suppresses it, but that is a hack on the
user's environment, not a fix.
Environment
ms-python.debugpy2026.6.0, linux-x64)[Clang 22.1.3], uv-managed CPython, virtualenv-created venv)"justMyCode": true,"subProcess": trueSummary
With Raised Exceptions checked in the Breakpoints panel, every debug session pauses during
debugpy's own startup on an
AttributeErrorthat debugpy raises and immediately catches itself.debugpy/common/log.pydeliberately probessys.real_prefix— an attribute stock CPython hasnever defined and that virtualenv ≥ 20 no longer sets — and handles its absence:
debugpy/common/util.pyexplicitly tries to keep the debugger from stopping there:That suppression does not work.
debugpy/common/util.pyis not registered anywhere inpydevd's skip tables, so pydevd classifies it as user code and stops on the exception.
Steps to reproduce
launch.jsonconfig of"type": "debugpy","request": "launch","justMyCode": true.Actual
The session pauses immediately, before any user code runs, with:
The frame shown is the compiled expression, reported as
util.pyline 1 — i.e. thepath=__file__trick puts a debugger-internal filename in the user's call stack instead of hiding it.
Expected
No pause. The exception is internal to debugpy, is caught by debugpy, and
util.evaluate's commentstates the intent to skip it.
Root cause
For
<...>/debugpy/common/util.py,PyDB.get_file_type()(pydevd.py:1043) returnsNone,which the docstring defines as "a regular user file which should be traced." Every branch that
could classify it as
PYDEV_FILEmisses:debugpy/common/util.pybasename.startswith(IGNORE_BASENAMES_STARTING_WITH)pydevd.py:1002('<frozen ', '<builtin', '<attrs', '<__array_function__')DONT_TRACE[basename]pydevd.py:1006util.pyandlog.pyare absent from the tableDONT_TRACE_DIRS[dirname]pydevd.py:1019-1023dont_trace_external_files(abs_path)pydevd.py:1109False— the default atpydevd.py:1026-1041always returnsFalseDONT_TRACE_DIRScovers only pydevd's own packages; nothing underdebugpy/:Verified directly:
The only mechanism that would set
dont_trace_external_filesto something non-trivial isPyDevdAPI.set_dont_trace_start_end_patterns(pydevd_api.py:953-976), driven by thesetDebuggerPropertyDAP request — which VS Code does not send for debugpy's own install root.So the path passed to
compile()never reaches a skip list, and the comment's promise("We use the path internally to skip exception inside the debugger") is not backed by any
registration.
Why users can't work around it
setExceptionBreakpointscannot narrow this. The adapter does not advertisesupportsExceptionFilterOptions(pydevd_process_net_command_json.py:232-257), so VS Code offersno condition UI on the filter, and the handler hardcodes:
supportsExceptionOptions=Trueis advertised, but it selects by exception name, not location —so the only expressible workaround is muting
AttributeErrorglobally, which is worse than the bug.That leaves users a binary choice: turn off "Raised Exceptions" entirely, or hit this on every F5.
Suggested fixes
Don't raise in the first place (smallest, fixes the reported symptom):
in
log.get_environment_description, probe withgetattr(sys, "real_prefix", None)rather thandepending on an
AttributeErrorpropagating out ofeval.Make the suppression real (fixes the whole class of leaks):
register debugpy's own package directories —
debugpy/common,debugpy/server,debugpy/adapter,debugpy/launcher— asPYDEV_FILEinDONT_TRACE_DIRS, or have debugpyinstall a
dont_trace_external_filescovering its install root at startup. Today pydevd hidesits own internals but not the debugpy layer wrapping it.
Notes
AttributeErroritself is harmless —report_pathscatches it and printssys.real_prefix: <missing>. This is purely a spurious debugger stop.migrating a project from 3.8.16 and have not verified behaviour on older interpreters, so I
can't claim this as a 3.14-specific regression.
sys.real_prefixvia a.pthin site-packages suppresses it, but that is a hack on theuser's environment, not a fix.