Templates

Django Fundamentals — Templates
Django Fundamentals
Course 1 · Chapter 4 · Templates

🎨 Templates

render() from the last chapter needs an actual template file to render. Django ships its own templating engine — the Django Template Language (DTL) — which looks a lot like Jinja2 (the templating engine FastAPI projects commonly reach for) but is deliberately more restricted: DTL can't call arbitrary Python, on purpose.

Where Templates Live

By default, Django looks for templates inside each installed app's own templates/ directory — and convention nests one more folder, named after the app, to avoid collisions between apps that both have a list.html:

catalog/ ├── templates/ │ └── catalog/ # the extra "catalog" folder prevents name clashes │ ├── base.html │ ├── book_list.html │ └── book_detail.html ├── views.py └── urls.py
# catalog/views.py from django.shortcuts import render def book_list(request): books = [{"title": "Dune"}, {"title": "1984"}] return render(request, "catalog/book_list.html", {"books": books})

🧵 Variables, Tags & Filters

DTL syntax will look immediately familiar if you've used Jinja2 — double curly braces print a value, {% %} runs a tag, and | applies a filter:

<h1>{{ book.title }}</h1> <p>Published: {{ book.year|default:"unknown" }}</p> <p>{{ book.title|upper }}</p> <p>{{ book.description|truncatewords:20 }}</p>

Jinja2 (FastAPI) vs Django Template Language

Jinja2
{{ book.get_display_name() }} {% for book in books %} {{ book.title }} {% endfor %}

Method calls with parentheses and arguments are allowed directly in the template.

Django Template Language
{{ book.get_display_name }} {% for book in books %} {{ book.title }} {% endfor %}

No parentheses — a no-argument method is called automatically without them; arbitrary Python calls with arguments aren't allowed at all.

Logic-Light by Design

DTL intentionally can't run arbitrary Python — the idea is that business logic belongs in views.py and models, not scattered across templates.

Dot Lookup Does Everything

book.title tries, in order: dictionary lookup, attribute lookup, then a no-argument method call — one syntax covers all three.

Control Flow Tags

<ul> {% if books %} {% for book in books %} <li>{{ book.title }} ({{ forloop.counter }} of {{ books|length }})</li> {% endfor %} {% else %} <li>No books yet.</li> {% endif %} </ul>

forloop.counter is a DTL-specific convenience — a 1-indexed loop counter available automatically inside any {% for %}, with no separate variable to pass in from the view.

🛡️ Auto-Escaping

Every variable is HTML-escaped by default — the same defense the XSS course covered as the primary fix for output-context injection, applied automatically here without you having to remember to call an escape function:

# If book.title == '<script>alert("xss")</script>' {{ book.title }} # renders as the literal text &lt;script&gt;alert("xss")&lt;/script&gt; — inert, not executed {{ book.title|safe }} # ⚠️ opts OUT of escaping — only use this for content you trust completely, # e.g. HTML you sanitized yourself with a library, per the XSS course's DOMPurify chapter

Template Inheritance: extends and block

Nearly every page shares a header, nav, and footer — inheritance defines that shared shell once, in a base template, and lets each page override just the parts that differ:

A Base Template and a Child Page

<!-- catalog/templates/catalog/base.html --> <!DOCTYPE html> <html> <head> <title>{% block title %}My Library{% endblock %}</title> </head> <body> <nav>Home | Books</nav> {% block content %}{% endblock %} <footer>&copy; 2026</footer> </body> </html> <!-- catalog/templates/catalog/book_list.html --> {% extends "catalog/base.html" %} {% block title %}All Books{% endblock %} {% block content %} <ul> {% for book in books %} <li>{{ book.title }}</li> {% endfor %} </ul> {% endblock %}

{% extends %}

Must be the very first tag in a child template — declares which base template this page fills in.

{% block %} / {% endblock %}

Marks a named region a child can override; a block left unfilled in the child just uses the base template's default content.

{% include %}

Pulls in a separate template fragment inline ({% include "catalog/_book_card.html" %}) — for reusable partials rather than a whole-page shell.

Deep Inheritance Chains

A child template can itself be extended by a grandchild — base.htmlcatalog/base.htmlbook_list.html is a common real pattern.

💻 Coding Challenges

Challenge 1: Render a List With Filters

Write a template that loops over a books context variable, printing each book's title in uppercase and its description truncated to 15 words, with a fallback message if the list is empty.

Goal: Practice {% for %}/{% if %} tags together with the upper and truncatewords filters.

→ Solution

Challenge 2: Build a Base Template + Two Child Pages

Write a base.html with a title block and a content block, then two child templates (book_list.html and book_detail.html) that each {% extends %} it and fill in both blocks differently.

Goal: Practice the extends/block inheritance pattern across more than one page.

→ Solution

Challenge 3: Demonstrate Auto-Escaping

Pass a string containing <script> tags into a template context, render it two ways — once as plain {{ value }} and once as {{ value|safe }} — and explain what each one actually sends to the browser and why one is dangerous outside of trusted content.

Goal: Practice recognizing auto-escaping in action and understanding exactly what |safe turns off.

→ Solution

⚠️ Gotcha: A Typo Renders as Nothing, Not an Error

Reference {{ boook.title }} (misspelled) or a context variable that was never passed in, and DTL doesn't raise an exception — it silently renders an empty string and moves on. A page that's missing text with no error in the console or logs is very often exactly this: a typo'd variable name in a template. When something looks blank that shouldn't be, check the exact variable name against what the view actually passed into render()'s context dict before assuming the bug is elsewhere.

🎯 What's Next

Templates can now display data — the next chapter covers where that data actually comes from: Models & the ORM, defining database tables as Python classes, migrations, and querying with Django's QuerySet API.