Looping defaultdict In Templates: A long-standing "Bug" in Django
I am new to Django and recently spent a good chunk of time trying to debug what I thought was quite trivial. Looping a defaultdict in templates.
This is a pretty common pattern when using Django, using a dict to pass and render data via templates, but there is something to be learned here. Let's jump into it!
The "Bug"
When you have a defaultdict whose default value is a list, you might run into this issue. Here is a simple example:
from collections import defaultdict
# Create a defaultdict with default type list
dd = defaultdict(list)
# Add some values to the defaultdict
dd['fruits'].append('apple')
dd['fruits'].append('banana')
dd['vegetables'].append('carrot')
dd['vegetables'].append('broccoli')
If you print(dd) the output will look something like
defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot', 'broccoli']})
All good till now. Now let's try to use this in a template like
{% for category, items in data.items %}
<h1> {{ category}} </h1>
<p> {{ items|join:", " }} </p>
{% endfor %}
#expected output
<h1> Fruits </h1>
<p> apple, banana </p>
<h1> Vegetables </h1>
<p> carrot, broccoli </p>
But in reality, it will print nothing! Let's see why this happens.
Explanation
Here is the discussion from the wayback machine which basically says
Indeed, it boils down to the fact that the template language uses the same syntax for dictionary and attribute lookups.
When Django resolves the template expression, it will first try to access data['items'] and since it's a defaultdict and there is no key called "items" it will set the value to an empty list and then try to iterate it and end up doing nothing!
The intended action would be to call the method items with no arguments of the instance data (in short: data.items()), but since data['items'] was a valid expression, Django stopped there and got the empty list just created.
Also since list
is iterable, it doesn't raise any error. If you initialized your dict with something like defaultdict(int)
, it will throw an error
"TypeError: 'int' object is not iterable".
Okay, now we know why this is happening, let's see how we can fix it.
Fix 1: Disabling the defaulting feature of defaultdict
You can disable the defaulting nature by setting
new_data.default_factory = None
Here, when you set the default value toNone
, when Django tries to do data['items']
it will get None
and it will move to try data.items()
and hence act as intended.
Fix 2: Re-cast it to a Dict
You can simply cast the defaultdict to dict before passing it to the templates.
dict(data)
I like the 2nd fix more, but I would ideally suggest not using defaultdicts at all in Django templates since it might have unexpected behavior which is really easy to miss.
Subscribe to my newsletter
Read articles from Dhaval Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by