This document captures important design decisions, debugging findings, and technical notes for the Qore Python module to facilitate future debugging and development.
The module supports multiple Python versions with different internal behaviors:
- Python 3.12: Uses
python312_internals.hfor GIL tracking - Python 3.13: Uses
python313_internals.hwith updated thread state management - Python 3.14+: Uses
python314_internals.hwith free-threading support (Py_GIL_DISABLED)
Python's internal thread state tracking changed significantly across versions:
-
Python's TSS (Thread-Specific Storage): Python uses
autoTSSkeyto track the current thread state per-thread. However,PyEval_ReleaseThread()does NOT clear this TSS in Python 3.12+, leading to stale references. -
Our Tracking Variables (in
python3XX_internals.h):_qore_tss_tstate: Thread-local tracking of current thread state_qore_tss_initialized: Whether our tracking has been initialized for this thread_qore_gil_held: Whether the current thread holds the GIL
-
Why We Need Our Own Tracking:
PyGILState_Check()can return incorrect results after GIL transitionsPyGILState_GetThisThreadState()can return stale thread states- Sub-interpreter thread states can be incorrectly reported by Python's TSS
-
CRITICAL: TSS vs GIL Tracking Order:
_qore_tss_tstateis used to track the current thread state_qore_gil_heldtracks whether the GIL is actually held- For GIL-enabled Python (3.12, 3.13, 3.14), NEVER set
_qore_tss_tstatebefore acquiring the GIL - Setting TSS before GIL acquisition causes
_qore_PyCeval_GetGilLockedStatus()to return true when we don't actually hold the GIL, leading to skippedPyEval_RestoreThread()calls - Free-threading mode (
Py_GIL_DISABLED) requires TSS setup before thread state operations, so the rule is inverted there
Critical Bug Found (January 2026): The gilstate_counter field in PyThreadState
must be properly maintained. Python's PyEval_RestoreThread() asserts that
gilstate_counter > 0.
Root Cause: In QorePythonGilHelper::set(), when switching from the main interpreter's
thread state to a sub-interpreter's thread state after Py_NewInterpreter():
- The constructor incremented the MAIN interpreter's
gilstate_counter set()changednew_thread_stateto point to the SUB-interpreter's thread state- The destructor decremented the SUB-interpreter's
gilstate_counter - This caused sub-interpreter's counter to go from 1 to 0, corrupting the thread state
- Additionally, the main interpreter's counter was never decremented, causing a leak
Fix: In set(), we now:
- Decrement the MAIN interpreter's counter to balance the constructor's increment
- Increment the SUB interpreter's counter to balance the destructor's decrement
This ensures both interpreters maintain correct counter values.
These functions manage the Python execution context for Qore threads:
-
setContext() (in
QorePythonProgram.cpp):- Gets or creates a thread state for the current thread and interpreter
- Acquires the GIL if not already held
- Handles cross-interpreter thread state switching
- Returns
QorePythonThreadInfowith saved state for restoration
-
releaseContext():
- Restores the previous thread state
- Releases the GIL if it was acquired in setContext()
- Handles cross-interpreter restoration
- Every
setContext()must have a matchingreleaseContext() gilstate_countermust remain >= 1 for active thread states- Thread states must only be used with their owning interpreter
- When an interpreter is deleted, all its thread states become invalid
QorePythonGilHelperacquires GIL with main interpreter's thread state- For Python 3.13+:
releaseBeforeSubInterpreter()prepares for sub-interpreter creation Py_NewInterpreter()orPy_NewInterpreterFromConfig()creates sub-interpreter- New thread state is returned with
gilstate_counter = 1 QorePythonGilHelper::set()switches to the new thread state- On destructor, GIL is released with sub-interpreter's thread state
Thread states are cached in py_thr_map (per-PythonProgram, per-thread):
- Avoids creating new thread states for every Python call
- Thread states are reused across
setContext()/releaseContext()cycles - When a thread exits, its thread states are cleaned up
When an interpreter is deleted:
- All its thread states are freed by
zapthreads() - But Python's TSS (
autoTSSkey) may still point to the freed thread state - Next Python API call may use the stale/freed thread state
Before creating a new thread state, check if TSS points to a stale one:
- Get TSS thread state via
PyGILState_GetThisThreadState() - Check if its interpreter still exists (iterate
PyInterpreterState_Head()list) - If interpreter is invalid, the thread state is stale - clear
_status.bound_gilstate - This allows
PyThreadState_New()to properly bind the new thread state
Python 3.12 has similar TSS issues but different internal structures:
- Uses
gilstate_counterinstead of_status.bound_gilstatefor some tracking - Our tracking variables (
_qore_tss_tstate, etc.) are authoritative PyGILState_Check()results should not be trusted after GIL transitions
When Python is shutting down (python_shutdown flag is set or !Py_IsInitialized()):
QorePythonGilHelperconstructor returns early without acquiring the GIL- The
initializedflag remains false - Subsequent Python API calls will crash because there's no valid interpreter
QorePythonGilHelper::isInitialized() returns whether the GIL was successfully acquired.
Callers that create a QorePythonGilHelper and then call Python APIs must check this:
QorePythonGilHelper pgh;
if (!pgh.isInitialized()) {
// Python is shutting down - skip cleanup
return;
}
// Safe to call Python APIsThe following functions check isInitialized():
QorePythonProgram::deleteIntern()- sub-interpreter cleanup
When Python is built with free-threading support:
- There is no GIL - threads run truly concurrently
PyGILState_*APIs still exist but behave differently- Thread state attachment/detachment is critical
- Reference counting uses atomic operations
- Container operations have internal locking
Key differences in our code:
_qore_gil_heldis always false (no GIL to hold)_qore_acquire_thread_state()/_qore_release_thread_state()handle attachmentPyThreadState_Swap()internally handles_PyThreadState_Attach()
-
"thread state already initialized":
PyThreadState_New()tried to reuseinterp->_initial_threadwhenthreads.headis NULL but_status.initializedis 1. Usually caused by improper thread state cleanup. -
SIGSEGV in
_PyEvalFramePushAndInit: Thread state's data stack is corrupted, often due togilstate_counterbeing 0 whenPyEval_RestoreThread()is called. -
"non-NULL old thread state" in free-threading: Tried to attach a thread state when one was already attached. Use
PyGILState_GetThisThreadState()to check first.
Enable debug level 5 for detailed GIL tracking:
qore --debug=5 -l python your_script.q
Key debug messages to look for:
setContext() ENTRY: Shows thread state pointers and tracking stategot thread context: ShowsGIL:(PyGILState_Check),hG:(our tracking),refs:(gilstate_counter)creating new thread state: New thread state being createdPython 3.XX is_python_created_thread: Thread origin detection
When running valgrind:
- Use
qore -bto disable signals (avoids spurious errors) - Memory leaks in Python/Qore global state are expected for short-running tests
- Focus on "Invalid read/write" errors which indicate real issues
python3XX_internals.h: Version-specific Python internals accessQorePythonProgram.cpp: Main interpreter/thread state managementpython-module.cpp:QorePythonGilHelperandQorePythonReleaseGilHelperclassesQC_PythonProgram.qpp: Qore API for PythonProgram class
- Fixed
gilstate_counterbug inQorePythonGilHelper::set()causing Python 3.12 crashes - Added
Python::PythonVersionconstant - Improved stale TSS handling for Python 3.13+
- Fixed TSS/GIL tracking order issue for Python 3.13+:
- Changed
_qore_PyCeval_GetGilLockedStatus()to use_qore_gil_heldin Python 3.13 - Removed early TSS setting in
setContext()for GIL-enabled Python (only needed for free-threading)
- Changed
- Fixed
gilstate_counterleak inQorePythonGilHelper::set():- Now decrements main interpreter's counter when switching to sub-interpreter
- Added
QorePythonGilHelper::isInitialized()for shutdown-safe cleanup:deleteIntern()now checks if GIL was acquired before calling Python APIs