150

Is there a more elegant way to write this code?

What I am doing: I have keys and dates. There can be a number of dates assigned to a key and so I am creating a dictionary of lists of dates to represent this. The following code works fine, but I was hoping for a more elegant and Pythonic method.

dates_dict = dict() 
for key,  date in cur:
    if key in dates_dict:
        dates_dict[key].append(date)
    else:
        dates_dict[key] = [date] 

I was expecting the below to work, but I keep getting a NoneType has no attribute append error.

dates_dict = dict() 
for key,  date in cur:
    dates_dict[key] = dates_dict.get(key, []).append(date) 

This probably has something to do with the fact that

print([].append(1)) 
None 

but why?

thefourtheye
  • 233,700
  • 52
  • 457
  • 497
Michael Murphy
  • 1,921
  • 2
  • 18
  • 21

3 Answers3

236

list.append returns None, since it is an in-place operation and you are assigning it back to dates_dict[key]. So, the next time when you do dates_dict.get(key, []).append you are actually doing None.append. That is why it is failing. Instead, you can simply do

dates_dict.setdefault(key, []).append(date)

But, we have collections.defaultdict for this purpose only. You can do something like this

from collections import defaultdict
dates_dict = defaultdict(list)
for key, date in cur:
    dates_dict[key].append(date)

This will create a new list object, if the key is not found in the dictionary.

Note: Since the defaultdict will create a new list if the key is not found in the dictionary, this will have unintented side-effects. For example, if you simply want to retrieve a value for the key, which is not there, it will create a new list and return it.

thefourtheye
  • 233,700
  • 52
  • 457
  • 497
  • 2
    @chepner: Note that `__missing__()` is not called for any operations besides `__getitem__()`. This means that `get()` will, like normal dictionaries, return `None` as a default rather than using `default_factory` i.e., `key in dates_dict` and `dates_dict.get(key)` work as expected – jfs Oct 14 '14 at 18:49
  • 1
    Just as a followup. I ended up using setdefault as this git me exactly what I wanted without an extra import. Thanks for the help – Michael Murphy Oct 22 '14 at 19:45
  • 1
    Your explanation of why `[].append` returns `None` doesn't make sense to me. If you are assigning or printing it immediately, why does it matter it is an in-place operation? Why would `dates_dict.get(key, []).append` yield `None` unless `dates_dict[key] == None`? – cfwschmidt Apr 26 '16 at 22:16
  • 1
    Exactly! In one of the iterations, the result of append is stored against the key and the same is retrieved with get and appended. But this time it's not a list but None – thefourtheye Apr 27 '16 at 00:14
  • 7
    What's the advantage of using defaultdict over setdefault ? – mike_hornbeck Jul 09 '16 at 23:15
38

Is there a more elegant way to write this code?

Use collections.defaultdict:

from collections import defaultdict

dates_dict = defaultdict(list)
for key, date in cur:
    dates_dict[key].append(date)
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • When I try `for key, date_list in dates_dict:`, I get `error: too many values to unpack (expected 2)` – Mawg says reinstate Monica Nov 02 '18 at 08:11
  • 1
    @Mawg look at the code in the answer. I don't see your example there. – jfs Nov 02 '18 at 16:49
  • 1
    @Axl Look at the code in the answer and in the comment. Do you see a difference? (note: `cur` is not the same as `dates_dict` — they are different objects) – jfs May 28 '19 at 14:49
  • 1
    @MawgsaysreinstateMonica Because it's should be `for key, date_list in dates_dict.items():` – Takamura Oct 18 '22 at 13:01
  • 1
    @Takamura: wrong. `cur` is the input. `dates_dict` is for the result. It is empty in the beginning. There is no point to iterate over it. The code in the answer is correct as is. – jfs Oct 18 '22 at 14:28
  • I'm not talking about the code in the answer, I think what @Mawg tried was after running the code in the answer – Takamura Oct 18 '22 at 16:14
7

dates_dict[key] = dates_dict.get(key, []).append(date) sets dates_dict[key] to None as list.append returns None.

In [5]: l = [1,2,3]

In [6]: var = l.append(3)

In [7]: print var
None

You should use collections.defaultdict

import collections
dates_dict = collections.defaultdict(list)
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321