Describe the bug
Reusing a Filter.by_ref(...) (or .by_ref_multi_target(...)) builder to construct two different property filters on the same referenced collection raises an uncaught AssertionError the second time a terminal filter method (.equal(), .greater_than(), .contains_any(), etc.) is called on it.
Root cause
_FilterByRef.by_property() (and by_id(), by_creation_time(), by_update_time(), by_ref_count()) all pass the same, shared self.__target object into the new _FilterByProperty/_FilterById/etc. instance instead of a copy:
https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L599-L601
def by_property(self, name: str, length: bool = False) -> _FilterByProperty:
"""Define a filter based on a property to be used when querying and deleting from a collection."""
return _FilterByProperty(prop=name, length=length, target=self.__target)
When a terminal method (e.g. .equal()) is later called, _FilterBase._target_path() mutates that shared target object in place:
https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L157-L170
def _target_path(self) -> _FilterTargets:
if self._target is None:
return self._property
# get last element in chain
target = self._target
while target.target is not None:
assert isinstance(target.target, _MultiTargetRef) or isinstance(
target.target, _SingleTargetRef
)
target = target.target
target.target = self._property # <-- mutates the shared _SingleTargetRef/_MultiTargetRef
return self._target
The first call sets target.target on the shared _SingleTargetRef/_MultiTargetRef instance to a plain string (the property name, e.g. "name"). If the same _FilterByRef object is then used to build a second property filter (e.g. .by_property("rating")), _target_path() walks the chain again, finds target.target is no longer None (it's now the string left over from the first filter), and the assert isinstance(target.target, _MultiTargetRef) or isinstance(target.target, _SingleTargetRef) fails because a plain string is neither, raising AssertionError.
Reproduction
import weaviate
from weaviate.classes.query import Filter
print("weaviate-client version:", weaviate.__version__)
# Build a reference filter builder once, then try to compose two different
# property conditions against the same reference (e.g. to AND them together
# later) -- a natural pattern given the library supports chaining
# Filter.by_ref(...).by_property(...) directly.
ref = Filter.by_ref("hasCategory")
print("Building first filter (name == 'Electronics') ...")
f1 = ref.by_property("name").equal("Electronics")
print(" OK:", f1.target, f1.operator, f1.value)
print("Building second filter (rating > 4) on the SAME ref object ...")
f2 = ref.by_property("rating").greater_than(4)
print(" OK:", f2.target, f2.operator, f2.value)
Output
weaviate-client version: 4.23.0
Building first filter (name == 'Electronics') ...
OK: link_on='hasCategory' target='name' _Operator.EQUAL Electronics
Building second filter (rating > 4) on the SAME ref object ...
Traceback (most recent call last):
File "repro_filter_bug.py", line 31, in <module>
f2 = ref.by_property("rating").greater_than(4)
File ".../weaviate/collections/classes/filters.py", line 274, in greater_than
target=self._target_path(),
~~~~~~~~~~~~~~~~~^^
File ".../weaviate/collections/classes/filters.py", line 164, in _target_path
assert isinstance(target.target, _MultiTargetRef) or isinstance(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
target.target, _SingleTargetRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
AssertionError
As a sanity check, calling Filter.by_ref("hasCategory") twice (a fresh builder object for each property filter) works fine and produces the expected combined filter -- so the bug is specifically about reusing one _FilterByRef instance, not about referencing the same link_on string:
f1 = Filter.by_ref("hasCategory").by_property("name").equal("Electronics")
f2 = Filter.by_ref("hasCategory").by_property("rating").greater_than(4)
combined = f1 & f2 # works fine
Expected behavior
Filter.by_ref(...) should be safely reusable to build multiple, independent property/id/count/time filters against the same reference target (this is exactly the ergonomic use case the chaining API seems designed to support), without raising an AssertionError and without silently corrupting the target chain of previously-built filter objects.
Actual behavior
The first terminal filter built from a _FilterByRef instance permanently mutates the shared target-ref object. Any subsequent filter built from the same _FilterByRef instance raises AssertionError inside _target_path().
Environment
weaviate-client version: 4.23.0 (installed fresh from PyPI)
- Python 3.13.3
- Verified against the current
main branch of this repo: fetched weaviate/collections/classes/filters.py fresh via raw.githubusercontent.com/weaviate/weaviate-python-client/main/... and confirmed it is byte-identical (module content, aside from line endings) to both the locally cloned main and the installed 4.23.0 package where the crash was reproduced above. The bug is present on current main.
Suggested fix
_FilterByRef.by_property() / by_id() / by_creation_time() / by_update_time() / by_ref_count() should pass a deep copy of self.__target (or otherwise avoid sharing mutable state), so that each derived filter builder gets its own independent target chain instead of mutating a shared one.
Describe the bug
Reusing a
Filter.by_ref(...)(or.by_ref_multi_target(...)) builder to construct two different property filters on the same referenced collection raises an uncaughtAssertionErrorthe second time a terminal filter method (.equal(),.greater_than(),.contains_any(), etc.) is called on it.Root cause
_FilterByRef.by_property()(andby_id(),by_creation_time(),by_update_time(),by_ref_count()) all pass the same, sharedself.__targetobject into the new_FilterByProperty/_FilterById/etc. instance instead of a copy:https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L599-L601
When a terminal method (e.g.
.equal()) is later called,_FilterBase._target_path()mutates that shared target object in place:https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L157-L170
The first call sets
target.targeton the shared_SingleTargetRef/_MultiTargetRefinstance to a plain string (the property name, e.g."name"). If the same_FilterByRefobject is then used to build a second property filter (e.g..by_property("rating")),_target_path()walks the chain again, findstarget.targetis no longerNone(it's now the string left over from the first filter), and theassert isinstance(target.target, _MultiTargetRef) or isinstance(target.target, _SingleTargetRef)fails because a plain string is neither, raisingAssertionError.Reproduction
Output
As a sanity check, calling
Filter.by_ref("hasCategory")twice (a fresh builder object for each property filter) works fine and produces the expected combined filter -- so the bug is specifically about reusing one_FilterByRefinstance, not about referencing the samelink_onstring:Expected behavior
Filter.by_ref(...)should be safely reusable to build multiple, independent property/id/count/time filters against the same reference target (this is exactly the ergonomic use case the chaining API seems designed to support), without raising anAssertionErrorand without silently corrupting the target chain of previously-built filter objects.Actual behavior
The first terminal filter built from a
_FilterByRefinstance permanently mutates the shared target-ref object. Any subsequent filter built from the same_FilterByRefinstance raisesAssertionErrorinside_target_path().Environment
weaviate-clientversion: 4.23.0 (installed fresh from PyPI)mainbranch of this repo: fetchedweaviate/collections/classes/filters.pyfresh viaraw.githubusercontent.com/weaviate/weaviate-python-client/main/...and confirmed it is byte-identical (module content, aside from line endings) to both the locally clonedmainand the installed 4.23.0 package where the crash was reproduced above. The bug is present on currentmain.Suggested fix
_FilterByRef.by_property()/by_id()/by_creation_time()/by_update_time()/by_ref_count()should pass a deep copy ofself.__target(or otherwise avoid sharing mutable state), so that each derived filter builder gets its own independent target chain instead of mutating a shared one.