16

I was playing around with the OrderedDict type in Python 3.6 and was surprised by its behaviour. When I create a simple dict like this in IPython:

d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])

I get:

{'guido': 4127, 'jack': 4098, 'sape': 4139}

as an output, which doesn't preserve the order of elements at instantiation for some reason. Now, when I create an OrderedDict from d like this:

od = OrderedDict(d)

the output is:

OrderedDict([('sape', 4139), ('guido', 4127), ('jack', 4098)])

Now I ask myself, how can the OrderedDict-constructor know about the order of elements at instantiation of d? And does it always behave the same, such that I can rely on the order of elements in the OrderedDict?

I was already reading the Python docs about dictionaries and OrderedDicts but I didn't find an answer to my question.

The output from (sys.version):

In[22]: sys.version
Out[22]: '3.6.1 (default, Apr  4 2017, 09:40:21) \n[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)]'
Chris_Rands
  • 38,994
  • 14
  • 83
  • 119
drssdinblck
  • 260
  • 2
  • 11
  • 2
    See https://stackoverflow.com/questions/39980323/dictionaries-are-ordered-in-python-3-6 – Alasdair Jul 27 '17 at 09:49
  • 3
    There is something odd going on here. `dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])` shouldn't result in that output on Python `3.6`. – Dimitris Fasarakis Hilliard Jul 27 '17 at 09:55
  • Weird. In Python 3.6.0 and onwards, a plain `dict` retains insertion order (although that's currently an implementation detail which should not be relied on), so I don't know why you got `{'guido': 4127, 'jack': 4098, 'sape': 4139}` as the output. There were beta versions of 3.6 that used the old-style `dict` that doesn't maintain order, but I expect you'd mention that if you were using such a version. – PM 2Ring Jul 27 '17 at 09:55
  • 1
    @PM2Ring I get the same on `3.6.1`. Oddly enough, if I `print(dict([...]))` in a loop, I get the correct output. I'm under the impression I'm missing something but I can't see what. Maybe IPython is doing something weird because in the normal Python REPL, it's good. – Dimitris Fasarakis Hilliard Jul 27 '17 at 09:56
  • Please print `sys.version` in your script to verify that it's actually being run by Python 3.6. – PM 2Ring Jul 27 '17 at 09:57
  • @JimFasarakisHilliard What? Are you saying you can reproduce the OP's output on 3.6.1? If so, I'm _very_ surprised... and a little scared. ;) – PM 2Ring Jul 27 '17 at 09:58
  • @PM2Ring Only in IPython's REPL for some reason (with 3.6.1), they might have done something wacky I'd guess :-) As for OP, I concur that checking `sys.version` will probably solve it. – Dimitris Fasarakis Hilliard Jul 27 '17 at 10:00
  • just added my `sys.version` (see edit) – drssdinblck Jul 27 '17 at 10:03
  • 1
    You're on IPython too, apparently. There's something wacky going on there. Try it out in the REPL and see if you could repro it. – Dimitris Fasarakis Hilliard Jul 27 '17 at 10:04
  • 1
    @JimFasarakisHilliard Interesting, sounds similar to how `pprint` behaves in Python 3.6, where it actually sorts the `dict` rather than maintain insertion order – Chris_Rands Jul 27 '17 at 10:04
  • @Chris_Rands Seems like something like that. I really hate it when IPython confuses the hell out of me with things like this. – Dimitris Fasarakis Hilliard Jul 27 '17 at 10:05
  • 2
    @JimFasarakisHilliard I guess this just emphasizes that an implementation detail should not be relied on. Maybe the question should be edited and re-opened – Chris_Rands Jul 27 '17 at 10:08
  • @JimFasarakisHilliard yeah, I can also confirm that it only happens IPython. So apparantly, this is due to IPython – drssdinblck Jul 27 '17 at 10:13
  • 1
    Is it only the way IPython *prints* the dict? Is the order conserved if you iterate on `.items()`? – Thierry Lathuille Jul 27 '17 at 10:16
  • @ThierryLathuille See https://stackoverflow.com/questions/45347084/how-can-ordereddict-know-about-the-element-order-of-an-already-instantiated-dict#comment77655759_45347084 – PM 2Ring Jul 27 '17 at 10:18
  • @ThierryLathuille Yes, iterating behaves as I would an OrderedDict expect to behave. – drssdinblck Jul 27 '17 at 10:21
  • @Chris_Rands `pprint` has always done that, so I guess disabling that sorting functionality would break backwards compatibility. `pprint` can be handy, but it's a shame that there's not much you can do to control its behaviour. – PM 2Ring Jul 27 '17 at 11:11
  • 1
    @PM2Ring Agreed, and I don't think `pprint` should behave differently in Python 3.6, but if (as is likely) `dict` becomes guaranteed as ordered in 3.7 then this would create an issue, and something would have to change I guess – Chris_Rands Jul 27 '17 at 11:22

3 Answers3

12

It's now obvious that the custom hook (sys.displayhook) that IPython uses to display output is pretty printing things (using it's own pretty printer).

By directly calling displayhook you can see how it ruins the insertion order:

