The Flask Mega-Tutorial Part II: Templates

Posted by
on under

In this second installment of the Flask Mega-Tutorial series, I'm going to discuss how to work with templates.

For your reference, below is a list of the articles in this series.

After you complete Chapter 1, you should have a simple, yet fully working web application that has the following file structure:

microblog\
  venv\
  app\
    __init__.py
    routes.py
  microblog.py

To run the application you set the FLASK_APP=microblog.py in your terminal session (or add a .flaskenv file with this variable), and then execute flask run. This starts a web server with the application, which you can open by typing the http://localhost:5000/ URL in your web browser's address bar.

In this chapter you will continue working on the same application, and in particular, you are going to learn how to generate more elaborate web pages that have a complex structure and many dynamic components. If anything about the application or the development workflow so far isn't clear, please review Chapter 1 again before continuing.

The GitHub links for this chapter are: Browse, Zip, Diff.

What Are Templates?

I want the home page of my microblogging application to have a heading that welcomes the user. For the moment, I'm going to ignore the fact that the application does not have the concept of users yet, as this is going to come later. Instead, I'm going to use a mock user, which I'm going to implement as a Python dictionary, as follows:

user = {'username': 'Miguel'}

Creating mock objects is a useful technique that allows you to concentrate on one part of the application without having to worry about other parts of the system that don't exist yet. I want to design the home page of my application, and I don't want the fact that I don't have a user system in place to distract me, so I just make up a user object so that I can keep going.

The view function in the application returns a simple string. What I want to do now is expand that returned string into a complete HTML page, maybe something like this:

app/routes.py: Return complete HTML page from view function

from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    return '''
<html>
    <head>
        <title>Home Page - Microblog</title>
    </head>
    <body>
        <h1>Hello, ''' + user['username'] + '''!</h1>
    </body>
</html>'''

If you are not familiar with HTML, I recommend that you read HTML Markup on Wikipedia for a brief introduction.

Update the view function as shown above and give the application a try to see how it looks in your browser.

Mock User

I hope you agree with me that the solution used above to deliver HTML to the browser is not good. Consider how complex the code in this view function will become when you add blog posts from users, which are going to constantly change. The application is also going to have more view functions that are going to be associated with other URLs, so imagine if one day I decide to change the layout of this application, and have to update the HTML in every view function. This is clearly not an option that will scale as the application grows.

If you could keep the logic of your application separate from the layout or presentation of your web pages, then things would be much better organized, don't you think? You could even hire a web designer to create a killer web site while you code the application logic in Python.

Templates help achieve this separation between presentation and business logic. In Flask, templates are written as separate files, stored in a templates folder that is inside the application package. So after making sure that you are in the microblog directory, create the directory where templates will be stored:

(venv) $ mkdir app/templates

Below you can see your first template, which is similar in functionality to the HTML page returned by the index() view function above. Write this file in app/templates/index.html:

app/templates/index.html: Main page template

<!doctype html>
<html>
    <head>
        <title>{{ title }} - Microblog</title>
    </head>
    <body>
        <h1>Hello, {{ user.username }}!</h1>
    </body>
</html>

This is a mostly standard, very simply HTML page. The only interesting thing in this page is that there are a couple of placeholders for the dynamic content, enclosed in {{ ... }} sections. These placeholders represent the parts of the page that are variable and will only be known at runtime.

Now that the presentation of the page was offloaded to the HTML template, the view function can be simplified:

app/routes.py: Use render\_template() function

from flask import render_template
from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    return render_template('index.html', title='Home', user=user)

This looks much better, right? Try this new version of the application to see how the template works. Once you have the page loaded in your browser, you may want to view the source HTML and compare it against the original template.

The operation that converts a template into a complete HTML page is called rendering. To render the template I had to import a function that comes with the Flask framework called render_template(). This function takes a template filename and a variable list of template arguments and returns the same template, but with all the placeholders in it replaced with actual values.

The render_template() function invokes the Jinja2 template engine that comes bundled with the Flask framework. Jinja2 substitutes {{ ... }} blocks with the corresponding values, given by the arguments provided in the render_template() call.

Conditional Statements

You have seen how Jinja2 replaces placeholders with actual values during rendering, but this is just one of many powerful operations Jinja2 supports in template files. For example, templates also support control statements, given inside {% ... %} blocks. The next version of the index.html template adds a conditional statement:

app/templates/index.html: Conditional statement in template

<!doctype html>
<html>
    <head>
        {% if title %}
        <title>{{ title }} - Microblog</title>
        {% else %}
        <title>Welcome to Microblog!</title>
        {% endif %}
    </head>
    <body>
        <h1>Hello, {{ user.username }}!</h1>
    </body>
</html>

Now the template is a bit smarter. If the view function forgets to pass a value for the title placeholder variable, then instead of showing an empty title the template will provide a default one. You can try how this conditional works by removing the title argument in the render_template() call of the view function.

Loops

The logged in user will probably want to see recent posts from connected users in the home page, so what I'm going to do now is extend the application to support that.

Once again, I'm going to rely on the handy fake object trick to create some users and some posts to show:

app/routes.py: Fake posts in view function

from flask import render_template
from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    posts = [
        {
            'author': {'username': 'John'},
            'body': 'Beautiful day in Portland!'
        },
        {
            'author': {'username': 'Susan'},
            'body': 'The Avengers movie was so cool!'
        }
    ]
    return render_template('index.html', title='Home', user=user, posts=posts)

To represent user posts I'm using a list, where each element is a dictionary that has author and body fields. When I get to implement users and blog posts for real I'm going to try to preserve these field names as much as possible, so that all the work I'm doing to design and test the home page template using these fake objects will continue to be valid when I introduce real users and posts.

On the template side I have to solve a new problem. The list of posts can have any number of elements, it is up to the view function to decide how many posts are going to be presented in the page. The template cannot make any assumptions about how many posts there are, so it needs to be prepared to render as many posts as the view sends in a generic way.

For this type of problem, Jinja2 offers a for control structure:

app/templates/index.html: for-loop in template

<!doctype html>
<html>
    <head>
        {% if title %}
        <title>{{ title }} - Microblog</title>
        {% else %}
        <title>Welcome to Microblog</title>
        {% endif %}
    </head>
    <body>
        <h1>Hi, {{ user.username }}!</h1>
        {% for post in posts %}
        <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
        {% endfor %}
    </body>
</html>

Simple, right? Give this new version of the application a try, and be sure to play with adding more content to the posts list to see how the template adapts and always renders all the posts the view function sends.

Mock Posts

Template Inheritance

Most web applications these days have a navigation bar at the top of the page with a few frequently used links, such as a link to edit your profile, to login, logout, etc. I can easily add a navigation bar to the index.html template with some more HTML, but as the application grows I will be needing this same navigation bar in other pages. I don't really want to have to maintain several copies of the navigation bar in many HTML templates, it is a good practice to not repeat yourself if that is possible.

Jinja2 has a template inheritance feature that specifically addresses this problem. In essence, what you can do is move the parts of the page layout that are common to all templates to a base template, from which all other templates are derived.

So what I'm going to do now is define a base template called base.html that includes a simple navigation bar and also the title logic I implemented earlier. You need to write the following template in file app/templates/base.html:

app/templates/base.html: Base template with navigation bar

<!doctype html>
<html>
    <head>
      {% if title %}
      <title>{{ title }} - Microblog</title>
      {% else %}
      <title>Welcome to Microblog</title>
      {% endif %}
    </head>
    <body>
        <div>Microblog: <a href="/index">Home</a></div>
        <hr>
        {% block content %}{% endblock %}
    </body>
</html>

In this template I used the block control statement to define the place where the derived templates can insert themselves. Blocks are given a unique name, which derived templates can reference when they provide their content.

With the base template in place, I can now simplify index.html by making it inherit from base.html:

app/templates/index.html: Inherit from base template

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

Since the base.html template will now take care of the general page structure, I have removed all those elements from index.html and left only the content part. The extends statement establishes the inheritance link between the two templates, so that Jinja2 knows that when it is asked to render index.html it needs to embed it inside base.html. The two templates have matching block statements with name content, and this is how Jinja2 knows how to combine the two templates into one. Now if I need to create additional pages for the application, I can create them as derived templates from the same base.html template, and that is how I can have all the pages of the application sharing the same look and feel without duplication.

Template Inheritance

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

132 comments
  • #101 Miguel Grinberg said

    @Abhinav: keep working on this tutorial. in a few chapters the nav bar will be modified depending on the user being logged in or not.

  • #102 ing said

    Hi,

    Is there any way to make the terminal reload the server if change were made?
    Up to now, I got to re-start the server each time I made some changes in the app ..

  • #103 Miguel Grinberg said

    @ing: Yes, you can start the server in debug mode by adding export FLASK_ENV=development, either in the command line, or by adding it to the .flaskenv file.

  • #104 Pam said

    from flask import render_template
    from app import app

    @app.route('/')
    @app.route('/index')
    def index():
    user = {'username': 'Miguel'}
    posts = [
    {
    'author': {'username': 'John'},
    'body': 'Beautiful day in Portland!'
    },
    {
    'author': {'username': 'Susan'},
    'body': 'The Avengers movie was so cool!'
    }
    ]
    return render_template('index.html', title='Home', user=user, posts=posts)

    Excellent tutorial..!! I am stuck here with the above code i am " raise TemplateNotFound(template)" added template folder at the same level as app, but still getting the error

  • #105 Miguel Grinberg said

    @Pam: check what the correct project structure is by downloading the code from the links at the top of the article. You must be putting your template in the wrong place.

  • #106 seNex said

    visiting three years later:

    tutorial is still awesome

  • #107 Jerry Kauffman said

    Hi Miguel, As others have stated, your tutorial is top notch.
    I have done a couple other tutorials and at first used MS Visual studio then switched to Pycharm.
    I have finished with your first two chapters and am about to start #3.
    I would like to use Pycharm but when I tried it complained that the directory was not empty.
    Obviously this is a newbee question.
    Can you give any hints on how to switch?

  • #108 Miguel Grinberg said

    @Jerry: I'm not familiar with the error. What directory is it complaining about? Maybe you are creating a new project instead of opening an existing one?

  • #109 Jerry Kauffman said

    See, you do have a hint. I did select new..... not open.....
    Thanks!

  • #110 Adi said

    Hi Miguel,

    Hope you are well.
    My HTML file is not running and I am sure it is in the templates folder and I have rendered it correctly.

    return render_template('index.html', title='Home', user=user, posts=posts)

    Is there any other issue?
    Appreciate it!

  • #111 Miguel Grinberg said

    @Adi: what do you mean by "is not running"? Do you get any errors? What do you see on your browser?

  • #112 Sjoerd said

    Hi Miguel,

    This tutorial is helping me a lot and reflecting much tech of the tutorial on my own application. I more or less finished it (I skipped chapter 18 and 19). At this point I'm about to add a dynamically generated menu (just a small table from the database that is fetch with

    items = SomeData.query.order_by(SomeData.name.asc()).all()

    which is fed to render_template(). The variable for this is obviously in base.html so every page (@bp.route...) needs the get the SomeData which is not the way.. Well - the items variable should be fed to render_temple on each @bp.route but it should not be fetched on each @bp.route. What is the best way to implement this? Is the best way to fetch is in app.init.py ?

    What is best practice

  • #113 Mustopha Mubarak said

    After about 4 year since you wrote this article and I'm still gaining a lot from it....so much thanks to u man

  • #114 Miguel Grinberg said

    @Sjoerd: I don't understand. If you want to render the menu on every page, and the data that supports this menu comes from the database, then you have to query this data for every request. You may optimize by adding caching, for example, but the data needs to be provided to render_template in every request the same.

    Side note, you may want to consider creating a context processor, so that you don't have to pass the data in the render_template call: https://flask.palletsprojects.com/en/1.1.x/templating/#context-processors.

  • #115 Sjoerd said

    @Miguel: The menu is sort of static, the table with menu-items should ideally only be fetched once and not with every request - this reduces database actions and increases the speed. Is it possible to preload these kind of variables in creat_app() on the init.py
    The context processor seems not to do that, but called before every request (as described on the url you specified). If I understand it correcty something like this should be done then:

    @app.context_processor
    def get_static_menu():
    items = SomeData.query.order_by(SomeData.name.asc()).all()
    return dict(menu_items=items)

    Where menu_items is actually the dictionary used in the template. ?

    Regards,
    Sjoerd

  • #116 Miguel Grinberg said

    @Sjoerd: You can cache the data that you read from the database in a global variable, so that it is ready only once and then reused. That doesn't change the fact that a context processor is a good pattern to use, because if not you'll need to pass your menu options in every render_template call.

  • #117 Sjoerd said

    @Miguel: I got it working as you suggested. The context_processor is quite a handy mechanism. I added a method that gets the information and works like a charm. The first time the data is fetched from the database - subsequent request are from the "cache". I checked that with a couple of dirty print statements.

    Regards,
    Sjoerd

  • #118 Ahmed Aly said

    Thanks a lot for this tutorial, Mr. Miguel!

  • #119 Hannes said

    Miguel, thank you so much for this tutorial!

  • #120 Ryan Wyler said

    I was noticing that the html output was not "whitespace" accurate and found an interesting link that helped me figure out how to stop adding extra lines and correctly spacing the lines.

    Link to properly control whitespaces / blank lines in the output:
    https://jinja.palletsprojects.com/en/3.0.x/templates/#whitespace-control

    Here are the updated template files:

    app/templates/base.html

    <!doctype html>
    <html>
        <head>
        {%- if title %}
            <title>{{ title }} - Microblog</title>
        {% else %}
            <title>Welcome to Microblog</title>
        {% endif -%}
        </head>
        <body>
            <div>Microblog: <a href="/index">Home</a></div>
            <hr>
            {%- block content %}{% endblock %}
        </body>
    </html>
    

    app/templates/index.html:

    {% extends "base.html" %}
    {% block content %}
            <h1>Hi, {{ user.username }}!</h1>
    {%- for post in posts %}
            <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {%- endfor %}
    {%- endblock %}
    
  • #121 Ryan said

    Hi Miguel,

    What is the difference between user['username'] and user.username after switching from

    <h1>Hello, ''' + user['username'] + '''!</h1>

    TO

    <h1>Hello, {{ user.username }}!</h1>

    This has been the pain in my past learning of Flask

    Thanks,
    Ryan

  • #122 Miguel Grinberg said

    @Ryan: Thanks for providing the updated templates.In general maintaining the whitespace in HTML templates for a complex application is extremely hard and it really doesn't improve the application in any considerable way, so I do not consider generating clean HTML important. It is actually easier to generate an HTML beautifier/formatter on the raw HTML output from Jinja than manually managing the whitespace in every control structure.

    Regarding the second question, in Jinja templates user['username'] is equivalent to user.username. Jinja uses a simplified dot notation for dictionaries, but keep in mind that this only works in templates, for regular Python code you have to use the dictionary syntax.

  • #123 Ryan said

    Thank you for the explanation

  • #124 Mike said

    When you pass the dictionary for post to the index template - why can you call the dictionary values using the dot notation you would normally use for an object?

    For example why does index use the below:
    user.username
    When you normal access a dictionary using the below:
    user['username']

  • #125 Miguel Grinberg said

    @Mike: Jinja templates have a simplified syntax for dictionaries using the dot as separator. You can also use the standard Python syntax is you prefer.

Leave a Comment