Learn
09.2 | JINJA TEMPLATES AND STATIC FILES
01 | WHY A LONG HTML STRING BECOMES AWKWARD
A route returning one short heading is manageable. A full page has a head, navigation, repeated cards and a footer. Building all of that with Python string concatenation mixes presentation with request logic and makes changes harder to read.
A template is a text document containing page structure and placeholders. Flask uses Jinja to render it: combine the template with supplied values and produce the final response.
The browser receives the rendered HTML, not the original Jinja instructions. A for loop in a template runs on the server during rendering, while ordinary JavaScript in the final page runs in the browser.
02 | EXPLORE THE DOWNLOADABLE LEARNING LAB
The learning lab provides the complete route, template and form files used in lessons 09.1 to 09.3. Download and extract it, install requirements.txt in your environment and follow its README to set a local secret before starting the app.
app.py contains the factory and routes. templates contains base.html, home.html, tasks.html and new.html. static contains lab.css. instance holds the generated local SQLite file and is not included in the download.
This first lab has no account access rules, so use fictional records locally. The separate portal later adds sign-in and ownership. Do not treat a practice task form as a publicly secured student-data system.
Download the Flask learning lab| Location | Purpose | Sent directly to browser? |
|---|---|---|
| app.py | Request and data logic | No |
| templates/*.html | Server-side page templates | Rendered output only |
| static/lab.css | Stylesheet asset | Yes |
| instance/tasks.sqlite3 | Stored task records | No |
03 | UNDERSTAND THE THREE JINJA DELIMITERS
{{ ... }} inserts an expression’s value into the output. {% ... %} controls template statements such as loops and conditions. {# ... #} is a template comment omitted from the rendered response.
A name such as tasks must be supplied by the route or template context. It is not automatically every Python variable in your module. render_template("tasks.html", tasks=rows) explicitly provides that data.
Use templates for display decisions, while database queries, access rules and complex validation belong in Python. A page should not need to know how to assemble a SQL query just to show a list.
TEMPLATE | TEMPLATES/TASKS.HTML
{% extends 'base.html' %}{% block title %}Tasks | Flask Learning Lab{% endblock %}{% block content %}<h2>Planned tasks</h2><ul>{% for task in tasks %}<li>{{ task['title'] }}: {{ task['minutes'] }} minutes</li>{% else %}<li>No tasks yet.</li>{% endfor %}</ul>{% endblock %}
04 | A RUNNABLE INLINE DEMONSTRATION
This complete small file uses render_template_string to make one rendering example runnable before creating multiple template files. Save it as template_demo.py and run it through Flask. The lab uses separate files for maintainable page structure.
The route supplies a list of dictionaries. The template loop outputs each task, while its else block handles the empty collection. That else belongs to the loop’s empty-case behaviour, not to a Python condition.
Set records to [] and request the page again to inspect the empty view. The page should explain that no records exist rather than display an unexplained blank area.
from flask import Flask, render_template_string
app = Flask(__name__)
@app.get("/tasks")
def tasks():
records = [{"title": "Trace loops", "minutes": 25}]
template = """<h1>Study tasks</h1><ul>
{% for task in tasks %}
<li>{{ task['title'] }}: {{ task['minutes'] }} minutes</li>
{% else %}
<li>No tasks yet.</li>
{% endfor %}</ul>"""
return render_template_string(template, tasks=records)05 | SHARE ONE BASE LAYOUT
base.html defines the document head, navigation and main region. Child templates extend it and fill named blocks. If you improve the shared navigation, every inheriting page can receive the change.
A block is a replaceable template region. The base defines {% block content %}; the child supplies its own content block. The page title can have a separate block so browser tabs remain informative.
This is server-side inheritance, not CSS inheritance. CSS inheritance controls selected style properties; template inheritance controls generated document structure.
TEMPLATE | BASE LAYOUT EXCERPT
<title>{% block title %}Flask Learning Lab{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>TEMPLATE | CHILD EXCERPT
{% extends "base.html" %}
{% block title %}Tasks | Flask Learning Lab{% endblock %}
{% block content %}<h2>Your study tasks</h2>{% endblock %}06 | BUILD URLS FROM ENDPOINTS
url_for uses the endpoint name, normally the view function name, to build a URL. url_for("tasks") describes the destination without duplicating the current route spelling in every template.
For a parameterised route, supply the value: url_for("hello", name="Maya"). Flask constructs the path according to that registered rule. Missing required values or an unknown endpoint are real errors to fix, not reasons to suppress the exception.
This also helps when an application is served under a configured URL prefix. Do not use a filesystem path such as templates/tasks.html as a browser navigation URL. The handler renders that file; the client requests a route.
07 | STATIC ASSETS HAVE THEIR OWN URL
The stylesheet is a static file: Flask can send its bytes without rendering Jinja. Generate its link through url_for("static", filename="lab.css"). The filename is relative to the configured static folder.
A missing stylesheet might be a wrong filename, folder or URL, not a broken template loop. Inspect the browser’s network requests and the server response status to locate the problem.
A web app’s static folder is different from a template source folder. Do not place secrets or private databases inside static, because those files are intended to be served to clients.
TEMPLATE | QUOTED STYLESHEET ATTRIBUTE
<link rel="stylesheet" href="{{ url_for('static', filename='lab.css') }}">08 | PREVIEW DATA VERSUS HTML
The model shows a template value as text. An HTML-looking title remains text rather than becoming a new element. The actual learning lab uses Jinja autoescaping; this browser illustration uses textContent.
ILLUSTRATED OUTPUT
09 | KEEP AUTOESCAPING AND CONTEXT IN MIND
Flask enables autoescaping for its normal HTML template use, including the .html files here. Characters that could open tags are escaped when untrusted values are inserted as text. This helps prevent a title becoming executable HTML.
Do not apply |safe to user text merely because you want formatting. That marks the value as trusted markup and bypasses an important protection. If rich text is required, it needs a deliberate sanitisation policy.
Escaping is context-dependent. Quote attribute values; do not assume HTML text escaping makes arbitrary JavaScript, CSS or URL schemes safe. The project displays descriptions as text and avoids user-supplied active URLs and scripts.
10 | TEST THE PAGE, THE EMPTY STATE AND THE ASSET
Check a normal list, an empty list, a title containing an apostrophe and HTML-looking text. Inspect the response source to see escaped characters and the final generated URL.
Use the test client to request /static/lab.css and verify its status and content type. A successful main page does not prove every asset is present.
Try it: change the base title block, add a child heading and verify shared navigation still works. Keep data access in the route and presentation in the template.
PUT YOUR SKILLS TO WORK
Use the Section 9 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 9 challenges Python course navigation