Summary
Context: The TypedSearchAttributes class in temporalio/common.py implements a collection-like interface for managing workflow search attributes with type-safe key-value pairs.
Bug: The __contains__ method incorrectly returns False when checking for keys that have falsy values (0, False, 0.0).
Actual vs. expected: The method evaluates the truthiness of the value instead of checking for key existence, violating Python’s in operator semantics which should return True whenever a key exists regardless of its value.
Impact: In temporalio/client.py, this causes attributes with falsy values to be incorrectly duplicated in both typed and untyped search attribute collections, leading to potential data corruption and query failures.
Code with bug
def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(v for k, v in self if k == key)
def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(v for k, v in self if k == key)
def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(v for k, v in self if k == key)
The expression any(v for k, v in self if k == key) yields the value v for matching keys, then evaluates any() on those values. When a matching key has a falsy value, any() returns False despite the key existing.
Test demonstration
A simple test demonstrates the bug across all falsy value types:
def buggy_contains(search_attributes, key):
"""The buggy implementation"""
return any(v for k, v in search_attributes if k == key)
def correct_contains(search_attributes, key):
"""The correct implementation"""
return any(k == key for k, v in search_attributes)def buggy_contains(search_attributes, key):
"""The buggy implementation"""
return any(v for k, v in search_attributes if k == key)
def correct_contains(search_attributes, key):
"""The correct implementation"""
return any(k == key for k, v in search_attributes)def buggy_contains(search_attributes, key):
"""The buggy implementation"""
return any(v for k, v in search_attributes if k == key)
def correct_contains(search_attributes, key):
"""The correct implementation"""
return any(k == key for k, v in search_attributes)Test results:
Test Case 1: Integer value of 0
----------------------------------------
Search attributes: [('int_key', 0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 2: Boolean value of False
----------------------------------------
Search attributes: [('bool_key', False)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 3: Float value of 0.0
----------------------------------------
Search attributes: [('float_key', 0.0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 4: Truthy value (control)
----------------------------------------
Search attributes: [('int_key', 42)]
Test Case 1: Integer value of 0
----------------------------------------
Search attributes: [('int_key', 0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 2: Boolean value of False
----------------------------------------
Search attributes: [('bool_key', False)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 3: Float value of 0.0
----------------------------------------
Search attributes: [('float_key', 0.0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 4: Truthy value (control)
----------------------------------------
Search attributes: [('int_key', 42)]
Test Case 1: Integer value of 0
----------------------------------------
Search attributes: [('int_key', 0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 2: Boolean value of False
----------------------------------------
Search attributes: [('bool_key', False)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 3: Float value of 0.0
----------------------------------------
Search attributes: [('float_key', 0.0)]
Buggy implementation: False
Correct implementation: True
Expected: True
Test Case 4: Truthy value (control)
----------------------------------------
Search attributes: [('int_key', 42)]
The bug manifests consistently: when a key exists with a falsy value, the buggy implementation returns False while the correct implementation returns True.
Recommended fix
Change the implementation to check for key existence rather than value truthiness:
def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(k == key for k, v in self) def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(k == key for k, v in self) def __contains__(self, key: object) -> bool:
"""
Check whether this search attribute contains the given key.
This uses key equality so the key must be the same name and type.
"""
return any(k == key for k, v in self) This ensures the method returns True whenever a matching key is found, regardless of the associated value.
The bug affects production code in temporalio/client.py where the following logic depends on correct __contains__ behavior:
ntyped_not_in_typed = {
k: v
for k, v in self.untyped_search_attributes.items()
if k not in self.typed_search_attributes
}ntyped_not_in_typed = {
k: v
for k, v in self.untyped_search_attributes.items()
if k not in self.typed_search_attributes
}ntyped_not_in_typed = {
k: v
for k, v in self.untyped_search_attributes.items()
if k not in self.typed_search_attributes
}After the fix, this code will correctly exclude attributes that exist in typed search attributes, even when they have falsy values.