Repository navigation
MutableDict |= and MutableList *= do not emit change events; changes are not persisted #13643
Description
Activity
- addedbugSomething isn't workingSomething isn't workingrobots involved 🤖issue/discussion/PR that is located using machine processes such as LLMs / fuzzers etcissue/discussion/PR that is located using machine processes such as LLMs / fuzzers etc
on Oct 4, 2026 claude on behalf of zzzeek
Reproduced: yes. Using the ORM with sqlite in-memory, I tried each in-place operator on the built-in mutable collections:
op is_modified persisted MutableDict |=False no MutableList *=False no MutableList +=True yes MutableSet |=,&=,-=,^=True yes The only gaps are the two the report names.
MutableDicthas no__ior__, sodict.__ior__runs without callingchanged().MutableListhas no__imul__, solist.__imul__doesn't call it either. Theobj.attr op= xform then assigns the same object back to the attribute, and that assignment isn't counted as a change.Versions: main (2.1.4.dev) and rel_2_0 (2.0.55) give identical results, so this is not a 2.1 regression. The operators were missed when #3853 added
MutableSet__ior__/__iand__/__ixor__/__isub__andMutableList.__iadd__. Milestone is set to 2.0.x.Severity: low-to-moderate. Changes are silently lost, but neither operator is used much on a mapped JSON/pickle attribute.
dict |=(PEP 584, Python 3.9+) is the likelier one to show up in real code.list *=is rare. The workaround is.update()for dicts, orflag_modified().Tests: these go in
test/ext/test_mutable.py:_MutableDictTestBase: add atest_operator_updatenext totest_update. It runs throughMutableWithScalarPickleTest,MutableWithScalarJSONTest,MutableColumnCopyJSONTest,MutableAssociationScalar*Test, etc._MutableListTestBase: add atest_operator_multiplynext totest_operator_extend. It runs throughMutableListWithScalarPickleTestandMutableColumnCopyArrayTest.
The existing tests don't catch this because they only cover the explicit methods plus the operators added in #3853. Nothing calls
|=on a dict or*=on a list.Fix: in
lib/sqlalchemy/ext/mutable.py:MutableDict.__ior__(self, other): callself.update(other)and returnself, the same pattern asMutableSet.__ior__.MutableList.__imul__(self, n): calllist.__imul__(self, n), thenself.changed(), and returnself.
The diff proposed in the report is essentially right. Typing will probably need the same
# type: ignore[override,misc]comments the sibling methods use.Thanks for the quick triage!
Just to clarify regarding the
robots involvedlabel: I investigated the issue and wrote the reproducer myself, but used an LLM to help translate and format the report since English is not my native language.Happy to submit a PR with the fix and tests if this is open for pull requests.
Mike Bayer has proposed a fix for this issue in the main branch:
*emit change events for MutableDict |= and MutableList = https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/7030
Mike Bayer has proposed a fix for this issue in the rel_2_0 branch:
*emit change events for MutableDict |= and MutableList = https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/7032
- addedcode review in progresscode has been provided that's in review as PR and/or gerritcode has been provided that's in review as PR and/or gerrit
on Oct 5, 2026 - added a commit that references this issue
on Oct 6, 2026 - added a commit that references this issue
on Oct 6, 2026
Describe the bug
When modifying a
MutableDictusing the in-place union operator (|=) or aMutableListusing in-place multiplication (*=), change events are not emitted. The underlying instance is not marked as modified (session.is_modified(instance) == False), and subsequentsession.commit()orsession.flush()silently skips persisting the changes to the database.MutableDictdoes not implement__ior__(introduced for Python dictionaries in PEP 584 / Python 3.9), andMutableListdoes not implement__imul__. As a result, the built-indict.__ior__andlist.__imul__run without callingself.changed().In ticket #3853, upstream added
__ior__,__iand__,__ixor__, and__isub__toMutableSet, and__iadd__(+=) toMutableListfor the exact same reason.dict |=andlist *=appear to have been overlooked.To Reproduce
Error
No error is raised; changes made via
|=and*=are silently ignored by the unit of work and discarded upon session close.Output of the snippet above:
Possible fix
Implement
__ior__inMutableDictand__imul__inMutableList:Environment