Django Template Language
Every render() call so far has pointed at a .html file without explaining
what's actually allowed to go inside it. Those files are written in the Django Template
Language (DTL) — a small, deliberately limited templating syntax that lives inside ordinary
HTML. This lesson covers how to output data, branch and loop over it, reshape it with filters, and
leave notes for yourself that never reach the browser.
{{ }} and {% %} is not
Python and isn't executed anywhere on this page — these are static, correct examples of what a real
.html template inside a Django project would contain.What DTL Is (and Deliberately Isn't)
DTL intentionally cannot do most of what real Python can do inside a template — there's no arbitrary function calling, no assignment of new variables from expressions, no importing modules. That's a design choice, not a limitation Django failed to lift: keeping templates unable to run arbitrary logic forces presentation concerns (how something looks) to stay separate from business logic (what something means or how it's calculated), which belongs in views and models instead. A template that can barely do more than loop and branch is a template that stays readable to whoever opens it next, designer or developer.
Variable Output and Dotted Lookup
A value passed into context from a view is printed into the page with double curly
braces:
<p>{{ task.title }}</p>
The dot in task.title is DTL's one universal lookup operator, and it tries several
things in order until one of them works:
- Dictionary lookup —
task['title'], iftaskis a dict - Attribute lookup —
task.title, iftaskis an object (this is what matches a model instance's field) - List-index lookup —
task.0, iftaskis a list or tuple and0parses as an integer
Because it's the same dot for every case, the same syntax reaches straight through a relationship without any special notation:
<h2>{{ task.title }}</h2>
<p>Project: {{ task.project.name }}</p>
<p>Owner: {{ task.project.owner.username }}</p>
task.project.name follows the Task's project foreign key to
a Project, then reads its name field — three attribute lookups chained by
dots, no different syntactically than the first one.
Core Template Tags
Anything that isn't a value to print is written between {% %} instead of
{{ }} — these are tags, and they cover control flow.
| Tag | Purpose |
|---|---|
{% if %} … {% elif %} … {% else %} … {% endif %} | Conditional branching |
{% for item in list %} … {% endfor %} | Looping over a queryset or list |
{% for item in list %} … {% empty %} … {% endfor %} | Looping, with a fallback shown when the list is empty |
{% if task.completed %}
<span class="badge-done">Done</span>
{% elif task.due_date %}
<span class="badge-pending">Due {{ task.due_date }}</span>
{% else %}
<span class="badge-pending">No due date</span>
{% endif %}
{% empty %} is worth calling out on its own — it's the block shown in place of the
loop body specifically when the list being iterated has zero items, without needing a separate
{% if tasks %} wrapped around the whole loop:
<ul>
{% for task in tasks %}
<li>{{ task.title }}</li>
{% empty %}
<li>No tasks yet — enjoy the quiet.</li>
{% endfor %}
</ul>
Built-In Filters
A filter transforms a value on its way to being printed, applied with a pipe character after the variable. Filters can be chained, and some accept an argument after a colon.
| Filter | What it does |
|---|---|
{{ value|default:"Untitled" }} | Prints "Untitled" if value is falsy (empty string, None, etc.) |
{{ task.due_date|date:"M d, Y" }} | Formats a date, e.g. Aug 16, 2026 |
{{ description|truncatewords:20 }} | Cuts text down to a maximum word count, adding an ellipsis |
{{ title|upper }} | Converts text to uppercase |
<h3>{{ task.title|upper }}</h3>
<p>Due: {{ task.due_date|date:"M d, Y"|default:"No date set" }}</p>
<p>{{ task.description|truncatewords:20 }}</p>
The second line chains two filters: date formats the value first, and if
task.due_date was empty to begin with, date passes an empty result along
to default, which supplies the fallback text.
Comments
{# ... #} writes a comment that DTL strips out entirely — unlike an HTML comment
(<!-- ... -->), which still gets sent to the browser, a DTL comment never
reaches the rendered output at all.
{# TODO: add a priority icon here once design finalizes the colors #}
<p>{{ task.title }}</p>
Putting It Together: The TaskFlow Task List Template
Combining variable output, dotted lookup, the for/empty tag, and a couple
of filters produces a genuinely useful template with very little markup:
{# taskflow/templates/taskflow/task_list.html #}
<h1>Your Tasks</h1>
<ul>
{% for task in tasks %}
<li>
<strong>{{ task.title }}</strong>
({{ task.project.name|default:"No project" }})
<br>
Due: {{ task.due_date|date:"M d, Y"|default:"Not scheduled" }}
{% if task.completed %}
<span class="badge-done">Completed</span>
{% endif %}
</li>
{% empty %}
<li>No tasks yet. Add one to get started.</li>
{% endfor %}
</ul>
Everything above reads directly from the context handed over by task_list (or
TaskListView) in the previous two lessons — no additional view code is needed to
support any of this template's behavior.