Back to Posts
Code editor displaying CSS with syntax highlighting, emphasizing the .screen-reader class. This image reflects the backend customization often seen in web development, similar to rendering templates with Jinja2 in Flask.

Advanced Flask: Rendering HTML with Jinja2

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:

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts