Skip to content

debugpy 1.8.19 creates _DummyThread when inspecting PySide6 QThread under Python 3.13, causing teardown crash #1985

Open

Description

@Erriez

Summary

When debugging a PySide6 application using debugpy 1.8.19 (via VS Code Python Debugger extension 2025.18.0) on Python 3.13.4, debugpy’s sys‑monitoring creates _DummyThread objects while inspecting a QThread that performs HTTP requests (requests/urllib3).
During interpreter shutdown, Python 3.13 attempts to finalize these _DummyThread objects and crashes with:

Exception ignored in: <function _DeleteDummyThreadOnDel.__del__>
TypeError: 'NoneType' object does not support the context manager protocol

This does not occur:

  • when running the same code outside VS Code (no debugpy)
  • when using only Python threads
  • when using QThread without debugpy
  • when using requests/urllib3 without debugpy

The issue is reproducible with a minimal PySide6 + requests script.

I have not tested this on Python 3.12, so I cannot confirm whether the issue is specific to Python 3.13 or also affects earlier versions


Environment

  • Python: 3.13.4 (Windows x64)
  • VS Code Python Debugger extension: 2025.18.0
  • debugpy version: 1.8.19
    (verified via import debugpy; debugpy.__version__)
  • OS: Windows 11
  • PySide6: 6.10.1
  • requests: 2.32.5
  • urllib3: 2.6.0

Minimal pip freeze:

certifi==2025.11.12
charset-normalizer==3.4.4
idna==3.11
PySide6==6.10.1
PySide6_Addons==6.10.1
PySide6_Essentials==6.10.1
requests==2.32.5
shiboken6==6.10.1
urllib3==2.6.0

Minimal Reproduction

qt_debugpy_repro.py:

import sys
import time
import threading
import requests
from PySide6.QtCore import QObject, QThread, Signal
from PySide6.QtWidgets import QApplication

import debugpy
print(f"debugpy version: {debugpy.__version__}")

class Worker(QObject):
    tick = Signal()

    def run(self):
        # Simulate Spotipy/urllib3 behavior: property access on HTTPResponse
        for _ in range(3):
            r = requests.get("https://httpbin.org/get")
            _ = r.raw.closed  # <-- triggers urllib3.response.closed
            time.sleep(0.1)
        print("worker done")


def dump_threads(label):
    print(f"\nTHREADS {label}:")
    for t in threading.enumerate():
        print(" -", t, "daemon=", t.daemon, "type=", type(t))


def main():
    app = QApplication(sys.argv)

    worker = Worker()
    thread = QThread()
    worker.moveToThread(thread)
    thread.started.connect(worker.run)

    thread.start()

    QThread.msleep(500)

    dump_threads("BEFORE SHUTDOWN")

    # Stop QThread without joining (mirrors real-world teardown timing)
    thread.quit()

    dump_threads("AFTER QUIT")

    print("exiting app")
    sys.exit(0)


if __name__ == "__main__":
    main()

Steps to Reproduce

  1. Open the script in VS Code
  2. Select Python 3.13.4 interpreter
  3. Use this launch.json:
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debug Repro",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/qt_debugpy_repro.py",
            "console": "integratedTerminal",
            "python": "${workspaceFolder}/venv/Scripts/python"
        }
    ]
}
  1. Run with Start Debugging
  2. Observe thread list and shutdown behavior

Observed Behavior (under debugpy)

In the minimal reproduction code listed above, debugpy creates a _DummyThread when inspecting a PySide6 QThread.
The minimal repro does not crash Python, but it clearly shows the unexpected _DummyThread created by debugpy:

THREADS BEFORE SHUTDOWN:
 - <_MainThread(MainThread, ...)>
 - <_DummyThread(Dummy-6, ...)>

THREADS AFTER QUIT:
 - <_MainThread(MainThread, ...)>
 - <_DummyThread(Dummy-6, ...)>

exiting app
QThread: Destroyed while thread '' is still running

Behavior in the full application

In my real application (which uses the same pattern but performs more work inside the QThread), the _DummyThread created by debugpy survives until interpreter shutdown.
On Python 3.13.4 this leads to a teardown crash:

Exception ignored in: <function _DeleteDummyThreadOnDel.__del__>
TypeError: 'NoneType' object has no support for the context manager protocol

This crash does not occur outside debugpy.

The _DummyThread is created by debugpy’s sys‑monitoring:

Example captured stack:

