Django Template Language (DTL): A Beginner's Guide

Blog / Django · March 9, 2019 · Updated June 10, 2026 · 9 min read
Django Template Language (DTL): A Beginner's Guide

The Django Template Language (DTL) is Django's built-in mini-language for turning data from a view into HTML. It gives you three constructs — variables {{ ... }}, tags {% ... %}, and comments {# ... #} — plus filters (the | pipe) that transform output, and it deliberately limits logic so your presentation layer stays clean.

If you have ever wondered how a view hands data to a page, DTL is the bridge: the view builds a context dictionary, and the template renders that context into markup.

Key takeaways

  • DTL has three constructs: variables {{ }}, tags {% %}, and comments {# #}; filters transform output with the pipe (|).
  • Variable lookups use dot notation that tries dictionary key, then attribute or method, then list index — in that order.
  • Tags drive logic and flow ({% if %}, {% for %}, {% url %}, {% block %}); filters format values (|date, |default, |length).
  • Template inheritance with {% extends %} and {% block %} keeps layouts DRY, while {% include %} reuses partials.
  • Autoescaping is ON by default to stop XSS; only reach for |safe on HTML you fully trust.
  • DTL is intentionally underpowered — heavy logic belongs in the view, not the template.

This guide focuses on DTL syntax for beginners. If you instead need to configure the engine itself — backends, DIRS, APP_DIRS, and loaders — see our companion guide on the basics of the Django template engine.

What is the Django Template Language?

The Django Template Language is the default templating syntax shipped with Django (5.2 LTS in 2026). A template is just a text file — usually HTML — with placeholders that Django fills in at render time using a context: a dictionary of variables supplied by your view.

DTL recognises exactly three special delimiters, and everything else is emitted verbatim:

Delimiter Purpose Example
{{ ... }} Output the value of a variable {{ user.username }}
{% ... %} Run a tag (logic, loops, includes) {% if user.is_staff %}
{# ... #} Template comment, never rendered {# TODO: paginate this list #}

That small surface area is intentional: DTL keeps business logic out of templates so the markup stays editable and safe.

How do variables and dot lookups work?

You output a context variable by wrapping its name in double curly braces. Django resolves a dotted name like post.author.name using a fixed lookup order, so the same {{ }} syntax works for dictionaries, objects, and lists:

  1. Dictionary lookup — post["author"]
  2. Attribute or method lookup — post.author (methods are called with no arguments)
  3. Numeric list-index lookup — tags.0

If a variable is missing, DTL renders an empty string instead of raising an error. Here is a view that prepares a context:

# blog/views.py
from django.shortcuts import render

def post_detail(request):
    context = {
        "post": {
            "title": "Hello, DTL",
            "published": None,            # missing / empty value
            "tags": ["django", "templates", "python"],
        },
        "author": request.user,           # object: attribute lookup
        "comment_count": 3,
    }
    return render(request, "blog/post_detail.html", context)

And here is the matching template. Notice how dot lookups, conditionals, loops, and filters all combine to render that context into HTML:

{# blog/post_detail.html #}
<article>
  <h1>{{ post.title }}</h1>

  {# dot lookups: dict key, then attribute, then list index #}
  <p>By {{ author.username|default:"Anonymous" }}</p>
  <p>First tag: {{ post.tags.0 }}</p>

  {# filters format values for display #}
  <p>Published: {{ post.published|date:"Y-m-d"|default:"Draft" }}</p>
  <p>{{ post.tags|length }} tag{{ post.tags|length|pluralize }}</p>

  {% if comment_count %}
    <p>{{ comment_count }} comment{{ comment_count|pluralize }}</p>
  {% else %}
    <p>No comments yet.</p>
  {% endif %}

  <ul>
    {% for tag in post.tags %}
      <li>{{ forloop.counter }}. {{ tag|title }}</li>
    {% empty %}
      <li>No tags.</li>
    {% endfor %}
  </ul>
</article>

What is the difference between tags and filters?

This is the question beginners ask most. Tags ({% ... %}) perform logic — loops, conditionals, URL reversal, inheritance. Filters ({{ value|filter }}) transform the output of a single variable. You can even chain filters left to right, as in {{ post.published|date:"Y-m-d"|default:"Draft" }}.

Common built-in tags:

Tag What it does Example
{% for %}{% endfor %} Loop over an iterable {% for p in posts %}
{% if %} / {% elif %} / {% else %} Conditional rendering {% if user.is_authenticated %}
{% url %} Reverse a named URL pattern {% url "blog:detail" slug=post.slug %}
{% extends %} Inherit from a parent template {% extends "base.html" %}
{% block %}{% endblock %} Define an overridable region {% block content %}
{% include %} Render another template inline {% include "partials/card.html" %}
{% load %} Load a tag or filter library {% load static %}
{% csrf_token %} Output the CSRF hidden field in forms <form>{% csrf_token %}</form>

Common built-in filters:

Filter What it does Example
date Format a date or datetime {{ post.created|date:"Y-m-d" }}
default Fallback when the value is falsy or missing {{ name|default:"Guest" }}
length Count items in a list or characters in a string {{ tags|length }}
truncatewords Trim text to N words {{ body|truncatewords:25 }}
title / upper / lower Change letter case {{ "django"|title }}
pluralize Add a plural suffix based on a number {{ n }} tag{{ n|pluralize }}
slugify Make a URL-safe slug {{ title|slugify }}
safe Mark a string as safe, skipping autoescape {{ raw_html|safe }}

How do the url and static tags work?

Two tags keep your templates portable by removing hard-coded paths. {% static %} builds the correct URL for a static asset (after you {% load static %}), and {% url %} reverses a named URL pattern from urls.py, so links keep working even if you change the path:

{# load the staticfiles tag library first #}
{% load static %}

<link rel="stylesheet" href="{% static 'css/site.css' %}">
<img src="{% static 'img/logo.svg' %}" alt="Logo">

{# reverse a named URL instead of hard-coding the path #}
<a href="{% url 'blog:post_detail' slug=post.slug %}">Read more</a>
<a href="{% url 'home' %}">Home</a>

How does template inheritance work?

Template inheritance is DTL's answer to repeated layout. You define a base template with named {% block %} regions, then child templates {% extends %} it and override only the blocks they need. Shared markup — head, navigation, footer — lives in one place.

Three tags do the work:

  • {% extends "base.html" %} — declare the parent (must be the first tag in a child template).
  • {% block name %}…{% endblock %} — define a region a child can override; the parent value is the default.
  • {% include "partial.html" %} — drop in a reusable fragment, optionally passing data with with.

For fragments that need their own prepared context, Django also offers inclusion tags, and you can package reusable rendering logic as a custom template tag or filter.

{# base.html — the parent layout #}
<!DOCTYPE html>
<html lang="en">
<head>
  <title>{% block title %}MicroPyramid Blog{% endblock %}</title>
</head>
<body>
  <header>{% include "partials/nav.html" %}</header>

  <main>
    {% block content %}{% endblock %}
  </main>

  <footer>&copy; 2026 MicroPyramid</footer>
</body>
</html>

A child template then extends the base and fills only the blocks it cares about:

{# blog/post_detail.html — a child template #}
{% extends "base.html" %}

{% block title %}{{ post.title }} — MicroPyramid Blog{% endblock %}

{% block content %}
  <article>
    <h1>{{ post.title }}</h1>
    {% include "partials/share_buttons.html" with url=post.url %}
  </article>
{% endblock %}

How does autoescaping protect against XSS?

By default, DTL autoescapes every variable, converting characters like <, >, &, ', and " into HTML entities. This means user-supplied data such as a comment cannot inject a <script> tag — Django neutralises it automatically, which is a strong first line of defence against cross-site scripting (XSS).

When you genuinely need to render trusted HTML — say, sanitised content from your own admin — mark it safe with the safe filter, e.g. {{ trusted_html|safe }}, or wrap a region in {% autoescape off %}…{% endautoescape %}. Only ever do this on data you control.

DTL also deliberately limits what you can do in a template. You cannot call a function with arguments, assign variables freely, or run arbitrary Python. That constraint is a feature: it pushes business logic into your views and models — where it is testable — and keeps templates focused on presentation.

DTL vs Jinja2: which should you use?

For most Django projects, stick with DTL — it is the default, integrates seamlessly with forms, the admin, and {% csrf_token %}, and its restraint keeps templates maintainable. Reach for Jinja2 only when you need its extra expressiveness (calling methods with arguments, richer macros) or are porting templates from another framework. Django supports both, and you can even run them side by side by configuring multiple backends — covered in our Django template engine guide.

Aspect Django Template Language Jinja2
Design goal Deliberately limited logic in templates More Python-like expressiveness
Method calls with arguments Not allowed in the template Allowed, e.g. {{ obj.method(arg) }}
Extending behaviour Custom template tags and filters Macros, tests, and global functions
Autoescaping On by default On by default in Django/Flask setups
Availability Default backend bundled with Django Supported as an alternate backend

Frequently Asked Questions

What is the Django Template Language used for?

The Django Template Language (DTL) is used to generate HTML — or any text output — from a context of data supplied by a view. It separates presentation from logic: the view prepares the data, and the template decides how it is displayed using variables, tags, and filters.

What is the difference between a tag and a filter in Django?

A tag ({% ... %}) performs logic such as loops, conditionals, URL reversal, or template inheritance. A filter ({{ value|filter }}) transforms the output of a single variable — formatting a date, supplying a default, counting items, or changing case. In short: tags control flow, filters shape values.

How do I add an if/else condition in a Django template?

Wrap the branches in {% if condition %}, optional {% elif other %} and {% else %}, and close with {% endif %}. Conditions can use comparison operators (==, !=, >, <) and the keywords and, or, and not. Django renders only the matching branch.

Can I call functions with arguments in Django templates?

No. DTL intentionally does not let you call methods or functions with arguments inside a template; methods are invoked only with no arguments during attribute lookup. For anything more, move the logic into your view, or write a custom template tag or filter.

How does template inheritance work in Django?

A base template defines {% block %} regions, and a child template starts with {% extends "base.html" %} and overrides those blocks. Markup outside the blocks is inherited as-is, so shared layout lives in a single file. Use {% include %} to reuse smaller fragments across templates.

Is Django template autoescaping enough to prevent XSS?

Autoescaping is on by default and escapes HTML characters in every variable, which prevents most XSS from rendered context data. It is not a complete security strategy on its own: avoid |safe on untrusted input, sanitise rich text before storing it, and combine templates with Django's CSRF protection and a Content Security Policy.

Build production-grade Django apps with MicroPyramid

Mastering DTL syntax is the first step; shipping a fast, secure, well-tested Django product is the rest. With 12+ years of Django delivery since 2014 and 50+ projects behind us, our team can help you design clean templates, pair them with class-based views, and build the features your roadmap needs. Explore our Django development services or get in touch to scope your project.

Share this article