PYTHON / DJANGO
Templates and template inheritance
Build a base.html skeleton with {% block %} holes and write child templates that extend it, overriding or appending to blocks with {{ block.super }}.
What you will learn
- Write a base.html skeleton whose {% block %} tags mark the parts pages may replace
- Override only the blocks you need, with {% extends %} as the child's first tag
- Add to a parent block instead of replacing it using {{ block.super }}
- Explain why markup outside a block in a child template never reaches the output
Understanding Templates and template inheritance
A Django template is plain text plus a small language: {{ ... }} looks a name up in the context, {% ... %} runs a tag. Rendering happens in two phases: the template source is compiled once into a tree of nodes, then that tree is walked against a context dict to build the output string. Template names in {% extends %} and {% include %} are resolved by the configured loaders (the dirs in TEMPLATES[0]['DIRS'], then every installed app's templates/ folder when APP_DIRS is on), never by the directory the current file happens to live in.
The key mental model for inheritance is that a child template does not wrap the parent. When you render a template whose first tag is {% extends "base.html" %}, Django collects the child's {% block %} nodes into a block context and then renders base.html's node list. The parent decides the order and the surrounding HTML; the child only supplies replacement bodies for named holes. That single fact explains two behaviours beginners find surprising: text a child puts outside any block is never visited, and a block the child does not override simply renders the parent's default body.
Because a block override replaces the parent body completely, {{ block.super }} exists to render the parent's version at a chosen point inside your override, which is how you append a script tag or extend a nav list instead of retyping it. Chains work the same way: base.html then dashboard_base.html then page.html, with each level overriding or calling block.super on the level above. Keep each app's templates under app/templates/app/ because loaders return the first matching name they find, so two apps that each ship templates/base.html will silently shadow each other.
import django
from django.conf import settings
BASE = """<html>
<head><title>{% block title %}Untitled{% endblock %} | Bookshelf</title></head>
<body>
{% block content %}<p>No content.</p>{% endblock %}
<footer>{% block footer %}(c) 2024 Bookshelf{% endblock %}</footer>
</body>
</html>"""
DETAIL = """{% extends "base.html" %}
this line sits outside every block, so it is thrown away
{% block title %}{{ book.title }}{% endblock %}
{% block content %}<h1>{{ book.title }}</h1>
<p>by {{ book.author }}</p>{% endblock %}"""
settings.configure(
TEMPLATES=[{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"loaders": [
("django.template.loaders.locmem.Loader",
{"base.html": BASE, "detail.html": DETAIL}),
],
},
}],
)
django.setup()
from django.template.loader import get_template
html = get_template("detail.html").render(
{"book": {"title": "Dune", "author": "Frank Herbert"}}
)
print(html)Rendering a child template actually renders the parent's node tree, with the child contributing only named block bodies.
Worked examples
Appending with block.super
Shows how a child can keep the parent's block body and add to it instead of replacing it.
import django
from django.conf import settings
BASE = """<title>{% block title %}Bookshelf{% endblock %}</title>
<ul>{% block links %}<li>Home</li>{% endblock %}</ul>"""
PAGE = """{% extends "base.html" %}
{% block title %}{{ block.super }} - Shelf{% endblock %}
{% block links %}{{ block.super }}<li>Shelf</li>{% endblock %}"""
settings.configure(TEMPLATES=[{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {"loaders": [("django.template.loaders.locmem.Loader",
{"base.html": BASE, "page.html": PAGE})]},
}])
django.setup()
from django.template.loader import get_template
print(get_template("page.html").render({}))Example explained
Line 1{{ block.super }} renders the parent's body for the same block name at exactly that spot, so "Bookshelf" and "<li>Home</li>" survive.
Line 2Without block.super the override would be the whole block body and the parent text would vanish; nothing is merged automatically.
Line 3block.super only works inside a block that is overriding another one; in base.html itself it resolves to an empty string.
Line 4The context passed to render() is empty, proving the text came from the templates, not from variables.
Inheritance for the shell, include for repeated fragments
Contrasts {% extends %}, which restructures one render, with {% include %}, which renders a separate partial per call.
import django
from django.conf import settings
SOURCES = {
"base.html": "<main>{% block body %}{% endblock %}</main>",
"_row.html": "<tr><td>{{ label }}</td><td>{{ value }}</td></tr>",
"table.html": (
"{% extends 'base.html' %}"
"{% block body %}<table>\n"
"{% include '_row.html' with label='Title' value=book.title %}\n"
"{% include '_row.html' with label='Pages' value=book.pages %}\n"
"</table>{% endblock %}"
),
}
settings.configure(TEMPLATES=[{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {"loaders": [("django.template.loaders.locmem.Loader", SOURCES)]},
}])
django.setup()
from django.template.loader import get_template
print(get_template("table.html").render({"book": {"title": "Dune", "pages": 412}}))Example explained
Line 1_row.html has no blocks at all; it is a partial that renders whatever label and value it is handed.
Line 2{% include ... with a=1 b=2 %} renders the partial with the current context plus those extra names, so one file produces both rows.
Line 3The includes live inside {% block body %}, so extends decides the page shell while include handles repetition inside it.
Line 4Blocks defined inside an included file cannot be overridden by table.html: only an {% extends %} chain shares a block context.
Important notes
The parent name in {% extends %} may be a variable, which lets a view choose the skeleton at render time, but {% block %} names must be literal text.
Loaders return the first template whose name matches, so keep app templates in templates/<appname>/ or a second app's base.html can shadow yours.
Common mistakes
Putting {% load static %} or any other tag above {% extends %}: Django raises TemplateSyntaxError saying extends must be the first tag, and plain HTML placed above it is dropped without any warning.
Writing markup in a child template between or after its blocks and expecting it in the page; the child's non-block nodes are never rendered, so the content silently disappears.
Reusing one block name twice in the same template (two {% block content %} tags) raises TemplateSyntaxError about the block appearing more than once, because block names must be unique per template.
Try it yourself
Change, predict, then run
Using the locmem setup from the main example, add a third template list.html that extends base.html, overrides only title and content, and appends " - all rights reserved" to the footer with {{ block.super }}; render both detail.html and list.html and confirm only one footer changed.
Open the Python workspaceCheck your understanding
child.html contains, in this order: {% extends "base.html" %}, then the text <p>Hello</p>, then {% block content %}Hi{% endblock %}. base.html defines blocks content and footer. What does rendering child.html produce?
- base.html's full output with content replaced by "Hi", footer showing base.html's default, and <p>Hello</p> absent
- <p>Hello</p> first, then base.html's output with content replaced by "Hi"
- A TemplateSyntaxError, because content outside a block is not allowed in an extending template
- Only "Hi", because the child template is what is being rendered
Show answer
Rendering an extending template walks base.html's node tree, using the child's blocks as overrides, so the child's stray <p>Hello</p> node is never visited and the untouched footer block falls back to the parent's default. Option 2 is tempting if you picture the child wrapping or prepending to the parent, but nothing in the child outside a block is rendered; and it is not an error, only an easy way to lose markup.