Search code examples
pythondjangotemplatesmagic-methods

Django templates: why does __call__ magic method breaks the rendering of a non-model object?


Today I faced a strange issue on one of my development. I reproduced it with a very minimal example. Have a look at these 2 dummy classes (non Django model subclasses):

class DummyClassA(object):
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return 'Dummy1 object called ' + self.name


class DummyClassB(object):
    """Same as ClassA, with the __call__ method added"""
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return 'Dummy2 object called ' + self.name

    def __call__(self, *args, **kwargs):
        return "bar"

They are identical, but the second have a special __call__() method.

I want to display instances of these 2 objects in a view using the builtin Django template engine:

class MyView(TemplateView):

    template_name = 'myapp/home.html'

    def get_context_data(self, **kwargs):
        ctx = super(MyView, self).get_context_data(**kwargs)

        list1 = [
            DummyClassA(name="John"),
            DummyClassA(name="Jack"),
        ]

        list2 = [
            DummyClassB(name="Albert"),
            DummyClassB(name="Elmer"),
        ]

        ctx.update({
            'list1': list1,
            'list2': list2,
        })
        return ctx

and the corresponding template:

    <h1>Objects repr</h1>
    <ul>
        {% for element in list1 %}
            <li>{{ element }}</li>
        {% endfor %}
    </ul>
    <ul>
        {% for element in list2 %}
            <li>{{ element }}</li>
        {% endfor %}
    </ul>

    <h1>Access members</h1>
    <ul>
        {% for element in list1 %}
            <li>{{ element.name }}</li>
        {% endfor %}
    </ul>
    <ul>
        {% for element in list2 %}
            <li>{{ element.name }}</li>
        {% endfor %}
    </ul>

I obtain this result:

html result

When displaying instances of the second class ({{ element }}), the __call__ method is executed instead of __repr__(), and when I want to access a member of the class, it returns nothing.

I don't understand why defining the __call__() change the way Django template engine will handle those instances. I imagine this is not a bug but mostly a feature, but I am curious, why __call__() is run in such case. And why I can't get the value of element.name in the 2nd list ?


Solution

  • Because that's what the template language is designed to do. As the docs state:

    If the resulting value [of looking up a variable] is callable, it is called with no arguments. The result of the call becomes the template value.

    Without this, there would be no way of calling methods in templates, since the template syntax does not allow using parentheses.