Templates and template inheritance
Templates are how Django turns data into HTML. A view gathers the data; a template decides how it looks. The Django template language is deliberately small — it is not Python, and that limitation is a feature — and its inheritance system lets you write your site's shared structure once and fill in only what changes per page. This lesson covers the template language you will actually use and the inheritance that keeps a growing site maintainable.
Rendering a template with context
The view passes a context dictionary to render; the template reads from it with {{ }}:
return render(request, "patients/patient_list.html", {"patients": patients, "city": city})
<h1>Patients in {{ city }}</h1>
{{ city }} outputs the value of city from the context. The template sees only what the view puts in the
context — this separation is deliberate: the view does the logic and querying, the template does the
presentation. Verified: this renders "Patients in Pune" with the real city value.
The two syntaxes: {{ }} and {% %}
The template language has exactly two constructs, and knowing which is which is most of it:
{{ variable }}— output a value.{{ patient.name }},{{ city }},{{ patients|length }}.{% tag %}— do something: loop, branch, extend, include.{% for %},{% if %},{% block %}.
Attribute access uses a dot: {{ patient.name }} works for patient.name, patient["name"], or a method
patient.name() — the template tries each, so you write . regardless. Crucially, you cannot call a
method with arguments in a template ({{ obj.method(arg) }} is not allowed) — that is by design, to keep
logic out of templates. If you need computed data, compute it in the view (or a model method) and pass it in.
Loops and conditionals
The tags you use constantly, all verified in Nidaan's patient list:
<ul>
{% for p in patients %}
<li>{{ p.name }} — {{ p.phone }}</li>
{% empty %}
<li>No patients yet.</li>
{% endfor %}
</ul>
<p>Total: {{ patients|length }}</p>
{% for %} iterates; {% empty %} is a lovely touch — its block renders when the loop is empty, so you
handle "no results" without a separate {% if %}. Verified: with two patients it lists both and shows
"Total: 2"; with none, it shows "No patients yet." {% if %}/{% elif %}/{% else %} branch:
{% if patient.appointments.count %}
<p>{{ patient.appointments.count }} appointments</p>
{% else %}
<p>No appointments booked.</p>
{% endif %}
Inside a {% for %}, the forloop variable gives you forloop.counter (1-based index), forloop.first,
forloop.last — handy for numbering rows or styling the first/last.
Filters: transforming values for display
A filter transforms a value with |, for presentation formatting:
{{ patient.name|upper }} <!-- ASHA -->
{{ patients|length }} <!-- 2 -->
{{ appointment.scheduled_for|date:"d M Y" }} <!-- 27 Sep 2026 -->
{{ patient.notes|default:"—" }} <!-- fallback if empty -->
{{ description|truncatewords:20 }} <!-- first 20 words -->
Filters chain ({{ value|filter1|filter2 }}) and some take an argument after a colon (date:"d M Y"). They
are for display formatting — dates, truncation, defaults, capitalisation — not for logic. The common ones:
date, length, default, truncatewords, upper/lower/title, join, yesno.
Template inheritance: write the shell once
This is the feature that makes templates scale. Define a base template with the shared structure and
named {% block %}s; each page extends it and overrides only the blocks it needs:
<!-- patients/base.html -->
<!doctype html>
<html>
<head><title>{% block title %}Nidaan{% endblock %}</title></head>
<body>
{% block content %}{% endblock %}
</body>
</html>
<!-- patients/patient_list.html -->
{% extends "patients/base.html" %}
{% block title %}Patients{% endblock %}
{% block content %}
<h1>Patients in {{ city }}</h1>
...
{% endblock %}
{% extends %} must be the first line. The child fills the parent's blocks; anything not overridden
keeps the parent's default. Verified: the list page renders inside the base's HTML shell with its own title
and content. Now your site's header, footer, navigation, CSS links and layout live in one base template;
every page inherits them, and changing the header changes it everywhere. This is the antidote to copy-pasting
the same <head> into fifty templates — a real base template is the first thing a serious Django project
sets up.
{% include "patials/_card.html" %} complements inheritance by pulling a reusable fragment into a page —
use extends for the page's overall skeleton and include for repeated components.
Why the template language is deliberately limited
You may find the template language restrictive — no arbitrary Python, no calling methods with arguments, no complex expressions. This is intentional. It enforces the separation of concerns: business logic belongs in views and models where it can be tested and reused, not scattered through HTML where it cannot. When a template feels like it needs "just a bit of logic", that is the signal to move the computation into the view and pass the result in. A template that only displays pre-computed data stays readable and stays a designer's concern, not a programmer's — which is exactly what a template should be.
Check your work
How a template gets data. The view passes a context dict to render; the template reads it with
{{ }}. The template sees only the context — logic stays in the view.
The two syntaxes. {{ variable }} outputs a value; {% tag %} does something (loop, branch, extend).
Dot access works for attribute/key/method, but you cannot call methods with arguments.
Loops and conditionals. {% for %}/{% empty %} (verified: lists two, shows "No patients yet." when
empty), {% if %}/{% else %}, and forloop.counter/.first/.last.
Filters. | transforms a value for display (date, length, default, truncatewords), chainable,
some with a :argument — for formatting, not logic.
Template inheritance. A base template with {% block %}s; pages {% extends %} it (first line) and
override blocks — shared structure written once. Verified: the list renders inside the base shell.
Why the language is limited. To keep logic out of templates — when a template "needs logic", compute it in the view and pass the result.
Practice
- Render
patient_list.htmlwith a context; confirm{{ city }}and the{% for %}output match the data. - Empty the queryset and confirm the
{% empty %}block shows "No patients yet." - Use three filters (
date,length,default) and confirm each formats as expected. - Build a
base.htmlwithtitleandcontentblocks; make two pages extend it. Change the base's<head>once and confirm both pages update. - Try
{{ patient.get_display_name(True) }}(a method call with an argument) and read the error; move the computation into the view/model and pass the value in. - Extract a repeated fragment into a partial and pull it in with
{% include %}.
Official documentation
- Django — Templates — The template system overview.
- Django — Built-in template tags and filters — Every tag and filter.
- Django — Template inheritance —
extends,block,include.
Next: class-based views, and when they help.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship