Summary
Calling an overloaded .NET method from Python with an empty **kwargs dict
(e.g. obj.Method(arg, **{})) throws an unhandled
System.IndexOutOfRangeException inside
MethodBinder.CheckMethodArgumentsMatch instead of binding normally (as the
equivalent positional call obj.Method(arg) does) or raising a clean
TypeError.
Because the exception is an unhandled native IndexOutOfRangeException, it
tears down the host process rather than surfacing as a catchable Python error.
In our case (the LEAN engine) this crashes the algorithm during
Initialize(), before any user output.
Environment
- QuantConnect/pythonnet fork (bundled with LEAN engine v2.5.0.0, 64-bit)
- Python 3.11.14 (GCC 11.2.0), Linux
- .NET runtime as shipped with the above
(Please confirm the exact pythonnet commit on the fork; the crash is in
MethodBinder.CheckMethodArgumentsMatch / MethodBinder.Bind.)
Reproduction (confirmed, via LEAN QCAlgorithm.get_parameter)
QCAlgorithm.GetParameter is overloaded:
string GetParameter(string name);
string GetParameter(string name, string defaultValue);
int GetParameter(string name, int defaultValue);
double GetParameter(string name, double defaultValue);
A single-file algorithm that wraps get_parameter and forwards the call with
*args, **kwargs (a common Python decorator/monkeypatch idiom) crashes on the
first invocation:
# region imports
from AlgorithmImports import *
# endregion
class MultiStrategyAlgorithm(QCAlgorithm):
def initialize(self):
# Wrap the C# get_parameter, then forward positionally + **kwargs.
# Every caller invokes get_parameter(name) with a single positional
# arg, so kwargs is ALWAYS an empty dict here -- yet forwarding it
# still crashes the binder.
original = self.get_parameter
def recording_get_parameter(name, *args, **kwargs):
return original(name, *args, **kwargs) # <-- crashes
self.get_parameter = recording_get_parameter
# First call through the wrapper -> System.IndexOutOfRangeException
self.get_parameter("probe-network")
original(name, *args, **kwargs) with an empty kwargs → crash
original(name) (no **kwargs) → works
self.get_parameter("probe-network") called directly → works
So the trigger is purely the presence of a (borrowed, empty) keyword-args dict
on the call, which routes binding through the kwargDict code path.
Distilled trigger (pure pythonnet — please confirm)
The above reduces to: any overloaded method whose overloads differ in
parameter count, invoked with an empty kwargs mapping.
public class Overloaded
{
public string Get(string name) => name;
public string Get(string name, string def) => def;
public int Get(string name, int def) => def;
}
o = Overloaded()
o.Get("x") # OK
o.Get("x", **{}) # System.IndexOutOfRangeException in CheckMethodArgumentsMatch
Actual behavior — traceback
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
at Python.Runtime.MethodBinder.CheckMethodArgumentsMatch(Int32 clrArgCount, Int32 pyArgCount, Dictionary`2 kwargDict, ParameterInfo[] parameterInfo, String[] parameterNames, Boolean& paramsArray, ArrayList& defaultArgList)
at Python.Runtime.MethodBinder.Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info)
at Python.Runtime.MethodBinder.Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo)
at Python.Runtime.MethodBinder.Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info)
at Python.Runtime.MethodObject.Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase info)
at Python.Runtime.MethodBinding.tp_call(BorrowedReference ob, BorrowedReference args, BorrowedReference kw)
Expected behavior
Forwarding an empty **kwargs should be equivalent to the positional-only call:
the method should bind and return normally. At minimum, an unresolvable
kwarg/overload combination should raise a Python-catchable exception (e.g.
TypeError/ArgumentException), never an unhandled
IndexOutOfRangeException that crashes the host.
Analysis / pointer
The fault is in MethodBinder.CheckMethodArgumentsMatch (called from
Bind) when a non-null kwargDict is present. It appears to index
parameterInfo / parameterNames out of range while reconciling positional
args, the kwargs dict, and overloads of differing arity — even when the kwargs
dict is empty. An empty keyword mapping should be treated as no keyword
arguments (fast path), and the bounds arithmetic on the kwargDict path should
be guarded.
Notes
- Found via the LEAN engine; the minimal single-file reproduction above is
available as a standalone project.
- Impact is elevated because the exception is unhandled at the native boundary:
it does not propagate as a Python exception, so callers cannot try/except
it, and long-running hosts crash outright.
Summary
Calling an overloaded .NET method from Python with an empty
**kwargsdict(e.g.
obj.Method(arg, **{})) throws an unhandledSystem.IndexOutOfRangeExceptioninsideMethodBinder.CheckMethodArgumentsMatchinstead of binding normally (as theequivalent positional call
obj.Method(arg)does) or raising a cleanTypeError.Because the exception is an unhandled native
IndexOutOfRangeException, ittears down the host process rather than surfacing as a catchable Python error.
In our case (the LEAN engine) this crashes the algorithm during
Initialize(), before any user output.Environment
(Please confirm the exact pythonnet commit on the fork; the crash is in
MethodBinder.CheckMethodArgumentsMatch/MethodBinder.Bind.)Reproduction (confirmed, via LEAN
QCAlgorithm.get_parameter)QCAlgorithm.GetParameteris overloaded:A single-file algorithm that wraps
get_parameterand forwards the call with*args, **kwargs(a common Python decorator/monkeypatch idiom) crashes on thefirst invocation:
original(name, *args, **kwargs)with an emptykwargs→ crashoriginal(name)(no**kwargs) → worksself.get_parameter("probe-network")called directly → worksSo the trigger is purely the presence of a (borrowed, empty) keyword-args dict
on the call, which routes binding through the
kwargDictcode path.Distilled trigger (pure pythonnet — please confirm)
The above reduces to: any overloaded method whose overloads differ in
parameter count, invoked with an empty kwargs mapping.
Actual behavior — traceback
Expected behavior
Forwarding an empty
**kwargsshould be equivalent to the positional-only call:the method should bind and return normally. At minimum, an unresolvable
kwarg/overload combination should raise a Python-catchable exception (e.g.
TypeError/ArgumentException), never an unhandledIndexOutOfRangeExceptionthat crashes the host.Analysis / pointer
The fault is in
MethodBinder.CheckMethodArgumentsMatch(called fromBind) when a non-nullkwargDictis present. It appears to indexparameterInfo/parameterNamesout of range while reconciling positionalargs, the kwargs dict, and overloads of differing arity — even when the kwargs
dict is empty. An empty keyword mapping should be treated as no keyword
arguments (fast path), and the bounds arithmetic on the
kwargDictpath shouldbe guarded.
Notes
available as a standalone project.
it does not propagate as a Python exception, so callers cannot
try/exceptit, and long-running hosts crash outright.