In [1]: from sys import displayhook
   ...: displayhook({'1': 0, '0': 1})
Out[1]: {'0': 1, '1': 0}

In addition, if you grabbed the dictionary str instead (sending a string to be displayed instead of a dict object) you'd get the correct and expected order:

In [2]: d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
   ...: d
Out[2]: {'guido': 4127, 'jack': 4098, 'sape': 4139}

In [3]: str(dict(t))
Out[3]: "{'sape': 4139, 'guido': 4127, 'jack': 4098}"

similarly by printing it.

I'm not sure why IPython does this with 3.6, it was quite confusing (edit: see relevant issue on GitHub). In your standard Python REPL, this behavior won't manifest since sys.displayhook isn't implemented to do any pretty printing.


The dict d you've created does maintain insertion order, that's why the OrderedDict is maintaining that same order.

The fact that it does is, of course, an implementation detail. Until that is changed (and it does appear that it will) you should stick to using OrderedDict to reliably maintain order across implementations.


By the way, if you want this disabled, you could start IPython with the --no-pprint option which disables its pretty printer:

➜ ipython --no-banner --no-pprint 

In [1]: dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
Out[1]: {'sape': 4139, 'guido': 4127, 'jack': 4098}
Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
  • 1
    Here's the relevant IPython bug report: https://github.com/ipython/ipython/issues/10110. Basically they argue that the fact that Python 3.6 maintains dictionary order is an implementation detail and should not be relied upon, which makes at least some amount of sense. – Sven Marnach Jul 30 '17 at 08:44
7

In 3.6, as an implementation detail, all dicts are ordered. You're being fooled by IPython: Before 3.6, the order of keys was arbitrary, so for user-friendliness, IPython's interactive output for dict and set (where normal Python would just print the repr) sorts the keys. That's why your dict appears to be in alphabetical order. It's possible IPython might eventually drop that behavior when running on 3.6+, since as you've noticed, it is quite confusing.

If you explicitly print, rather than relying on ipython to output the results of the previous expression for you, you'll bypass ipython's REPL magic and see the "natural" order. Same goes for just about any other means of interacting with the dict, since iteration will proceed in the expected order.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Does the IPython interactive output sort `set` s as you state? `pprint` doesn't in Python 3 https://stackoverflow.com/questions/45016455/pprint-sorting-dicts-but-not-sets/ – Chris_Rands Jul 27 '17 at 11:24
  • @Chris_Rands: It does. `repr({'a', 'bbb', 'cde', 'ffe'})` on my current run (Linux x64 Py3.5.2, IPython 5.1.0) is `"{'cde', 'a', 'bbb', 'ffe'}"`, but the REPL output is `{'a', 'bbb', 'cde', 'ffe'}`. – ShadowRanger Jul 27 '17 at 11:42
  • Ok thanks, Jim's answer suggested that the underlying cause of this behavior for `dict`s in IPython was `pprint`, so I just thought `set`s might work the same way, but apparently not – Chris_Rands Jul 27 '17 at 11:47
  • 1
    @Chris_Rands Jim's answer isn't talking about the standard library's `pprint.pprint`, it's talking about ipython's `pprint` option. – PM 2Ring Jul 27 '17 at 11:49
  • @PM2Ring Thanks, I didn't know that the behavior of Ipython's `pprint` and `pprint.pprint` was different – Chris_Rands Jul 27 '17 at 11:54
  • 1
    @Chris_Rands I know almost nothing about IPython, but I assume that they use [their own pretty-printer](https://ipython.org/ipython-doc/3/api/generated/IPython.lib.pretty.html) for pretty-printing in the REPL rather than the virtually unconfigurable `pprint.pprint`. – PM 2Ring Jul 27 '17 at 12:02
4

As you probably know, dictionaries in Python are not ordered according to the language specification. They do have an inherent order but that order is arbitrary.

So when you pass a standard dictionary to the constructor of an OrderedDict, the new OrderedDict will be filled from the values of the original dictionary by iterating its values. That way, the inherent order of the dictionary will be used, and that will be what you will be seeing in the final OrderedDict.

Now, with Python 3.6, there was a change in the implementation of the default dictionary. As discussed and explained on this question, standard dictionaries now preserve the insertion order. That’s why when you created the OrderedDict from the Python 3.6 dict, the original order is preserved as well.

Does this mean that OrderedDict becomes obsolete in Python 3.6+? No, as the order preserving of standard dictionaries is an implementation detail. Instead of the arbitrary order of previous implementations, the new dictionary just happens to have the “correct” order. But this is in no way guaranteed by the language specification, and may or may not be the case for other implementations. As such you cannot and should not rely on it.

Btw. note that Python 3.6 (the language, not just the implementation) does guarantee that the order of keyword arguments to OrderedDict is preserved. E.g. this preserves the order:

>>> OrderedDict(sape=4139, guido=4127, jack=4098)
OrderedDict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
poke
  • 369,085
  • 72
  • 557
  • 602
  • "As such you cannot and should not rely on it." -- hence the well-known Python slogan, "Use the *freaking documentation*, Luke" ;-) – Steve Jessop Jul 27 '17 at 16:55