Bug report
Bug description:
What happened?
collections.OrderedDict.pop() can leave an OrderedDict in an internally inconsistent state if a key's __eq__ implementation returns different results across successive comparisons.
Unlike dict.pop(), OrderedDict.pop() performs multiple independent lookups internally. If those lookups disagree, the linked list and the underlying dictionary can become desynchronized.
Minimal reproducer:
from collections import OrderedDict
class K:
def __init__(self):
self.calls = 0
def __hash__(self):
return 12345
def __eq__(self, other):
self.calls += 1
return self.calls <= 2
k1 = K()
k2 = K()
od = OrderedDict()
od[k1] = "value"
sentinel = object()
found = od.pop(k2, sentinel)
print(found is sentinel)
print(len(od))
print(list(od))
print(list(od.keys()))
print(k1 in od)
print(od[k1])
try:
print(od.popitem())
except Exception as e:
print(type(e).__name__, e)
try:
od.move_to_end(k1)
except Exception as e:
print(type(e).__name__, e)
Observed output:
True
1
[]
[]
True
value
KeyError 'dictionary is empty'
KeyError <__main__.K object at ...>
The container reports a length of 1 and the key is still retrievable by lookup, but iteration reports no keys and operations such as popitem() and move_to_end() behave as though the container is empty.
For comparison, performing the equivalent operation on a plain dict leaves the container in a consistent state.
Expected behavior:
OrderedDict should remain internally consistent after pop().
- Even if stateful
__eq__ implementations violate normal expectations, the container should not end up in a split state where the dictionary and linked list disagree.
This appears to be distinct from the recent crash in OrderedDict.pop() when no default value is supplied. Supplying a default avoids the NULL dereference path while still reproducing this container desynchronization.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Bug report
Bug description:
What happened?
collections.OrderedDict.pop()can leave anOrderedDictin an internally inconsistent state if a key's__eq__implementation returns different results across successive comparisons.Unlike
dict.pop(),OrderedDict.pop()performs multiple independent lookups internally. If those lookups disagree, the linked list and the underlying dictionary can become desynchronized.Minimal reproducer:
Observed output:
The container reports a length of 1 and the key is still retrievable by lookup, but iteration reports no keys and operations such as
popitem()andmove_to_end()behave as though the container is empty.For comparison, performing the equivalent operation on a plain
dictleaves the container in a consistent state.Expected behavior:
OrderedDictshould remain internally consistent afterpop().__eq__implementations violate normal expectations, the container should not end up in a split state where the dictionary and linked list disagree.This appears to be distinct from the recent crash in
OrderedDict.pop()when no default value is supplied. Supplying a default avoids the NULL dereference path while still reproducing this container desynchronization.CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux