diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 5dbcac19e7a9272..630f1d4620ae11e 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -1187,7 +1187,10 @@ def __len__(self): def __getitem__(self, key): if key in self.data: - return self.data[key] + try: + return self.data[key] + except KeyError: + pass if hasattr(self.__class__, "__missing__"): return self.__class__.__missing__(self, key) raise KeyError(key) @@ -1208,7 +1211,10 @@ def __contains__(self, key): def get(self, key, default=None): if key in self: - return self[key] + try: + return self[key] + except KeyError: + pass return default diff --git a/Lib/test/test_userdict.py b/Lib/test/test_userdict.py index c60135ca5a12a8f..ee2cfe99db238db 100644 --- a/Lib/test/test_userdict.py +++ b/Lib/test/test_userdict.py @@ -307,6 +307,66 @@ def test_mixed_ior(self): self.assertIs(type(u), UserDictSubclass) self.assertIs(u, u2) + def test_matches_dict(self): + key, value, missing = object(), object(), object() + class Missing: + def __missing__(self, key): + return missing + class _Dict(dict, Missing): pass + class _UserDict(UserDict, Missing): pass + _dict = _Dict({key: value}) + _user_dict = _UserDict({key: value}) + self.assertIs(_dict[key], _user_dict[key]) + self.assertIs(_dict.get(key), _user_dict.get(key)) + self.assertIs(_dict.get(missing), _user_dict.get(missing)) + self.assertIs(_dict[missing], _user_dict[missing]) + + def test_data_delegation(self): + class Dict(dict): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.calls = [] + def __contains__(self, *args, **kwargs): + self.calls.append('__contains__') + return super().__contains__(*args, **kwargs) + def __getitem__(self, *args, **kwargs): + self.calls.append('__getitem__') + return super().__getitem__(*args, **kwargs) + def __missing__(self, *args, **kwargs): + self.calls.append('__missing__') + if hasattr(self.data, '__missing__'): + return super().__missing__(*args, **kwargs) + def get(self, *args, **kwargs): + self.calls.append('get') + return super().get(*args, **kwargs) + class _UserDict(UserDict): + def __init__(self, **kwargs): + super().__init__() + self.data = Dict(**kwargs) + def __missing__(self, key): + return 'missing' + + # get with value + _dict = _UserDict(key='value') + self.assertEqual('value', _dict.get('key')) + self.assertEqual(['__contains__', '__contains__', '__getitem__'], # LH ??? why two contains + _dict.data.calls) + + # get without value + _dict = _UserDict() + self.assertEqual(None, _dict.get('key')) + self.assertEqual(['__contains__'], _dict.data.calls) + + # getitem with value + _dict = _UserDict(key='value') + self.assertEqual('value', _dict['key']) + self.assertEqual(['__contains__', '__getitem__'], _dict.data.calls) + + # getitem without value + _dict = _UserDict() + self.assertEqual('missing', _dict['key']) + self.assertEqual(['__contains__'], _dict.data.calls) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-09-10-20-48-42.gh-issue-156544.YGR1Ig.rst b/Misc/NEWS.d/next/Library/2026-09-10-20-48-42.gh-issue-156544.YGR1Ig.rst new file mode 100644 index 000000000000000..70bd793d714244f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-10-20-48-42.gh-issue-156544.YGR1Ig.rst @@ -0,0 +1 @@ +Fix free-threading race in which UserDict.__getitem__ may not call __missing__ and UserDict.get() may not return the default.