MAD Study Hub

Home / MAD1 / Week 3

Week 3 · Views: Jinja2 templates, Bootstrap and forms

Official syllabus line: Presentation layer - View
About 100 minQuiz 1 material (see exam radar)BSCS2003 (+ project BSCS2003P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD1 weeks 1 and 2 (HTTP, GET and POST, HTML forms, CSS and Bootstrap basics). Python functions, lists and dictionaries.

The one-minute story

Think of it like this

A template is a form letter. "Dear NAME, your marks in COURSE are MARKS." You do not write a new letter for every student. You keep one letter with blanks, and a clerk fills in the blanks from a list. The template is the letter, the data is the list, and rendering is the clerk filling the blanks.

A website does the same. The controller (a Flask route) collects the data. The view (a Jinja2 template) is an HTML page with blanks and a few instructions like "repeat this row for every student". Rendering produces plain HTML that the browser shows. When the visitor fills in a form, the browser posts the answers back to the controller.

By the end of this week you can:

  • write a string with variables using f-strings and string.Template, and say how Jinja2 differs;
  • predict the output of a Jinja2 template with variables, filters, loops, conditions and set;
  • build a base layout with extends, block, super(), include and a macro;
  • render a template from Flask with render_template and link CSS with url_for('static', ...);
  • read form data with request.form and request.args and validate it on the server;
  • explain why Jinja2 escapes HTML and what |safe does.

Words you need

Words you need
  • Template - a text or HTML file with placeholders and small instructions.
  • Rendering - filling a template with data to produce the final text.
  • Jinja2 - the template engine Flask uses.
  • Expression - {{ ... }}: print a value.
  • Statement - {% ... %}: control flow such as for and if, prints nothing itself.
  • Filter - a small function applied with a pipe, like name|upper.
  • Inheritance - a child template reuses a base layout and fills its block slots.
  • Macro - a template function you define once and call many times.
  • Autoescape - Jinja2 turns < into &lt; in values so user text cannot inject HTML.
  • Static file - CSS, JavaScript or an image served as it is from the static folder.
  • request.form / request.args - data from the POST body / from the URL query string.
  • Client-side / server-side validation - checking input in the browser / in your Flask code.
  • PyHTML - a Python library that builds HTML from nested function calls (it also appears in past papers).

Idea 1: Strings with blanks, the stepping stone

Before templates, Python itself can fill blanks. Compare three ways:

name, price = "Ria", 49.5

print(f"{name:>6}|{price:.2f}")             # f-string
print("{} owes {:.1f}".format(name, price)) # str.format
print("%s owes %d" % (name, 3))             # old % style

from string import Template
t = Template("The $color car is in $place.")
print(t.substitute(color="red", place="Goa"))
print(t.safe_substitute(color="red"))
try:
    t.substitute(color="red")
except KeyError as e:
    print("KeyError", e)

Output:

   Ria|49.50
Ria owes 49.5
Ria owes 3
The red car is in Goa.
The red car is in $place.
KeyError 'place'
  • f-string: f"..." with {expression} and a format spec after a colon: .2f two decimals, >6 right-align in width 6, , thousands separator.
  • string.Template: placeholders are written $name. substitute raises KeyError if a value is missing; safe_substitute leaves the placeholder as it is.
  • The Template from string and the Template from jinja2 are different tools with different syntax. string.Template only substitutes simple $names: no loops, no conditions, no filters.
Solved example: 1. Two templating engines in a row

Question. What does this print?

from string import Template as T1
from jinja2 import Template as T2

temp = "A student needs to complete $c1 level to get to the {{c2}} level."
out = T1(temp).substitute({'c1': 'foundation', 'c2': 'diploma'})
print(out)
print(T2(out).render({'c2': 'diploma'}))

Solution. T1 replaces only $c1. It does not understand {{c2}}, and the extra key c2 in the dictionary is simply ignored. So the first line is A student needs to complete foundation level to get to the {{c2}} level. That string is then fed to Jinja2, which fills {{c2}}, so the second line is A student needs to complete foundation level to get to the diploma level.

If a $ placeholder is left unfilled, substitute raises KeyError: 'place', while safe_substitute keeps $place. I ran both.

Watch out - real exam trap
  • substitute fails (KeyError) on a missing key; safe_substitute does not.
  • Extra keys are ignored by both.
  • Two engines in a row: each only replaces its own syntax, and the other's syntax passes through untouched.
  • In Jinja2 a missing variable prints as an empty string (not an error).

PyHTML is a third way: build HTML with Python calls, for example html(head(title("Quiz 2")), body(h2("Heading"), p("Text"))) and .render() gives the nested HTML in the same order as the calls. Jinja2 is the one used in this course for real pages.

Idea 2: Jinja2 basics: variables, expressions, filters

Jinja2 works without Flask too, which is a good way to test a template:

from jinja2 import Template

t = Template("Hi {{ name }}, 2 x 3 = {{ 2 * 3 }}, you have {{ items|length }} items. {{ missing }}|")
print(t.render(name="Ria", items=[1, 2, 3]))

Output: Hi Ria, 2 x 3 = 6, you have 3 items. |

The missing variable printed nothing.

The idea

Three kinds of markers.

  • {{ expression }} prints a value.
  • {% statement %} is logic (for, if, set, block...) and prints nothing itself.
  • {# comment #} is removed from the output.

Passing data. render(name="Ria") and render({'name': 'Ria'}) are the same. Values may be strings, numbers, lists, dictionaries or objects. Inside the template, d.k and d['k'] both read a dictionary key, l[0] reads a list item, and methods can be called: if loan1.get_loan_details() returns (101, 45900, 5) then {{ l1.get_loan_details() }} prints (101, 45900, 5).

Expressions work like Python: {{ 7 // 2 }} is 3, {{ 7 / 2 }} is 3.5, {{ 'a' ~ 1 }} is a1 (~ joins text), {{ 'ab' * 2 }} is abab, {{ 'yes' if n > 1 else 'no' }}.

Filters go after a pipe and can be chained:

  • {{ name|upper }} gives RIA, {{ 'ria dev'|title }} gives Ria Dev
  • {{ name|default('guest') }} gives guest when name is not defined
  • {{ items|length }} is the size (the paper's right answer; len(items) and items|len are wrong)
  • {{ items|join(' / ') }}, {{ items|first }}, {{ items|last }}, {{ items|sort }}
  • {{ price|round(1) }}: 3.14159 becomes 3.1
  • {{ items|sum }}, {{ items|max }}, {{ s|trim }}, {{ s|replace('a','z') }}

Chain: {{ ['b','a']|sort|reverse|list }} gives ['b', 'a'].

Autoescaping and |safe

If a user types <script> as their name, printing it raw would run their script on every visitor's page (an attack called XSS). So Flask turns autoescape on for .html templates: < becomes &lt;, the browser shows the characters instead of treating them as a tag.

from flask import render_template_string
render_template_string("{{ s }} | {{ s|safe }}", s="<b>hi</b>")

Result: &lt;b&gt;hi&lt;/b&gt; | <b>hi</b>. The first shows the literal text <b>hi</b> in the browser; the second makes real bold text.

The idea

|safe means "I trust this HTML, do not escape it". Never use it on text a user typed. Note: a plain jinja2.Template(...) used outside Flask does not escape by default (autoescape is off unless you ask), which is why {{ s }} printed <b>hi</b> unchanged in my console test.

Solved example: 2. A missing variable and a method call

Question. Predict the output.

from jinja2 import Template

template = '''
Welcome to {{Degree}} DEGREE
First level to clear is {{level}}
'''
print(Template(template).render(Degree="IITM BS"))

Solution. Only Degree is passed. {{level}} is undefined and prints nothing:

(blank line)
Welcome to IITM BS DEGREE
First level to clear is

Names are case sensitive: {{ Level }} would also be blank even if level had a value.

Idea 3: Control flow: for, if, set, whitespace

{% for c in courses %}
  <li>{{ loop.index }}. {{ c }}{% if loop.last %} (last){% endif %}</li>
{% else %}
  <li>No courses</li>
{% endfor %}

With courses = ["MAD1", "MAD2", "DBMS"] the items are 1. MAD1, 2. MAD2, 3. DBMS (last). The else of a for runs only when the list is empty.

Loop variableMeaning
loop.index1, 2, 3 ... (starts at 1)
loop.index00, 1, 2 ...
loop.first, loop.lastTrue on the first / last pass
loop.lengthTotal passes
loop.revindexPasses left, counting down to 1

Loop over a dictionary with {% for k, v in d.items() %}{{ k }}={{ v }};{% endfor %}, which prints a=1;b=2; for d = {"a": 1, "b": 2}. Use range: {% for i in range(3) %} runs for 0, 1, 2.

Conditions: {% if n > 10 %}big{% elif n > 5 %}mid{% else %}small{% endif %}; with n = 7 this prints mid. You can use and, or, not, in, and tests such as is defined, is even.

{% set %} and its scope. set makes a variable. A set inside a for loop is local to that loop and is gone afterwards.

from jinja2 import Template
t = Template("""
{% for i in range(3) %}
  {% set x = i %}
{% endfor %}
[{{ x }}]
""")
print(t.render())

The output contains []: the x outside the loop is undefined, so it prints nothing (not 0, not 2). A past paper asked exactly this twice, once with range(3) and once with [1, 2, 3] in a Flask template (answer: "Output is:" and nothing after it). To add up values across a loop, use namespace:

{% set ns = namespace(t=0) %}{% for i in [1,2,3] %}{% set ns.t = ns.t + i %}{% endfor %}{{ ns.t }}

This prints 6. A set outside any loop works normally: {% set x = 5 %}[{{ x }}] prints [5].

Whitespace. Jinja2 keeps the newlines around your tags. A minus sign next to the tag brackets trims the whitespace on that side:

A = "<ul>\n{% for x in c %}\n<li>{{ x }}</li>\n{% endfor %}\n</ul>"
B = "<ul>\n{% for x in c -%}\n<li>{{ x }}</li>\n{%- endfor %}\n</ul>"
# c = ["a", "b"]
# A renders to '<ul>\n\n<li>a</li>\n\n<li>b</li>\n\n</ul>'
# B renders to '<ul>\n<li>a</li><li>b</li>\n</ul>'

The extra blank lines in A are harmless in HTML (the browser ignores them), so you will rarely need -. A past paper question puts groupby in a template:

temp = """
{% for vid, members in interfaces|groupby(attribute='lan') %}
 Interfaces in lan {{ vid }}: {{ members|map(attribute='name') | join(', ') }}
{% endfor %}
"""

With interfaces Ethernet1 (50), Ethernet2 (40), Ethernet3 (50), Ethernet4 (60) the output is three lines: lan 40: Ethernet2, lan 50: Ethernet1, Ethernet3, lan 60: Ethernet4. groupby sorts by the key and groups equal keys together; map(attribute='name') takes each item's name.

Your turn: 1. Predict the loop

{% for c in ['a','b','c'] %}{{ loop.index }}{{ c }}{% if not loop.last %}-{% endif %}{% endfor %} prints what?

Need a hint?

loop.index starts at 1 and a dash follows each item except the last.

Show the full solution

1a-2b-3c

Your turn: 2. Scope of set

What does {% set t = 0 %}{% for i in [1,2,3] %}{% set t = t + i %}{% endfor %}{{ t }} print?

Need a hint?

Is the t set inside the loop the same t as outside?

Show the full solution

0. The inner set creates a new local t that disappears after the loop, so the outer t is still 0. Use namespace to get 6.

Idea 4: Templates in Flask

myapp/
  app.py
  templates/
    base.html
    index.html
    form.html
  static/
    style.css
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", courses=["MAD1", "MAD2"], user="Ria")
  • render_template looks in a folder named templates next to app.py, and extra arguments become variables. If you create the app with Flask(__name__, template_folder='my_templates') Flask looks only in my_templates, so a file in templates/ fails with TemplateNotFound. (This was the 5-mark question in a recent paper: routes using paths inside my_templates work, those asking for index.html or page.html that live elsewhere raise TemplateNotFound.) Subfolders are named in the call: render_template('public/home.html').
  • Static files (CSS, images, JS) go in static/. Link them with url_for:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<img src="{{ url_for('static', filename='logo.png') }}" alt="logo">

which renders as /static/style.css.

url_for(function_name, **values) builds a URL from the name of the view function, so changing the route later does not break your links:

@app.route("/home")
def index(): ...

@app.route("/student/<student_name>/<int:student_id>")
def profile(student_name, student_id): ...

with app.test_request_context():
    print(url_for("index"))                                       # /home
    print(url_for("profile", student_name="John", student_id=101))# /student/John/101
    print(url_for("index", page=2))                               # /home?page=2

Values that are part of the route fill the path; extra values become the query string. The argument is the function name profile, not the path. redirect(url_for("index")) sends the browser a 302 and a Location header, and the browser then makes a new GET (as in the 2022 paper's index2 route).

Idea 5: Reuse: inheritance, include, macros

Inheritance. Put the common page frame in one base file and fill the gaps in each page.

<!-- templates/base.html -->
<!doctype html>
<html>
<head>
  <title>{% block title %}Study Hub{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  {% block content %}{% endblock %}
  {% block foot %}(c) Hub{% endblock %}
</body>
</html>
<!-- templates/page.html -->
{% extends "base.html" %}
{% block title %}Home - {{ super() }}{% endblock %}
{% block content %}<h1>Hello</h1>{% endblock %}

Rendering page.html: the title becomes Home - Study Hub (super() pulls in the parent's block text), the content is <h1>Hello</h1>, and foot is not overridden so the parent's (c) Hub appears. I rendered a small version of this.

The idea

Rules of extends. {% extends "base.html" %} must be the first tag in the child. In a child, only things inside {% block name %}...{% endblock %} are shown; anything outside blocks is ignored. A block is replaced by the child's version; {{ super() }} keeps the parent's text too. The right syntax is {% extends "file" %} and {% block name %} (a block needs a name).

Include inserts another file at that spot, as is: {% include "navbar.html" %}. It sees the same variables. Use it for small repeated pieces (navbar, footer).

Macros are functions in a template:

{% macro box(text, kind='info') %}
  <div class="alert alert-{{ kind }}">{{ text }}</div>
{% endmacro %}

{{ box('Saved') }}
{{ box('Failed', 'danger') }}

Output: <div class="alert alert-info">Saved</div> and <div class="alert alert-danger">Failed</div> (default argument used for the first). In the 2022 paper a macro with a for loop printed the courses as page text, one after another, App1 App2 python Data science Database machine learning, not as a Python list with brackets and quotes. A macro must be called ({{ display(list) }}) or nothing shows.

Your turn: 3. Inheritance

base.html has {% block content %}{% endblock %}. A child contains {% extends "base.html" %}{% block content %}<p>Hi</p>{% endblock %} plus an extra line <h1>Outside</h1> before the block. What shows?

Need a hint?

What happens to content outside blocks in a child?

Show the full solution

Only <p>Hi</p> appears (inside the base layout). The line outside a block is ignored.

Idea 6: Forms: GET and POST into Flask

Template form.html:

{% extends "base.html" %}
{% block content %}
<form action="{{ url_for('signup') }}" method="post">
  <input type="text" name="uname" required>
  <input type="number" name="age" min="5">
  <button type="submit">Send</button>
</form>
{% if error %}<p class="text-danger">{{ error }}</p>{% endif %}
{% endblock %}

Route:

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        name = request.form.get("uname", "").strip()
        age = request.form.get("age", "")
        if not name:
            return render_template("form.html", error="Name is required"), 400
        if not age.isdigit():
            return render_template("form.html", error="Age must be a number"), 400
        return f"Welcome {name}, age {int(age)}"
    return render_template("form.html")

Test results (I posted these with Flask's test client): uname=Ria, age=20 returns Welcome Ria, age 20; an empty name returns the form again with the error and status 400; age=x returns the second error. A plain GET to /signup shows the form.

request.formrequest.argsrequest.method
HoldsPOST body fieldsURL query string"GET" or "POST"
Used withmethod="post"method="get" or a link with ?k=vchoosing what to do
  • A route accepts only GET unless you list others in methods=[...]. Forgetting "POST" makes the form submit fail with 405.
  • request.form["k"] raises 400 Bad Request if the key is missing; request.form.get("k") returns None.
  • A form with method="get" puts the fields in the query string, so you read them with request.args. Reading request.form for a GET form gives nothing.
  • request.args.getlist("tag") returns all values for a repeated key: ?tag=a&tag=b gives ['a', 'b'].
  • Good practice: after a successful POST, do return redirect(url_for(...)) so a page refresh does not submit the form again.

Client-side versus server-side validation

  • Client-side: HTML5 attributes (required, type="email", min, pattern) in the browser. Fast and friendly, but anyone can bypass it (edit the HTML, use curl).
  • Server-side: your Python checks. Required for security and correctness. The form above has both: the browser blocks an empty name, and signup re-checks.

Flask-WTF (with WTForms) is an extension that defines forms as Python classes and adds validators and CSRF protection; know its name and purpose, but plain request.form is what the course shows first.

Watch out - real exam trap
  • request.form is for POST form data; request.args is for URL parameters. Match them to the form's method.
  • Missing methods=["GET", "POST"] gives 405 when the form posts.
  • HTML validation alone is not safe; always validate on the server.
  • For request.args.get(...) a missing key is None; for request.args["..."] it is a 400 error. The paper's login check args["uid"] fails with 400 when uid is absent.

Idea 7: Bootstrap inside templates

Put the CDN link once in base.html, and every page that extends it gets the styles. A loop turns data into a styled table:

<table class="table table-striped">
  <thead><tr><th>#</th><th>Course</th></tr></thead>
  <tbody>
  {% for c in courses %}
    <tr><td>{{ loop.index }}</td><td>{{ c }}</td></tr>
  {% endfor %}
  </tbody>
</table>

With two courses this produces two tr rows, numbered 1 and 2, inside a Bootstrap-striped table. Other useful pieces: a form field is <input class="form-control" ...> in a <div class="mb-3">, a submit button is class="btn btn-primary", messages are alert alert-danger, and a top bar is a navbar (put it in its own file and include it).

Server-side rendering (what you do this week) sends finished HTML: fast first load and easy for search engines. In client-side rendering (MAD2) the server sends JSON and JavaScript builds the page. Neither removes the need for requests after the page loads.

Solved example: 3. Trace a request

Question. The user opens /hello/Ria. Describe all steps and give the HTML, given:

@app.route("/hello/<name>")
def hello(name):
    return render_template("hello.html", n=name, tags=["a", "b"])
<p>Hi {{ n|upper }}!</p>
<ul>{% for t in tags %}<li>{{ t }}</li>{% endfor %}</ul>

Solution. Flask matches /hello/<name> and calls hello("Ria"). render_template finds templates/hello.html and renders it with n="Ria" and tags. The browser receives status 200 and the body <p>Hi RIA!</p> and <ul><li>a</li><li>b</li></ul>. The browser then parses and displays the HTML. That is the whole controller to view path.

In your project

Make one base.html with your Bootstrap link, navbar and {% block content %}, and let every page extend it. Never build HTML strings in Python; always use render_template. In the viva you may be asked to add a field to a form: be ready to change the HTML input, the route (request.form), validation, and the template that shows it.

Your turn: 4. Which request reads what?

A form has method="get" and an input name="q". In the route, which expression reads the value: request.form['q'] or request.args['q']?

Need a hint?

GET forms put the data in the URL.

Show the full solution

request.args['q'] (and request.args.get('q') if it may be missing). request.form would be empty.

Your turn: 5. Escape or not

render_template_string("{{ s }}", s="<i>x</i>") in Flask: what does the page source contain? And with {{ s|safe }}?

Need a hint?

Flask autoescapes by default.

Show the full solution

&lt;i&gt;x&lt;/i&gt; (the user sees the literal text <i>x</i>). With |safe the source has <i>x</i>, which the browser shows as italic x.

Exam radar

Exam radar

This week mixes templates with Flask routing. The keyword counts are about 140 of 692 in Quiz 1, 45 of 624 in Quiz 2 and 252 of 1600 in the End Term (loose, as the match also catches forms and routes). In the real questions:

  • "Predict the output of this Jinja2 / string.Template code" appears again and again in Quiz 1: substitute versus safe_substitute (KeyError), {{ }} versus $, a missing variable printing blank, a method call in {{ }}, groupby, passing a dictionary to render.
  • {% set %} inside a loop (about two to four questions in recent Quiz 2 papers): the value is not visible after the loop.
  • Template inheritance syntax (extends, a named block), and macros (a loop printing text).
  • Syntax questions: {{ courses|length }} for the count; matching PyHTML, Jinja2, template rendering and string template.
  • Flask view questions: url_for output, redirect, template_folder and TemplateNotFound, abort(404) versus render_template(...), 404, first_or_404().
  • Statement questions: why server-side Jinja2 gives a faster first load and good SEO; front-end and back-end validation of a form.
Watch out - real exam trap
  • {{ x }} after a loop that did set x inside prints nothing.
  • Autoescape is on in Flask .html templates, off in bare jinja2.Template.
  • url_for takes the function name, not the URL; extra values go to the query string.
  • Returning render_template("404.html") alone gives status 200; to send a 404 you need , 404 or abort(404).
  • In the child template, text outside blocks is ignored; extends goes first.
  • Case matters in variable names: {{ Level }} is not {{ level }}.

Quick quiz

Quick quiz

Which code throws a KeyError? t = Template("The $color car is at the $building.") (string.Template)

  • t.substitute({'color': 'red'})
  • t.safe_substitute({'color': 'green'})
  • t.substitute({'color': 'blue', 'building': 'office', 'place': 'Chennai'})
  • t.safe_substitute({})
substitute needs every placeholder; the building key is missing. Extra keys are ignored and safe_substitute never raises.

What is printed? Template("{% for i in range(3) %}{% set x = i %}{% endfor %}[{{ x }}]").render()

  • [0]
  • [2]
  • []
  • an error
A set inside a for loop is local to the loop, so x is undefined outside and prints as nothing.

Which is the correct way to show how many courses are in a list in Jinja2?

  • {{ len(courses) }}
  • {{ courses|len }}
  • {{ courses|length }}
  • {{ courses.size }}
length is the Jinja2 filter; len is not available as a function or a filter.

Select all TRUE statements. (select all that apply)

  • {% extends "base.html" %} must come first in the child template.
  • {{ super() }} inside a block includes the parent's block content.
  • Text outside blocks in a child template is shown.
  • request.form reads the URL query string.
Content outside blocks is ignored, and request.args (not form) reads the query string.

A Flask app is created with Flask(__name__, template_folder='my_templates'). A route renders 'index.html', which exists only in templates/. What happens?

  • It renders normally
  • Flask falls back to templates/
  • TemplateNotFound error (a 500 page)
  • 404 Not Found
Only the folder you named is searched; a missing template raises TemplateNotFound.

render_template_string("{{ s }}", s="<b>x</b>") in Flask returns...

  • <b>x</b> as real bold text
  • &lt;b&gt;x&lt;/b&gt; which the browser shows as the characters
  • an error
  • an empty string
Autoescape turns the angle brackets into entities unless you apply |safe.

Viva: say it out loud

Viva: say it out loud
What is a template engine and why use one?

It lets me write the HTML once with placeholders and loops, and the server fills it with data at request time. This keeps Python logic and page layout separate (the controller and the view), and avoids building HTML in strings.

Explain extends, block and include in your project.

My base.html has the Bootstrap link, navbar and an empty content block. Each page uses extends to inherit that frame and fills the content block. I use include for small repeated parts like a footer.

What is autoescaping and why does it matter?

Jinja2 converts characters like < into &lt; so text typed by a user is shown as text and cannot inject script. It stops cross-site scripting. I use |safe only for HTML I wrote myself.

How does form data reach your Flask code?

The browser sends name=value pairs. For POST they are in the body and I read request.form; for GET they are in the URL and I read request.args. The route must list the method in methods.

Why validate on the server when the HTML already has required?

Client-side checks only help honest users: anyone can bypass them with developer tools or curl. The server must check again before saving anything, and show the form with an error message.

What does url_for('static', filename='style.css') do?

It builds the URL of a file in the static folder, /static/style.css. Using url_for means links stay correct even if I move the app or change a route.

What is the difference between a macro and an include?

An include just pastes another template at that spot. A macro is like a function with parameters that I call many times with different arguments, for example one alert box that takes a message and a colour.

Recap

Remember this
  • f"...", format and string.Template fill blanks in Python; substitute raises KeyError for a missing key, safe_substitute does not.
  • Jinja2: {{ }} prints, {% %} is logic, {# #} is a comment. Filters: length, upper, default, join, round.
  • A missing variable is blank; set in a loop does not leak out; for has loop.index, loop.first, loop.last and an else for empty lists.
  • Flask: templates/ folder, static/ folder, render_template, url_for (function names), redirect.
  • Reuse with extends + block + super(), include, and macros.
  • Forms: methods=["GET", "POST"], request.form for POST, request.args for GET. Validate on the server; HTML validation is only a convenience. Flask-WTF is the extension that formalises this.
  • Autoescape protects against injected HTML; |safe switches it off for trusted text.
Code and facts to keep
  • Render: render_template("page.html", name=value)
  • Static link: href="{{ url_for('static', filename='style.css') }}"
  • Loop: {% for x in xs %}...{{ loop.index }}...{% else %}empty{% endfor %}
  • If: {% if a %}..{% elif b %}..{% else %}..{% endif %}
  • Inheritance: {% extends "base.html" %} and {% block content %}...{{ super() }}{% endblock %}
  • Macro: {% macro name(arg, opt='x') %}...{% endmacro %} then {{ name(1) }}
  • Whitespace trim: {%- ... -%}; comment: {# ... #}
  • Form data: request.form.get("k"), request.args.get("k"), request.method
  • Count: {{ items|length }}; default: {{ x|default('n/a') }}
  • string.Template: $name, substitute() (KeyError) / safe_substitute()
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All Quiz 1 papers