Building modern user interfaces with Flask
Build a modular Flask application using Jinja and templates
When I began learning Flask, one of the first questions that I had was how to build a user interface. Having a background in React, I was used to just defining a function that returned a series of tags. I quickly discovered that this would not fly in Flask. My early Flask applications consisted of beefy modules with copious lines of hard-coded, static HTML with inline formatting. I knew that there had to be a better way. This is when I finally dug into what it was that I had installed when I ran pip install flask
, learned about Jinja, and discovered the wonderful world of templates.
Throughout this blog, we will build out a Flask application that uses Jinja and templates. To follow along, you can find the source code on my GitHub.
What is Jinja?
Jinja is a full-featured template engine for Python, and is the default template engine used by Flask. It is similar to the Django template engine but provides Python-like expressions while ensuring that the templates are evaluated in a sandbox. Jinja supports a text-based template language and thus can be used with templates written in many markup languages.
When getting started with templates, Jinja provides immediately useful features like integrating our template’s compile and runtime errors into the standard Python traceback system so that Jinja errors appear just like Python errors. Also, Jinja provides template inheritance which makes it possible to use the same or similar layouts across templates.
What are templates?
In general, templates are files that contain static data as well as placeholders for dynamic data. These placeholders take the form of variables, expressions, and statements that get replaced with values when the template is rendered. Templates are particularly useful in applications when each page will have the same basic layout but a different body.
In Jinja, a template is simply a text file. The Jinja template engine can generate any text-based format, e.g. HTML, XML, CSV, etc. When using Jinja, a template doesn’t need to have a specific extension: .html, .xml, or any other extension is just fine. For example, the Flask application that we will build throughout this blog will use the Jinja template engine to render templates written in HTML.
When building templates for the Jinja template engine, it is important to be familiar with the Jinja template syntax. The Jinja template syntax introduces new tags that are parsed by the Jinja template engine. By default, these tags begin with an opening curly brace {
, followed by a second symbol (e.g. %
, {
, or #
), and closed with that same symbol followed by a closing curly brace }
. In between the curly braces and the symbols is the variable, expression, or statement that we want Jinja to process. The second symbol is what tells Jinja the kind of data to be processed. If you are asking Jinja to do something like to process a conditional loop or a block statement use the %
symbol. If you are asking Jinja to say something like to display a variable use another {
symbol.
Why use templates and the Jinja template engine?
In the days of early web development, a web page was a markup file, like an HTML file, that presented static data. These HTML files resided in a web server which served the files to a web client, most often a browser, upon request. Anyone who requested the web page would be presented the same data as anyone else who requested that web page.
As web development has evolved, us web developers have been enabled to create increasingly customized experiences by replacing static data with dynamic data that changes based on who is making the request. The process has remained very similar: a web client requests a markup file, a web server serves the markup file, and a web client renders the markup file. The difference is that these markup files now contain variables which we can replace with data sourced from elsewhere. The dynamically displayed data can be a single piece of data, e.g. a welcome page that displays the name of the currently logged in user. Or the data can be an entire block of data that has been curated for that specific user, e.g. a list that displays a series of items that have been selected by the currently logged in user. In this case, the block of data and the page presenting the block of data would both be considered templates to Flask and the Jinja template engine.
In addition to creating customized experiences with dynamic data, templates and the Jinja template engine allow us to assemble our Flask application in pieces that are independent of one another making it much more modular. A modular application provides a better user experience because errors in one piece of the application will not take down the entire application. For example, in the case that something happens on the web server and suddenly the header.html
file won’t render, the other pieces of the application would still display rather than displaying just a ‘404 Not Found’ error.
Getting started with templates
We actually already took the first step in getting started when we ran the pip install flask
command. In addition to installing Flask, this command installed the Jinja2
package giving us access to the Jinja template engine.
Our next step is to consolidate the routing logic into a single file to make it easy to define new URLs as we add more templates to our application. We do this in a new file that we name routes.py
by convention. Within the routes.py
file, we import the render_template
method from the flask
module. Now, rather than returning a string containing hard-coded, static HTML, we will use this imported method to return our HTML templates. The first argument that this method accepts is a string with the filename of the template. The proceeding arguments after the filename can be any data that we want to pass to Jinja as keyword arguments.
In order for us to create a URL to a specific template, Flask contains a built-in wrapper for generating routes. It is the route
decorator, and we use this decorator to bind the function rendering the specific template to our desired URL. Decorators in Python provide a way to extend the functionality of a Python function, without permanently modifying it, by wrapping the function with another function containing logic. In our route
decorator example, we are wrapping the function rendering the template with a function that defines the routing logic for that template. When defining routes, there are a couple of important pieces of information to keep in mind. First, a route definition can contain variables so that, for example, we can route directly to a single item within a list of items. Second, the use of trailing slashes in a route definition does have an affect. A route with a trailing slash is similar to a folder in a file system so that if a user inputs the URL without a trailing slash then Flask redirects them to the canonical URL with the trailing slash. A route without a trailing slash is similar to the pathname of a file so that if a user inputs the URL with a trailing slash they will receive a ‘404 Not Found’ error.
Creating templates
When the Python interpreter parses render_template
, Flask instructs it to locate the template file with the designated filename within the templates
folder. Therefore, our next step is to create the aforementioned templates
folder. It is within this folder that we will create our templates.
As mentioned in the What are templates? section above, the Jinja template engine can generate any text-based format. In our application, we will create templates using HTML, but other formats like XML or CSV are acceptable as well. A basic HTML template can be as simple as follows:
<!DOCTYPE html>
<html lang=“en”>
<head>
<title>Using templates in Flask</title>
</head>
<body>
<h1>Hiyee!</h1>
</body>
</html>
We will save this as basic-template.html
in our templates
folder.
Now that we have a template, we will want to render that template. We will do this back in the routes.py
file that we created in the Getting started with templates section above. We will add a new function called basic_template
that calls the render_template
method and passes in the filename of our basic HTML template. Since templates can be written in various markup languages we need to include the extension in the name of the template that we pass to the render_template
method. In order to navigate to our template, we will wrap our basic_template
function with a route
decorator.
@app.route(‘/basic-template’)
def basicTemplate():
return render_template(‘basic-template.html’)
When running this on our local WSGI web server, we can now see our basic HTML template rendered in the browser when we put in the URL http://localhost:5000/basic-template.
Simplifying our layout with template inheritance
A particularly useful feature that Jinja provides is template inheritance which makes it possible to use the same or similar layouts across templates. For example, a standard convention for website design is to use the same header and the same footer across pages while dynamically changing the content between them. In the context of template inheritance, we can implement this with a single base template to contain the header and footer that we want to remain fixed and with various child templates to contain the content that we want to be specific for those pages.
This is where the new tags introduced by the Jinja template syntax that we discussed in the What are templates? section above comes into play. For template inheritance, the Jinja template syntax provides the {% block %}
tag and the {% extends %}
tag. We use the {% block <block_name> %}
tag in the base template to create blocks that we fill in with content from child templates. Anything in between the {% block %}
tags in the base template will be overridden in the child template. We use {% extends <base_template_name> %}
in the child templates to inherit properties like styling and format from the base template. We also use the {% block <block_name> %}
tag in the child templates to define the content in the child templates that we want Jinja to override in the base template.
Let’s simplify our application by refactoring the application’s project to use template inheritance. We start by creating a base template and name it base.html
.
<!DOCTYPE html>
<html>
<head>
<title>Using templates in Flask</title>
<link rel=”stylesheet” href=”static/styles.css”>
</head>
<body>
<header class=”header”>
<h1>Two-Liner of the Day</h1>
</header>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
<footer class=”footer”>
</footer>
</body>
</html>
Next, we create one child template that extends the base template.
{% extends ‘base.html’ %}{% block content %}
<div class=”joke”>
<p style=”color: blue”>I threw a boomerang a few years ago…</p>
</div>
<div class=”link”>
<a href=”{{ url_for(‘secondPage’) }}”>What’s the punchline?</a>
</div>
{% endblock %}
Then we create a second child template.
{% extends ‘base.html’ %}{% block content %}
<div class=”joke”>
<p style=”color: red”>…I now live in constant fear.</p>
</div>
<div class=”link”>
<a href=”{{ url_for(‘firstPage’) }}”>What was the setup?</a>
</div>
{% endblock %}
Lastly, we create new routes for our child templates in routes.py
so that they will render in their own web pages.
@app.route(‘/’)
@app.route(‘/setup’)
def firstPage():
return render_template(‘part-one.html’)@app.route(‘/punchline’)
def secondPage():
return render_template(‘part-two.html’)
By doing this refactoring we now have two routes that, when requested, render our two child templates that both extend our base template.
Navigating between routes
Our layout is finally coming together! We have a base template and two child templates extending that base template and injecting their own specific content. Plus, we have created the URL routes for the child templates that we want the browser to render. From the purely user interface perspective, it seems like we are done. But when we spin up our WSGI web server and run our Flask application, we quickly notice large gaps in our user experience. In order to get from one template to the next, the user would need to know the URL for that template and point their browser to that URL. Arguably, we have not built a user experience at all. Therefore, our last step is to build the navigation between our templates.
Flask adheres to the basic web development paradigm of linking content on a web page to another web page in order to navigate between them, so we could go around our application’s project hardcoding the URL routes that we created in the Creating templates section above. But this would mean a lot of updating if we ever wanted to change these routes. The better approach would be to manage these from a central location. That is where the url_for
method takes the spotlight! This method became available when we installed the Werkzeug
package, the WSGI web server library used by Flask. Werkzeug uses the url_for
method to build URLs for links based on the routes that we have defined centrally in the routes.py
file rather than us having to explicitly define them every time. All that we need to do is provide url_for
with the name of the function defined in routes.py
that returns our desired template.
<a href=”{{ url_for(‘secondPage’) }}”>What’s the punchline?</a>
Using templates and the Jinja template engine, we can build a much more modular application in Flask with a manageable user interface and a familiar user experience!
To see templates and the Jinja template engine in action, clone the source code from my GitHub of the application that we built throughout this tutorial and run it on your local machine.
About the author
Colin Kraczkowsky recently returned to web development after exploring the craft of product management. Colin’s professional history includes working in both enterprise and start up environments to code web and mobile applications, launch new products, build mockups and prototypes, analyze metrics, and continuously innovate.
In his spare time, Colin can be found checking out the latest Alite camp kit for a weekend away in Big Sur, planning his next line down a mountain at Kirkwood, or surfing the Horror section on Netflix. Colin is currently located in San Francisco, California.
Connect with Colin — https://www.linkedin.com/in/colinkraczkowsky