From 8f2bc6ab7c1f38c765f549f48aef3a90a8ab0e6a Mon Sep 17 00:00:00 2001 From: Yaniv Michael Kaul Date: Mon, 16 Mar 2026 17:53:41 +0200 Subject: [PATCH 1/2] perf: replace KeyError exception with dict.get() in BoundStatement.bind() Replace try/except KeyError with dict.get() + sentinel pattern in the per-column binding loop of BoundStatement.bind(). This loop runs once per column per execute() call for dict-style bindings, making it a hot path. Using dict.get() avoids the overhead of raising and catching KeyError for every missing/optional column. The sentinel object (_BIND_SENTINEL) is necessary to distinguish a missing key from an explicit None value in the bound dict. --- cassandra/query.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/cassandra/query.py b/cassandra/query.py index 6c6878fdb4..8851b4e626 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -49,6 +49,9 @@ Only valid when using native protocol v4+ """ +_BIND_SENTINEL = object() +"""Sentinel for dict.get() in BoundStatement.bind() to distinguish missing keys from None values.""" + NON_ALPHA_REGEX = re.compile('[^a-zA-Z0-9]') START_BADCHAR_REGEX = re.compile('^[^a-zA-Z0-9]*') END_BADCHAR_REGEX = re.compile('[^a-zA-Z0-9_]*$') @@ -608,15 +611,15 @@ def bind(self, values): # sort values accordingly for col in col_meta: - try: - values.append(values_dict[col.name]) - except KeyError: - if proto_version >= 4: - values.append(UNSET_VALUE) - else: - raise KeyError( - 'Column name `%s` not found in bound dict.' % - (col.name)) + val = values_dict.get(col.name, _BIND_SENTINEL) + if val is not _BIND_SENTINEL: + values.append(val) + elif proto_version >= 4: + values.append(UNSET_VALUE) + else: + raise KeyError( + 'Column name `%s` not found in bound dict.' % + (col.name)) value_len = len(values) col_meta_len = len(col_meta) From 68dbf6c3e3df9abffb77da7d1a4b4f16b670bb28 Mon Sep 17 00:00:00 2001 From: Yaniv Michael Kaul Date: Thu, 26 Mar 2026 09:45:28 +0200 Subject: [PATCH 2/2] fix: preserve dict subclass semantics in bind fast path --- cassandra/query.py | 18 ++++++++++++++---- tests/unit/test_parameter_binding.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cassandra/query.py b/cassandra/query.py index 8851b4e626..9925c2a1ab 100644 --- a/cassandra/query.py +++ b/cassandra/query.py @@ -608,13 +608,23 @@ def bind(self, values): if isinstance(values, dict): values_dict = values values = [] + plain_dict = type(values_dict) is dict # sort values accordingly for col in col_meta: - val = values_dict.get(col.name, _BIND_SENTINEL) - if val is not _BIND_SENTINEL: - values.append(val) - elif proto_version >= 4: + if plain_dict: + val = values_dict.get(col.name, _BIND_SENTINEL) + if val is not _BIND_SENTINEL: + values.append(val) + continue + else: + try: + values.append(values_dict[col.name]) + continue + except KeyError: + pass + + if proto_version >= 4: values.append(UNSET_VALUE) else: raise KeyError( diff --git a/tests/unit/test_parameter_binding.py b/tests/unit/test_parameter_binding.py index 5416ac461d..be5f98ace6 100644 --- a/tests/unit/test_parameter_binding.py +++ b/tests/unit/test_parameter_binding.py @@ -184,6 +184,14 @@ def test_unset_value(self): with pytest.raises(ValueError): self.bound.bind((0, 0, 0, UNSET_VALUE)) + def test_dict_subclass_missing_value(self): + class MissingDict(dict): + def __missing__(self, key): + return 0 + + self.bound.bind(MissingDict({'rk0': 0, 'rk1': 0, 'ck0': 0})) + assert self.bound.values == [b'\x00' * 4] * 4 + class BoundStatementTestV4(BoundStatementTestV3): protocol_version = 4 @@ -213,6 +221,14 @@ def test_unset_value(self): self.bound.bind((0, 0, 0, UNSET_VALUE)) assert self.bound.values[-1] == UNSET_VALUE + def test_dict_subclass_missing_value(self): + class MissingDict(dict): + def __missing__(self, key): + return 0 + + self.bound.bind(MissingDict({'rk0': 0, 'rk1': 0, 'ck0': 0})) + assert self.bound.values == [b'\x00' * 4] * 4 + class BoundStatementTestV5(BoundStatementTestV4): protocol_version = 5