Flask is often a joy to use due to its ease of use, but under its simple exterior lies a powerful and incredibly useful context and templating system that allows you to build powerful Python-powered pages.
Jinja2
Flask is powered by the Jinja2 templating engine. This powerful library allows us to render HTML dynamically from templates and a custom templating syntax, allowing for reusable code snippets. Furthermore, Jinja2 provides filtering and context processing tools to enable you to present and render data generated in Python.
The structure of a template
Jinja2 templates resemble normal HTML but also incorporate additional control structures:
{# This comment will not be rendered #}
<h1 class="header">{{ app_name }}</h1>
<p class="greeting">Hello {{ app_owner }}</p>
{% for hobby in app_owner_hobbies %}
<p>{{ hobby }}</p>
{% endfor %}
Statements: {% ... %}
Statements enable users to execute actions such as iterating over a list of objects or carrying out conditional rendering.
Expressions: {{ ... }}
Expressions are processed and incorporated into the HTML content. This encompasses variables and function calls and is typically where filters are applied.
Comments: {# ... #}
Similar to other programming languages, Jinja2 includes a commenting syntax. These comments will not appear in the final output, offering a more clean and streamlined alternative to traditional HTML comments.
Rendering a template
Flasks wrapper for Jinja2 provides two main methods of rendering your templates.
Rendering strings
Strings can be rendered by passing them to flask.render_template_string
.
The first and only positional argument is the string to be rendered. It also accepts any number of keyword arguments to act as the template context.
INDEX_TEMPLATE = "
{# This comment will not be rendered #}
<h1 class="header">{{ app_name }}</h1>
<p class="greeting">Hello {{ app_owner }}</p>
{% for hobby in app_owner_hobbies %}
<p>{{ hobby }}</p>
{% endfor %}
"
@app.route("/")
def index():
return flask.render_template_string(
INDEX_TEMPLATE,
app_name="Example App",
app_owner="Arjan",
app_owner_hobbies = ["Coding", "AI", "Guitar"]
)
Rendering files
Unless the template in question is very small, you will usually want to store your templates as files. By default, Flask will use a folder called templates
in the root of the project to store these. It doesn’t matter what file extensions these files have; conventionally, give them the extension .jinja
.
@app.route("/")
def index():
return flask.render_template_string(
"index.html",
app_name="Example App",
app_owner="Arjan",
app_owner_hobbies = ["Coding", "AI", "Guitar"]
)
The only difference from rendering a string is that we pass a string containing the path to the file relative to the templates, so the path to the file here is /templates/index.html
Rendered output
<h1 class="header">Example App</h1>
<p class="greeting">Hello Arjan</p>
<p>coding</p>
<p>reading</p>
<p>writing</p>
Including sub-templates
Jinja2 is most powerful when you start separating snippets into reusable files.
We can create a file called base.html
.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{app_name}}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
And modify our previous example to extend this template.
{% extends "base.html" %} {% block content %}
<h1 class="header">{{ app_name }}</h1>
<p class="greeting">{{ app_owner | greet }}</p>
{% for hobby in app_owner_hobbies | no_guitar %}
<p>{{ hobby | title }}</p>
{% endfor %} {% endblock %}
And our new rendered output will include base.html
with the content
block replaced with the content
block in the index.html
file.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Stuffi-Notes</title>
</head>
<body>
<h1 class="header">Example App</h1>
<p class="greeting">Good evening, Arjan</p>
<p>Coding</p>
<p>Reading</p>
</body>
</html>
Context processors
In most web applications, there will be some global variables that never change, such as the application name or the owner. Flask allows us to define context processors, functions that, when called, return a dictionary of values that can then be referenced in templates.
@app.context_processor
def global():
return dict(app_name="Example App", app_owner="Arjan")
Now we don’t need to pass these variables as arguments to the render_template
and render_template_string
methods, as they will be available globally. This can help greatly reduce clutter in the route function.
Template filters
Template filters allow us to modify the result of expressions and statements, allowing us to filter, format, and otherwise modify them before rendering them to the document.
Let’s write two simple filters.
@app.template_filter
def greet(name: str):
now = datetime.datetime.now()
if now.hour < 12:
return f"Good morning, {name}"
elif 12 <= now.hour < 18:
return f"Good afternoon, {name}"
else:
return f"Good evening, {name}"
@app.template_filter
def no_guitar(hobbies: list[str]):
return [h for h in hobbies if h != "guitar"]
We can then modify our template to make use of these new templates.
<h1 class="header">{{ app_name }}</h1>
<p class="greeting">{{ app_owner | greet }}</p>
{% for hobby in app_owner_hobbies | no_guitar %}
<p>{{ hobby | title }}</p>
{% endfor %}
Our result will greet the owner based on the time of day, and it will not include guitar
in the list of hobbies
. Flask also has a number of built-in filters, such as title
, which is simply the str.title
function.
<h1 class="header">Example App</h1>
<p class="greeting">Good evening, Arjan</p>
<p>Coding</p>
<p>Reading</p>
Final thoughts
As you can see, with very few lines of code, we can create beautiful HTML by passing the data we want to be rendered and filtering and modifying it to present it as we wish. I have barely scratched the surface of the many features provided by these libraries, so check out their documentation here:
- Flask
- Jinja2 And for more web application tips, check out my post here: FastAPI Tutorial: Creating REST APIs in Python