=== DummyThread created ===
  File "qt_debugpy_repro.py", line XX, in run
  File "...debugpy/_pydevd_sys_monitoring.py", line 330, in _create_thread_info
    t = threading.current_thread()
  File "threading.py", line 1439, in current_thread
    return _DummyThread()
=== end DummyThread stack ===

Expected Behavior

  • No _DummyThread should be created for QThreads
  • No crash during interpreter shutdown
  • Behavior should match Python 3.12 and non-debugpy execution

Analysis

  • QThread is not a Python threading.Thread
  • debugpy’s sys‑monitoring inspects thread state during property access (urllib3.response.closed)
  • threading.current_thread() returns _DummyThread() when the thread is not in threading._active
  • Python 3.13 changed teardown order
  • _DummyThread.__del__ now runs after parts of threading are already destroyed
  • This leads to the observed TypeError during shutdown

This issue does not occur with:

  • Python threads
  • QThreads without debugpy
  • requests/urllib3 without debugpy

Only the combination of debugpy + QThread + urllib3 + Python 3.13 triggers it.


Possible Fix Direction

  • Avoid calling threading.current_thread() on non‑Python threads
  • Or guard _create_thread_info() against QThreads
  • Or avoid creating _DummyThread objects during sys‑monitoring
  • Or ensure _DummyThread is not left alive until interpreter teardown

Thank you

Happy to test patches or provide additional instrumentation if needed.

Activity

  1. Erriez commented on Dec 30, 2025

    @Erriez
    Author

    Temporary workaround / monkeypatch for Python 3.13 + debugpy

    Until this issue is resolved in debugpy, here is a minimal and safe monkeypatch that prevents Python 3.13 from crashing during interpreter shutdown when debugpy creates _DummyThread objects for non‑Python threads (e.g. PySide6 QThread).

    This patch is intentionally narrow in scope:

    • Only activates on Python ≥ 3.13
    • Only activates if debugpy is imported
    • Only suppresses destructor calls originating from debugpy internals
    • Prints a one‑time warning pointing back to this GitHub issue
    • Leaves all other destructor behavior untouched
    """
    Monkeypatch to suppress Python 3.13 teardown crashes caused by debugpy creating _DummyThread objects 
    for non-Python threads (e.g., PySide6 QThread).
    
    This patch is intentionally narrow in scope:
    - Only activates on Python >= 3.13
    - Only activates if debugpy is imported
    - Only suppresses destructor calls originating from debugpy internals
    - Prints a one-time warning with a link to the GitHub issue
    """
    
    import sys
    import threading
    
    _ISSUE_URL = "https://github.com/microsoft/debugpy/issues/1985"
    _debugpy_warning_printed = False
    
    
    def _patch_debugpy_dummy_thread_del():
        if sys.version_info < (3, 13):
            return
        if "debugpy" not in sys.modules:
            return
    
        original_del = threading._DeleteDummyThreadOnDel.__del__
    
        def patched_del(self):
            global _debugpy_warning_printed
    
            # Teardown‑safe frame inspection
            triggered_by_debugpy = False
            try:
                frame = sys._getframe()
                while frame:
                    filename = frame.f_code.co_filename
                    if "debugpy" in filename.replace("\\", "/"):
                        triggered_by_debugpy = True
                        break
                    frame = frame.f_back
            except Exception:
                pass  # Even _getframe() may fail late in teardown
    
            if triggered_by_debugpy:
                if not _debugpy_warning_printed:
                    print(
                        f"[WARNING] Suppressed _DummyThread destructor triggered by debugpy "
                        f"to avoid Python 3.13 teardown crash. "
                        f"See issue: {_ISSUE_URL}"
                    )
                    _debugpy_warning_printed = True
                return
    
            # Call original destructor safely
            try:
                return original_del(self)
            except Exception:
                pass
    
        threading._DeleteDummyThreadOnDel.__del__ = patched_del
    
    
    _patch_debugpy_dummy_thread_del()

    How to use it

    Place this file anywhere in your project (e.g. debugpy_teardown_patch.py) and import it before starting any PySide6 threads or Spotipy/requests activity:

    import debugpy_teardown_patch

    This ensures the patch is active before debugpy begins monitoring threads.


    This workaround has been tested with:

    • Python 3.13.4
    • debugpy 1.8.19
    • VS Code Python Debugger extension 2025.18.0
    • PySide6 6.10.1
    • requests 2.32.5 / urllib3 2.6.0

    It prevents the teardown crash while keeping the rest of the system untouched.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions