Using a model property (list of dictionaries) as an input to django's format_html_join() yields KeyError

I am attempting to use Django's format_html_join() util to return an html formatted version history for one of my models. But I cannot get format_html_join() to accept my list of dictionaries.

Here is what the documentation suggests:

format_html_join(
    "\n",
    '<li data-id="{id}">{id} {title}</li>',
    ({"id": b.id, "title": b.title} for b in books),
)

That third argument is intended to be: args_generator should be an iterator that yields arguments to pass to format_html(), either sequences of positional arguments or mappings of keyword arguments.

I have tried different ways to get this to work and I'm not getting it, so I'm asking for help. I thought a list of dictionaries is iterable. I'm also thinking there has to be a way to use a list of dictionaries in a util that is expecting a list of dictionaries without having to re-create the list of dictionaries. Here is the model method I have to get the version history:

    @property  # I have tried this as a property and not as a property, neither works
    def get_version_history(self):
        versions = Version.objects.get_for_object(self)
        version_history = []
        for version in versions:
            history_fields = version.field_dict
            hdict = {"question": history_fields['question'],
                      "answer": history_fields['answer'],
                      "user": version.revision.user.username,
                      "timestamp": version.revision.date_created.strftime("%Y-%m-%d %H:%M"),
                    }
            version_history.append(hdict)
        return version_history

That method returns something like this:

[{'question': "I'm out of questions",
  'answer': 'bye',
  'user': 'test.supervisor',
  'timestamp': '2025-07-31 20:19'},
 {'question': "I'm out of questions",
  'answer': 'me too',
  'user': 'test.supervisor',
  'timestamp': '2025-07-31 20:18'},
 {'question': "I'm out of questions",
  'answer': '',
  'user': 'test.supervisor',
  'timestamp': '2025-07-31 20:18'}]

Now I am trying to return an html formatted version of that list of dictionaries:

    def version_html(self):

        html = format_html_join(
            "\n",
            """<tr>
                <td>{question}</td>
                <td>{answer}</td>
                <td>{user}</td>
                <td>{timestamp}</td>
            </tr>""",
            self.get_version_history
        )
        return html

The above code returns this error:

File ~/.virtualenvs/cep/lib/python3.12/site-packages/django/utils/html.py:112, in format_html(format_string, *args, **kwargs)
    110 args_safe = map(conditional_escape, args)
    111 kwargs_safe = {k: conditional_escape(v) for (k, v) in kwargs.items()}
--> 112 return mark_safe(format_string.format(*args_safe, **kwargs_safe))

KeyError: 'question'

I have tried various things for the third argument, all with various errors:

self.get_version_history
self.get_version_history()

**self.get_version_history
**self.get_version_history()

{"question": v.question, "answer": v.answer, "user": v.user, "timestamp": v.timestamp,} for v in self.get_version_history())
({"question": v['question'], "answer": v['answer'], "user": v['user'], "timestamp": v['timestamp']} for v in self.get_version_history())

{"question": v.question, "answer": v.answer, "user": v.user, "timestamp": v.timestamp,} for v in self.get_version_history)
({"question": v['question'], "answer": v['answer'], "user": v['user'], "timestamp": v['timestamp']} for v in self.get_version_history)

(d for d in self.get_version_history)
(d for d in self.get_version_history())

[d for d in self.get_version_history]
[d for d in self.get_version_history()]

Now I'm just thrashing.

This feature has been added recently. Indeed, this is implemented in pull request 18469 [GitHub], and thus was introduced in . Indeed, it is mentioned in the release notes [Django-doc]:

format_html_join() now supports taking an iterable of mappings, passing their contents as keyword arguments to format_html().

Until , one could only use iterables, like tuples or lists, making it harder to format it properly. We can see this in the source code [GitHub]:

return mark_safe(
    conditional_escape(sep).join(
        format_html(format_string, *args) for args in args_generator
    )
)

So you there are essentially three things you can do:

  1. upgrade to ;

  2. roll your own format_html_join, which is not very hard:

    from collections.abc import Mapping
    
    from django.utils.html import conditional_escape, format_html, mark_safe
    
    
    def format_html_join(sep, format_string, args_generator):
        return mark_safe(
            conditional_escape(sep).join(
                (
                    format_html(format_string, **args)
                    if isinstance(args, Mapping)
                    else format_html(format_string, *args)
                )
                for args in args_generator
            )
        )
    
  3. or use tuples instead:

    from operator import itemgetter
    
    def version_html(self):
         return format_html_join(
             '\n',
             '<tr>' + '<tr>{}</td>'*4 + '</tr>',
             map(itemgetter('question', 'answer', 'user', 'timestamp'), self.get_version_history)
         )
    
Вернуться на верх