Repository navigation
ChainMap reverse union bypasses missing-key lookup semantics #160929
Description
Bug report
Reverse union can replace the value exposed by its right-hand ChainMap with a different value from an underlying mapping.
from collections import ChainMap, Counter
chain = ChainMap(Counter(), {"x": 5})
print(chain["x"]) # 0
print(dict(chain)) # {'x': 0}
print(dict({"x": 99} | chain)) # {'x': 5}, expected {'x': 0}Counter.__missing__ returns zero, so the first mapping successfully handles the lookup. That shadowing behavior is intentional, as discussed in #117576 and #93432. The discrepancy here is specifically the reverse union, which bypasses the values exposed by the right operand.
ChainMap.__ror__ updates a dictionary from each underlying mapping in reverse order. That combines stored entries rather than obtaining values through ChainMap.__getitem__. In contrast, dict(chain) and dict.update(chain) preserve the view's lookup semantics. The existing test_union_operators test also expects (d | cm).maps == [d | dict(cm)] for dictionary-backed chains.
The proposed fix updates the result from the ChainMap itself. It preserves key order and the returned class, while using normal per-key lookups. Defaulting mappings can therefore have their usual read side effects.
Suggested priority is P3. The incorrect result requires a chain with specialized lookup semantics and a reverse union.
Duplicate search
Searched open and closed issues and PRs for ChainMap and combinations with Counter, __ror__, union, defaultdict, __missing__, and merge. Reviewed the PEP 584 issue #80325, the implementation and review discussion in #18832, and the related lookup reports #117576 and #93432. I did not locate a report of this reverse-union discrepancy. The original implementation discussion considered updating from the view but discussed efficiency rather than this value mismatch.
CPython versions tested on
The unchanged Lib/collections/__init__.py from main commit 7515b64aa7231e8a8cf5271155829a208d789583 was loaded directly on Python 3.15.0b2 on Windows. This is a test of the exact module source, not a native build of that main commit.
A native CPython 3.16.0a1+dev build from fix commit aa591236ad337499fe80e90e6ad28178c81a8367 was also tested on Ubuntu 24.04. With the original baseline collections module restored, the minimal reproduction and both new regression tests fail for the expected values. Restoring the fixed module makes them pass. Ten runs of the five selected test modules, with hash seeds 1 through 10, each report 286 cases, 284 passes, two skips, and no failures or errors. Both new tests are present and pass in every run. Native build and validation logs.
Tool assistance
AI tools assisted the investigation, patch, and tests. The reproduction, lookup contract, related reports, and proposed diff were reviewed before submission.
Activity
gh-160929: Preserve ChainMap lookup values in reverse unions (gh-160932)