Skip to content

Preserve recursively-set value in defaultdict.__missing__#7718

Merged
youknowone merged 1 commit intoRustPython:mainfrom
changjoon-park:fix-defaultdict-missing-reentrancy
Apr 29, 2026
Merged

Preserve recursively-set value in defaultdict.__missing__#7718
youknowone merged 1 commit intoRustPython:mainfrom
changjoon-park:fix-defaultdict-missing-reentrancy

Conversation

@changjoon-park
Copy link
Copy Markdown
Contributor

Background

CPython's defaultdict.__missing__ (Modules/_collectionsmodule.c::defdict_missing) calls default_factory() first; if the factory's recursion populated self[key] while running, the existing value is preserved instead of being overwritten.

RustPython ships a Python fallback at Lib/collections/_defaultdict.py (the C-implemented _collections.defaultdict is not available — see the comment at Lib/collections/__init__.py:60). That fallback unconditionally executed self[key] = val after the factory returned, overwriting any value the recursive call had already stored.

Repro

from collections import defaultdict
key = 'k'
count = 0
def fac():
    global count
    count += 1
    local = count
    if count == 1:
        d[key]   # recursive lookup — inner factory stores d[key] = 2
    return local
d = defaultdict(fac)

d[key]
# CPython 3.14: 2  (outer factory's local=1 does NOT overwrite)
# RustPython:   1  (overwrote)   (before this PR)

Fix

Add if key in self: return self[key] after the factory call, before the assignment. dict.__contains__ does not invoke __missing__, so there's no recursion risk; in the common (non-reentrant) case the check is False and behavior is unchanged.

Tests unmasked

  • test_defaultdict.TestDefaultDict.test_factory_conflict_with_set_value

Verification

  • CPython 3.14.4 byte-identical for the test's repro (result=2, count=2)
  • Normal cases unchanged: defaultdict(int) increment, defaultdict() with no factory still raises KeyError
  • No regressions across test_defaultdict, test_collections, test_dict, test_typing, test_descr (~1,116 tests)

CPython's defaultdict.__missing__ (Modules/_collectionsmodule.c::defdict_missing)
calls default_factory() first; if the factory's recursion already populated
self[key] while running, the existing value is preserved instead of being
overwritten.

RustPython ships a Python fallback at Lib/collections/_defaultdict.py
(the C _collections.defaultdict is not available). That fallback
unconditionally executed self[key] = val after the factory returned,
overwriting any value the recursive call had already stored.

Add a 'if key in self: return self[key]' guard before the assignment.
dict.__contains__ does not invoke __missing__, so there's no recursion
risk; in the common non-reentrant case the check is False and behavior
is unchanged.

Unmasks test_defaultdict.TestDefaultDict.test_factory_conflict_with_set_value.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • Lib/collections/_defaultdict.py is excluded by !Lib/**
  • Lib/test/test_defaultdict.py is excluded by !Lib/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: fda218c0-205b-4530-b9bd-f6e6fceee80a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

(module 'collections test_defaultdict' not found)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Comment thread Lib/collections/_defaultdict.py
Copy link
Copy Markdown
Contributor

@ShaharNaveh ShaharNaveh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@changjoon-park
Copy link
Copy Markdown
Contributor Author

lgtm!

thanks for the check !

@youknowone youknowone merged commit 330b18f into RustPython:main Apr 29, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants