I recently came across something in Python that seemed inconsistent, which I don’t usually expect from the language.
from collections import OrderedDict
{}.popitem()
# KeyError: 'popitem(): dictionary is empty'
OrderedDict().popitem()
# KeyError: 'dictionary is empty'
Both methods perform the same operation on an empty mapping, but the error messages are different. Is there a reason for this? Is it just because OrderedDict has its own implementation, or was this difference intentional? I know exception messages aren’t part of the API, so I’m not relying on the exact wording. I’m mainly curious if this is expected, or if making them consistent would be better.
You are expecting too much consistency between different implementers and even same person at different times. Comsider also
>>> [].pop()
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
[].pop()
~~~~~~^^
IndexError: pop from empty list
>>> set().pop()
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
set().pop()
~~~~~~~~~^^
KeyError: 'pop from an empty set'
Is OrderedDict that useful anymore? dict is ordered and that’s no longer a Cpython implementation detail.
You can also subclass dict, so there isn’t much utility in OrderedDict except for backwards compatibility.
When the big exception message changes were made a few years ago, this was still the case, so it’s likely that it was just passed over while the builtins were updated.
This exception is also hard coded into OrderedDict and not raised from the underlying dict:
class OrderedDict(dict):
... # snip
def popitem(self, last=True):
'''Remove and return a (key, value) pair from the dictionary.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
... # snip
They’re not fully equivalent: OrderedDict(a=1, b=2) != OrderedDict(b=2, a=1) (order matters), but dict(a=1, b=2) == dict(b=2, a=1) (order preserved but not used in comparing)
Always forget about that part. I usually just use all(a == b for a, b in zip(d1.items(), d2.items()) if I need to validate exact ordered matches. The OrderedDict api is definitely more clear.
Either way it seems that these just got out of sync at some point.
That makes sense. I didn’t expect every exception message in the standard library to be the same, especially with different implementations.
I asked because I was explaining the differences between dict and OrderedDict to a colleague. While showing that OrderedDict.popitem() adds the last argument to the API, we also tried calling popitem() on empty objects. My colleague thought it was strange that two similar APIs gave different KeyError messages for basically the same error, which caused some confusion in our discussion. For us, it wasn’t a big deal since we know exception messages aren’t part of the API, but I can see how this kind of inconsistency could be slightly confusing for someone new to Python who is still learning the relationship between dict and OrderedDict.
The intent of both KeyError is clear. I’m not saying code should rely on exception messages since they aren’t part of the API. I was just wondering if, in the future, someone might want to make the wording consistent, or if it’s not important enough to change. I hope you’re getting what I’m trying to convey.
The fix is incredibly simple, and small enough that you can likely just open an issue and resolve it with a 2 line PR:
Edit: I thought that maybe the popitem() was added recently, but running a git log on that bit of the code shows that it was committed by Guido on December 12, 2000… I guess the OrderedDict.popitem exception has just always been out of sync with the dict.popitem exception