Django Template Language

Difficulty: Intermediate

The Django Template Language (DTL) is a lightweight markup language designed for rendering HTML with dynamic data. It provides variables, tags, filters, and comments as its core building blocks. DTL is intentionally limited - it is not a full programming language, which enforces the separation between presentation logic and business logic.

Variables are surrounded by double curly braces: {{ variable }}. When the template engine encounters a variable, it evaluates it and replaces it with the result. You can access object attributes and dictionary keys using dot notation: {{ user.username }}, {{ article.title }}, {{ request.GET.page }}. If a variable does not exist, it is silently replaced with an empty string by default.

Template tags are surrounded by {% %} and provide logic such as loops, conditionals, and template loading. The most common tags are {% if %}, {% for %}, {% block %}, {% extends %}, {% include %}, and {% url %}. Tags can produce output, control flow, or load external content. Some tags require closing tags ({% endif %}, {% endfor %}), while others are self-contained.

Filters modify variables for display and use the pipe character: {{ variable|filter }}. Filters can be chained: {{ text|truncatewords:30|title }}. Common filters include 'default' (fallback value), 'date' (date formatting), 'truncatewords' (word limit), 'title' (title case), 'lower/upper' (case conversion), 'length' (count), 'linebreaks' (newlines to <br>), 'safe' (mark as safe HTML), and 'pluralize' (add plural suffix).

Django auto-escapes all variable output by default to prevent Cross-Site Scripting (XSS) attacks. This means characters like <, >, &, and quotes are converted to their HTML entity equivalents. If you need to output raw HTML (from a trusted source), use the |safe filter or the {% autoescape off %} tag. Never mark user-provided content as safe.

Comments use {# #} for single-line comments and {% comment %} for multi-line comments. Template comments are not included in the rendered output, unlike HTML comments which are visible in the page source.

The {% for %} loop provides a special forloop variable with useful attributes: forloop.counter (1-indexed count), forloop.counter0 (0-indexed), forloop.first (boolean), forloop.last (boolean), and forloop.parentloop (for nested loops). The {% empty %} tag inside a for loop handles the case when the list is empty.

Code examples

Variables and Dot Notation

<!-- Simple variables -->
<h1>{{ article.title }}</h1>
<p>By {{ article.author.username }}</p>
<p>Published: {{ article.published_at }}</p>

<!-- Dictionary access -->
<p>Page: {{ request.GET.page }}</p>

<!-- Method calls (no parentheses, no arguments) -->
<p>{{ article.get_status_display }}</p>
<p>Word count: {{ article.content.split|length }}</p>

<!-- Default value for missing variables -->
<p>{{ user.bio|default:"No bio provided" }}</p>

Dot notation traverses attributes, dictionary keys, list indexes, and method calls. Methods are called without parentheses and must take no arguments. The |default filter provides a fallback for empty or missing values.

Conditional Tags

<!-- Basic if/elif/else -->
{% if user.is_authenticated %}
  <p>Welcome, {{ user.username }}!</p>
{% elif user_count > 0 %}
  <p>Please log in to continue.</p>
{% else %}
  <p>No users found.</p>
{% endif %}

<!-- Comparison operators -->
{% if article.views > 1000 %}
  <span>Popular</span>
{% endif %}

<!-- Boolean operators -->
{% if user.is_staff and user.is_active %}
  <a href="/admin/">Admin Panel</a>
{% endif %}

{% if not article.is_published %}
  <span>Draft</span>
{% endif %}

<!-- Membership test -->
{% if "python" in article.tags.all %}
  <span>Python Article</span>
{% endif %}

Django templates support if, elif, else with comparison operators (==, !=, <, >, <=, >=), boolean operators (and, or, not), and membership tests (in, not in). Tags must be closed with {% endif %}.

For Loops and Empty

<!-- Basic loop -->
<ul>
{% for article in articles %}
  <li>
    <span>{{ forloop.counter }}.</span>
    <a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
    {% if forloop.first %}<span>(Latest)</span>{% endif %}
  </li>
{% empty %}
  <li>No articles found.</li>
{% endfor %}
</ul>

<!-- Nested loops -->
{% for category in categories %}
  <h3>{{ category.name }}</h3>
  <ul>
  {% for product in category.products.all %}
    <li>{{ product.name }} - ${{ product.price }}</li>
  {% endfor %}
  </ul>
{% endfor %}

<!-- Forloop variables -->
<!-- forloop.counter    : 1-indexed iteration count -->
<!-- forloop.counter0   : 0-indexed iteration count -->
<!-- forloop.first      : True if first iteration -->
<!-- forloop.last       : True if last iteration -->
<!-- forloop.parentloop : parent loop in nested loops -->

{% for %} iterates over sequences. {% empty %} handles empty lists. The forloop variable provides iteration metadata. Nested loops can access the outer loop via forloop.parentloop.

Common Filters

<!-- Text filters -->
{{ title|title }}                    <!-- "hello world" -> "Hello World" -->
{{ name|lower }}                     <!-- "JOHN" -> "john" -->
{{ bio|truncatewords:20 }}           <!-- Truncate to 20 words with ... -->
{{ content|linebreaks }}             <!-- Newlines to <p> and <br> -->
{{ text|striptags }}                 <!-- Remove HTML tags -->
{{ slug|slugify }}                   <!-- "My Title" -> "my-title" -->

<!-- Date filters -->
{{ pub_date|date:"M d, Y" }}         <!-- "Jan 15, 2024" -->
{{ pub_date|date:"F j, Y, g:i a" }} <!-- "January 15, 2024, 3:30 pm" -->
{{ pub_date|timesince }}             <!-- "3 days, 2 hours ago" -->

<!-- Number filters -->
{{ price|floatformat:2 }}            <!-- 29.9 -> "29.90" -->
{{ count|add:5 }}                    <!-- Add 5 to count -->

<!-- List filters -->
{{ tags|join:", " }}                 <!-- Join list with commas -->
{{ items|length }}                   <!-- Count items -->
{{ items|first }}                    <!-- First item -->
{{ items|last }}                     <!-- Last item -->

<!-- Safety -->
{{ html_content|safe }}              <!-- Mark as safe (no escaping) -->
{{ user_input|escape }}              <!-- Force escaping -->

Filters transform values for display. They are applied with the pipe character and can take arguments after a colon. Filters can be chained: {{ text|truncatewords:20|title }}. The |safe filter should only be used with trusted content.

Key points

Concepts covered

DTL, Variables, Tags, Filters, Comments, Auto-escaping