Skip to content

Debugger breaks on internal AttributeError: module 'sys' has no attribute 'real_prefix' with "Raised Exceptions" enabled #2058

Description

@VulumeCode

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

  1. Python 3.14.7 venv.
  2. Any launch.json config of "type": "debugpy", "request": "launch", "justMyCode": true.
  3. In the Breakpoints panel, check Raised Exceptions.
  4. 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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions