85
import collections

data = [
  {'firstname': 'John', 'lastname': 'Smith'}, 
  {'firstname': 'Samantha', 'lastname': 'Smith'}, 
  {'firstname': 'shawn', 'lastname': 'Spencer'}, 
]

new_data = collections.defaultdict(list)

for d in data:
    new_data[d['lastname']].append(d['firstname'])

print new_data

Here's the output:

defaultdict(<type 'list'>, {'Smith': ['John', 'Samantha'], 'Spencer': ['shawn']})

and here's the template:

{% for lastname, firstname in data.items %}
  <h1> {{ lastname }} </h1>
  <p> {{ firstname|join:", " }} </p>
{% endfor %}

But the loop in my template doesn't work. Nothing shows up. It doesn't even give me an error. How can i fix this? It's supposed to show the lastname along with the firstname, something like this:

<h1> Smith </h1>
<p> John, Samantha </p>

<h1> Spencer </h1>
<p> shawn </p>
2
  • You haven't shown the code that puts the dictionary into the context for the template. Are you sure that's happening properly? Commented Jan 21, 2011 at 22:02
  • Yes, everything else renders correctly outside of the loop. Commented Jan 21, 2011 at 22:12

3 Answers 3

111

You can avoid the copy to a new dict by disabling the defaulting feature of defaultdict once you are done inserting new values:

new_data.default_factory = None

Explanation

The template variable resolution algorithm in Django will attempt to resolve new_data.items as new_data['items'] first, which resolves to an empty list when using defaultdict(list).

To disable the defaulting to an empty list and have Django fail on new_data['items'] then continue the resolution attempts until calling new_data.items(), the default_factory attribute of defaultdict can be set to None.

Sign up to request clarification or add additional context in comments.

3 Comments

+1 - This is much more efficient than the selected answer, especially with a large dictionary set.
Great answer, explanation helped a lot understanding the underlaying problem!
I might add that a {% for k, v in detauldictvar %} allows you to iterate on the defaultdict's element even without setting the default_factory to None.With Sebastien explanation now I can understand why :-)
56

try:

dict(new_data)

and in Python 2 it is better to use iteritems instead of items :)

2 Comments

I can't believe something simple like that worked. Thanks a lot!
It was actually filed as a bug against django: code.djangoproject.com/ticket/16335 but turned out the only good solution was indeed to transform it to a dict, as it's now shown in the docs and it's visible in the changeset
9

Since the "problem" still exist years later and is inherint to the way Django templates work, I prefer writing a new answer giving the full details of why this behaviour is kept as-is.

How-to fix the bug

First, the solution is to cast the defaultdict into a dict before passing it to the template context:

context = {
    'data': dict(new_data)
}

You should not use defaultdict objects in template context in Django.

But why?

The reason behind this "bug" is detailed in the following Django issue #16335:

Indeed, it boils down to the fact that the template language uses the same syntax for dictionary and attribute lookups.

... and from the docs:

Dictionary lookup, attribute lookup and list-index lookups are implemented with a dot notation. [...] If a variable resolves to a callable, the template system will call it with no arguments and use its result instead of the callable.

When Django resolve your template expression it will try first data['items']. BUT, this is a valid expression, which will automatically creates a new entry items in your defaultdict data, initialized with an empty list (in the original author case) and returns the list created (empty).

The intented 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 stop there and gets the empty list just created.

If you try the same code but with data = defaultdict(int), you would get a TypeError: 'int' object is not iterable, because Django won't be able to iterate over the "0" value returned by the creation of the new entry of the defaultdict.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.