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.
<!-- 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.
<!-- 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 %}.
<!-- 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.
<!-- 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.
DTL, Variables, Tags, Filters, Comments, Auto-escaping