Expected Behavior
After a Python algorithm or Python-dependent test completes, LEAN should shut down the embedded Python runtime and exit cleanly.
A successful algorithm or test should not subsequently be reported as failed because of an exception on the .NET finalizer thread.
This should hold when:
- a supported CPython runtime is explicitly configured;
- tests are executed serially;
- all required data directories are present;
- no Interactive Brokers or other brokerage connection is used; and
- no earlier algorithm or test failure occurred.
Actual Behavior
During Python shutdown, the process can abort with:
Unhandled exception. System.InvalidOperationException:
GIL must always be released, and it must be released from the same thread that acquired it.
at Python.Runtime.Py.GILState.Finalize()
at System.GC.RunFinalizers()
The failure occurs after the Python work has completed and during runtime shutdown. Depending on how the process is launched, the observed exit is -6, 134, or an aborted testhost.
The current PythonInitializer.Shutdown() path obtains a GIL handle before calling PythonEngine.Shutdown():
/var pyLock = Py.GIL();
Log.Trace("PythonInitializer.Shutdown(): calling engine shutdown...");
PythonEngine.Shutdown();
The handle is not disposed or otherwise suppressed before the Python runtime shuts down. Python.NET performs managed garbage collection during or after shutdown, and the remaining GILState finalizer attempts to release a GIL associated with an already-shut-down runtime.
Because the exception occurs on the finalizer thread, the surrounding try/catch in PythonInitializer.Shutdown() does not prevent the process abort.
This has two observable impacts:
- a completed Python live or local algorithm exits abnormally after result handling; and
- a testhost can abort after passing test behavior, preventing reliable test reporting.
The same failure was reproduced with:
- no Financial Advisor code;
- no brokerage plugin;
- no Gateway connection;
- no orders;
- no market-data request;
- no test-runner parallelism; and
- an explicitly bound CPython 3.11 runtime.
A comparable C# lifecycle exits normally.
Related issue #9014 reported the same stack trace but was closed as a local Python-configuration problem. The isolated reproduction below indicates that missing map-file warnings and Python discovery configuration are not the underlying cause.
Potential Solution
Review whether PythonEngine.Shutdown() requires the additional outer Py.GIL() handle.
A process-isolated comparison produced these results:
- shutdown with LEAN’s additional outer GIL handle reproduces the abort;
- shutdown without the redundant outer handle exits normally; and
- simply changing the handle to using var is unsafe because disposal would occur after the Python runtime has already shut down.
A potential correction is therefore to remove the redundant outer GIL acquisition, subject to verification across LEAN’s supported operating systems and Python versions.
A regression test should run in a child process because an unhandled finalizer exception terminates the entire testhost. The child process should:
- initialize Python;
- shut it down through the production PythonInitializer path;
- force pending managed finalizers if necessary; and
- assert an exit code of zero with no Py.GILState.Finalize() exception.
The test should also verify that initialization and shutdown remain safe when invoked through the normal LEAN Engine lifecycle.
Reproducing the Problem
A minimal Python.NET reproduction of the pattern used by the current shutdown path is:
using Python.Runtime;
Runtime.PythonDLL = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");
PythonEngine.Initialize();
PythonEngine.BeginAllowThreads();
var pyLock = Py.GIL();
PythonEngine.Shutdown();
GC.Collect();
GC.WaitForPendingFinalizers();
Run the program in its own process with a valid CPython shared library:
export PYTHONHOME=/path/to/python-3.11
export PYTHONNET_PYDLL=/path/to/python-3.11/lib/libpython3.11.so
dotnet run
Observed result:
Unhandled exception. System.InvalidOperationException:
GIL must always be released, and it must be released from the same thread that acquired it.
at Python.Runtime.Py.GILState.Finalize()
at System.GC.RunFinalizers()
The LEAN-level behavior can also be observed by:
- checking out current master;
- explicitly configuring CPython 3.11 through PYTHONHOME and PYTHONNET_PYDLL;
- building LEAN;
- running a minimal Python algorithm that terminates normally; and
- observing the process after normal algorithm result handling reaches Python shutdown.
- The algorithm completes its intended work, but the process subsequently aborts from the GIL finalizer.
System Information
Reproduced with:
LEAN master: d865a40b28c105d0c4d47453086c8f8f86e853e3
Operating system: Ubuntu 26.04, x86-64
Kernel: Linux 7.0.0-29-generic
.NET SDK: 10.0.110
CPython: 3.11.11
QuantConnect.pythonnet: 2.0.65
Python binding: explicit PYTHONNET_PYDLL to libpython3.11.so
The same isolated behavior was also observed with QuantConnect.pythonnet 2.0.64.
No missing Python library, implicit runtime discovery, test parallelization, brokerage connection, or market-data dependency is required to reproduce the exception.
Checklist
Expected Behavior
After a Python algorithm or Python-dependent test completes, LEAN should shut down the embedded Python runtime and exit cleanly.
A successful algorithm or test should not subsequently be reported as failed because of an exception on the .NET finalizer thread.
This should hold when:
Actual Behavior
During Python shutdown, the process can abort with:
The failure occurs after the Python work has completed and during runtime shutdown. Depending on how the process is launched, the observed exit is -6, 134, or an aborted testhost.
The current PythonInitializer.Shutdown() path obtains a GIL handle before calling PythonEngine.Shutdown():
The handle is not disposed or otherwise suppressed before the Python runtime shuts down. Python.NET performs managed garbage collection during or after shutdown, and the remaining GILState finalizer attempts to release a GIL associated with an already-shut-down runtime.
Because the exception occurs on the finalizer thread, the surrounding try/catch in PythonInitializer.Shutdown() does not prevent the process abort.
This has two observable impacts:
The same failure was reproduced with:
A comparable C# lifecycle exits normally.
Related issue #9014 reported the same stack trace but was closed as a local Python-configuration problem. The isolated reproduction below indicates that missing map-file warnings and Python discovery configuration are not the underlying cause.
Potential Solution
Review whether PythonEngine.Shutdown() requires the additional outer Py.GIL() handle.
A process-isolated comparison produced these results:
A potential correction is therefore to remove the redundant outer GIL acquisition, subject to verification across LEAN’s supported operating systems and Python versions.
A regression test should run in a child process because an unhandled finalizer exception terminates the entire testhost. The child process should:
The test should also verify that initialization and shutdown remain safe when invoked through the normal LEAN Engine lifecycle.
Reproducing the Problem
A minimal Python.NET reproduction of the pattern used by the current shutdown path is:
Run the program in its own process with a valid CPython shared library:
Observed result:
The LEAN-level behavior can also be observed by:
System Information
Reproduced with:
The same isolated behavior was also observed with QuantConnect.pythonnet 2.0.64.
No missing Python library, implicit runtime discovery, test parallelization, brokerage connection, or market-data dependency is required to reproduce the exception.
Checklist
masterbranch