Template recursion: why does Django not allow `./` and `../`?

Hi. When trying to recursively include a Django template using relative paths that contain ./ or ../, Django raises a TemplateSyntaxError, which can be traced to line 268 of django/template/loader_tags.py. However, when using an “absolute” path instead (i.e. a relative path that does not use ./ or ../), Django does not throw any such error. Why is this a thing? Is there a reason that Django raises an error when using relative paths containing ./ or ../ but not with other ones?

For the sake of a minimal reproducible example, here are some steps to recreate the issue:

  1. Start a new Django project with django-admin startproject project.
  2. Change into the project directory and run django-admin startapp app.
  3. Add or modify the files according to the code snippets given below.
  4. Run the development server with python manage.py runserver.
  5. Visit http://localhost:8000 and confirm that the page displays a simple table of contents.
  6. In app/templates/app/ul.html, replace the path app/ul.html with ./ul.html.
  7. Visit http://localhost:8000 again and observe that the TemplateSyntaxError is produced.

app/views.py:

from django.shortcuts import render


def index(request):
    # Hierarchal data, from a database perhaps
    context = {
        "sections": [
            {
                "name": "Chapter 1: Mastering the art of foo",
                "sections": [
                    {
                        "name": "Section 1.1: What can foo do for you?",
                        "sections": [],
                    },
                    {
                        "name": "Section 1.2: A layman's guide to using a foobar",
                        "sections": [],
                    },
                ],
            }
        ],
    }
    return render(request, "app/ul.html", context)

app/templates/app/ul.html (after creating the app/templates/app/ directory):

<ul>
    {% for section in sections %}
        <li>
            <p>{{ section.name }}</p>
            {% if section.sections|length != 0 %}
                {% include "app/ul.html" with sections=section.sections %}
            {% endif %}
        </li>
    {% endfor %}
</ul>

app/urls.py (a new file):

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

project/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("", include("app.urls")),
]

project/settings.py (to install the app):

# ...

# Install the app we just created

INSTALLED_APPS = [
    "app.apps.AppConfig", 

    # ...
]

# ...

EDIT: I’ve modified the post title and text to clarify what I meant by “absolute” paths in the context of Django templates. The distinction is between paths that use ./ or ../ and paths that do not.

Welcome @gnpivo !

The easiest way to think about this is to realize that Django does a search for all templates. You’re actually not specifying an absolute path, you’re specify a path that is relative to the set of directories being searched in the TEMPLATES setting. As a result, there is no semantic meaning to the idea of a “current directory” in templates.

This line:

is a reference to a directory named app in any of the directories in the TEMPLATES search, not any one specific directory named app.

Hi @KenWhitesell, thank you for clarifying that detail. I’ve edited my earlier post to be more careful with my terminology. A path like app/ul.html is not actually an absolute path, but rather a path relative to the set of directories being searched by the template loader. Since the TEMPLATES setting in the project’s settings.py specifies APP_DIRS = True, the template loader will look in my app’s templates/ directory for the template as described in the Django documentation.

However, I still feel that the key mystery has not been solved. My understanding of the Django docs is that if a path begins with ./ or ../, it will be understood as a relative path from the directory of the template, as described in the Django docs for the extends tag (the include tag behaves as described in the docs for the extends tag).

It is not clear why the paths app/ul.html and ./ul.html should result in different behavior if they do indeed resolve to the exact same path (app/ul.html). In one case, the template compiles fine without problems. In the other case, an error is raised and the Django debug toolbar describes the error like this:

TemplateSyntaxError at /

The relative path ‘“./ul.html”’ was translated to template name ‘app/ul.html’, the same template in which the tag appears.

Why would this be an error? Was this a deliberate choice by the Django devs to prevent recursive template inclusion, but only with paths that use ./ or ../? Is there something I’m missing here?

Good question, one for which I do not see an answer.

None of the code in the relevent patch (Fixed #26402 -- Added relative path support in include/extends templa… · django/django@aec4f97 · GitHub), the original ticket (#26402 (Allow relative paths in {% extends %} and {% include %}) – Django), nor the referenced discussion (https://groups.google.com/g/django-developers/c/rDAJ0Ig6FoU) identify a reason for this specific test, other than it doesn’t make sense for a template to extend itself and the code added for the include tag uses the same test.

My gut reaction to this is that this could be considered a bug. I’d also make the guess that the right fix to this could be made without breaking anything that is currently working.

There’s a side note to this that is worth ensuring as being clear - it won’t only look in your app’s templates/ directory. It will search every installed app’s templates/ directory, including all of the Django apps and all third-party apps you may have listed in INSTALLED_APPS in addition to any directories specified in the DIRS list in the TEMPLATES setting. The first matching template name will be used.

Thanks again for the clarification. I took the initiative of opening a ticket and making a pull request. I would love to be able to brag to everyone I know that I contributed to the Django project (even if it was just to squash a tiny bug), and hopefully that dream will become a reality.

1 Like