Description
OrderedMapSerializedKey (cassandra/util.py) implements the collections.abc.Mapping interface, so callers naturally use .get(key) the way they would on a plain dict — expecting it to return None (or a supplied default) for a missing/invalid key. Instead, an unhandled AttributeError escapes from deep inside the CQL type serializer whenever the lookup key fails to serialize with the map's key type, crashing the caller instead of behaving like dict.get().
Steps to reproduce
Any code path that calls .get(key) on a driver-returned map<text, ...> value with a key that isn't a str (most commonly None, e.g. because the caller derived the key from a still-refreshing piece of cluster metadata) triggers the crash:
from cassandra.util import OrderedMapSerializedKey
from cassandra.cqltypes import UTF8Type
m = OrderedMapSerializedKey(UTF8Type, protocol_version=4)
m._insert_unchecked('dc1', UTF8Type.serialize('dc1', 4), 3)
m.get(None)
Traceback (most recent call last):
File "cassandra/util.py", line 725, in cassandra.util.OrderedMap.__getitem__
File "cassandra/util.py", line 790, in cassandra.util.OrderedMapSerializedKey._serialize_key
File "cassandra/cqltypes.py", line 785, in cassandra.cqltypes.UTF8Type.serialize
AttributeError: 'NoneType' object has no attribute 'encode'
Root cause
Mapping.get() (the mixin used by OrderedMap) is implemented roughly as:
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
OrderedMap.__getitem__ looks up self._index[self._serialize_key(key)]. For OrderedMapSerializedKey, _serialize_key calls self.cass_key_type.serialize(key, self.protocol_version) directly (cassandra/util.py:790), with no exception handling. When the key type is UTF8Type and the supplied key is not a string (e.g. None), UTF8Type.serialize (cassandra/cqltypes.py:782-788) only catches UnicodeDecodeError — it lets AttributeError (from None.encode(...)) or other serialization errors propagate unchanged:
@staticmethod
def serialize(ustr, protocol_version):
try:
return ustr.encode('utf-8')
except UnicodeDecodeError:
# already utf-8
return ustr
Because _serialize_key doesn't translate serialization failures into KeyError, Mapping.get()'s except KeyError never fires, and the raw AttributeError (or whatever the underlying type's serialize() happens to raise for a bad key) escapes to the caller. This defeats the whole point of using .get() and is surprising for a class that advertises the standard Mapping API.
Expected behavior
OrderedMapSerializedKey.get(key) (and __getitem__/__contains__) should behave like dict.get()/in for any key that cannot possibly be present — i.e. a key that fails to serialize against the map's declared key type should simply be treated as "not found" (KeyError from __getitem__, default from .get(), False from in), rather than letting an unrelated, type-specific low-level exception (AttributeError, or others depending on the CQL type) bubble up.
Suggested fix
In OrderedMapSerializedKey._serialize_key (cassandra/util.py), wrap the call to self.cass_key_type.serialize(...) and translate serialization failures (AttributeError, TypeError, struct.error, etc.) into a KeyError, e.g.:
def _serialize_key(self, key):
try:
return self.cass_key_type.serialize(key, self.protocol_version)
except Exception as exc:
raise KeyError(key) from exc
This keeps OrderedMap.__getitem__'s existing except KeyError (and therefore Mapping.get()/__contains__) working as documented, for any key type mismatch — not just None.
How we hit this
We hit this in ScyllaDB Cluster Tests (SCT), whose DataCenterTopologyRfControl._get_keyspaces_to_decrease_rf() reads a per-datacenter replication factor from a driver-returned map<text, int> (NetworkTopologyStrategy replication settings) with:
rf = replication.get(self.datacenter)
if rf is None:
... # handle "datacenter not in this keyspace's RF map"
self.datacenter is normally a str, but in a race window while the driver's cluster metadata was mid-refresh (the client observed a "host rediscovery flood" shortly before this), the caller's cached datacenter value resolved to None for one call. The intent of the replication.get(self.datacenter) / is None check was exactly to safely handle a datacenter that isn't a key in the map — but instead of returning None as dict.get() would, the call crashed with:
AttributeError: 'NoneType' object has no attribute 'encode'
File "sdcm/utils/replication_strategy_utils.py", line 184, in _get_keyspaces_to_decrease_rf
rf = replication.get(self.datacenter)
File "cassandra/util.py", line 725, in cassandra.util.OrderedMap.__getitem__
File "cassandra/util.py", line 790, in cassandra.util.OrderedMapSerializedKey._serialize_key
File "cassandra/cqltypes.py", line 785, in cassandra.cqltypes.UTF8Type.serialize
AttributeError: 'NoneType' object has no attribute 'encode'
This has recurred multiple times across several longevity_test.LongevityTest.test_custom_time runs against Scylla 2026.3.0~rc0 (internal tracking: SCT-695), always with .get() called with a non-str key on a map<text, ...> result. We are also implementing a client-side guard in SCT (never pass a non-str/None key into .get()), but the driver's .get() should not be able to crash like this regardless of caller input.
Environment
cassandra-driver (ScyllaDB python-driver), current master
- Observed against Scylla
2026.3.0~rc0
- Python 3.x
Description
OrderedMapSerializedKey(cassandra/util.py) implements thecollections.abc.Mappinginterface, so callers naturally use.get(key)the way they would on a plaindict— expecting it to returnNone(or a supplied default) for a missing/invalid key. Instead, an unhandledAttributeErrorescapes from deep inside the CQL type serializer whenever the lookup key fails to serialize with the map's key type, crashing the caller instead of behaving likedict.get().Steps to reproduce
Any code path that calls
.get(key)on a driver-returnedmap<text, ...>value with a key that isn't astr(most commonlyNone, e.g. because the caller derived the key from a still-refreshing piece of cluster metadata) triggers the crash:Root cause
Mapping.get()(the mixin used byOrderedMap) is implemented roughly as:OrderedMap.__getitem__looks upself._index[self._serialize_key(key)]. ForOrderedMapSerializedKey,_serialize_keycallsself.cass_key_type.serialize(key, self.protocol_version)directly (cassandra/util.py:790), with no exception handling. When the key type isUTF8Typeand the supplied key is not a string (e.g.None),UTF8Type.serialize(cassandra/cqltypes.py:782-788) only catchesUnicodeDecodeError— it letsAttributeError(fromNone.encode(...)) or other serialization errors propagate unchanged:Because
_serialize_keydoesn't translate serialization failures intoKeyError,Mapping.get()'sexcept KeyErrornever fires, and the rawAttributeError(or whatever the underlying type'sserialize()happens to raise for a bad key) escapes to the caller. This defeats the whole point of using.get()and is surprising for a class that advertises the standardMappingAPI.Expected behavior
OrderedMapSerializedKey.get(key)(and__getitem__/__contains__) should behave likedict.get()/infor any key that cannot possibly be present — i.e. a key that fails to serialize against the map's declared key type should simply be treated as "not found" (KeyErrorfrom__getitem__,defaultfrom.get(),Falsefromin), rather than letting an unrelated, type-specific low-level exception (AttributeError, or others depending on the CQL type) bubble up.Suggested fix
In
OrderedMapSerializedKey._serialize_key(cassandra/util.py), wrap the call toself.cass_key_type.serialize(...)and translate serialization failures (AttributeError,TypeError,struct.error, etc.) into aKeyError, e.g.:This keeps
OrderedMap.__getitem__'s existingexcept KeyError(and thereforeMapping.get()/__contains__) working as documented, for any key type mismatch — not justNone.How we hit this
We hit this in ScyllaDB Cluster Tests (SCT), whose
DataCenterTopologyRfControl._get_keyspaces_to_decrease_rf()reads a per-datacenter replication factor from a driver-returnedmap<text, int>(NetworkTopologyStrategyreplication settings) with:self.datacenteris normally astr, but in a race window while the driver's cluster metadata was mid-refresh (the client observed a "host rediscovery flood" shortly before this), the caller's cached datacenter value resolved toNonefor one call. The intent of thereplication.get(self.datacenter)/is Nonecheck was exactly to safely handle a datacenter that isn't a key in the map — but instead of returningNoneasdict.get()would, the call crashed with:This has recurred multiple times across several
longevity_test.LongevityTest.test_custom_timeruns against Scylla2026.3.0~rc0(internal tracking: SCT-695), always with.get()called with a non-strkey on amap<text, ...>result. We are also implementing a client-side guard in SCT (never pass a non-str/Nonekey into.get()), but the driver's.get()should not be able to crash like this regardless of caller input.Environment
cassandra-driver(ScyllaDB python-driver), currentmaster2026.3.0~rc0