The Flask Mega-Tutorial, Part II: Templates

This is the second article in the series where I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to call microblog.

Here is an index of all the articles in the series that have been published to date:

Recap

If you followed the previous chapter you should have a fully working yet very simple web application that has the following file structure:

    microblog\
      flask\
        <virtual environment files>
      app\
        static\
        templates\
        __init__.py
        views.py
      tmp\
      run.py

To run the application you execute the run.py script and then open the http://localhost:5000 url on your web browser.

We are picking up exactly from where we left off, so you may want to make sure you have the above application correctly installed and working.

Why we need templates

Let's consider how we can expand our little application.

We want the home page of our microblogging app to have a heading that welcomes the logged in user, that's pretty standard for apps of this kind. Ignore for now the fact that we have no users in the app, I'll present a workaround for this issue in a moment.

An easy option to output a nice and big heading would be to change our view function to output HTML, maybe something like this:

from app import app

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

Give the app a try to see how this looks in your browser.

Since we don't have support for users yet I have resorted to using a placeholder user object, sometimes called fake or mock object. This allows us to concentrate on certain aspects of our app that depend on parts of the system that haven't been built yet.

I hope you agree with me that the solution above is very ugly. Consider how complex the code will become if you have to return a large and complex HTML page with lots of dynamic content. And what if you need to change the layout of your web site in a large app that has dozens of views, each returning HTML directly? This is clearly not a scalable option.

Templates to the rescue

If you could keep the logic of your application separate from the layout or presentation of your web pages 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 site's behaviors in Python. Templates help implement this separation.

Let's write our first template (file app/templates/index.html):

<html>
  <head>
    <title>{{title}} - microblog</title>
  </head>
  <body>
      <h1>Hello, {{user.nickname}}!</h1>
  </body>
</html>

As you see above, we just wrote a mostly standard HTML page, with the only difference that there are some placeholders for the dynamic content enclosed in {{ ... }} sections.

Now let's see how we use this template from our view function (file app/views.py):

from flask import render_template
from app import app

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

Try the application at this point to see how the template works. Once you have the rendered page in your browser you may want to view the source HTML and compare it against the original template.

To render the template we had to import a new function from the Flask framework called render_template. This function takes a template name and a variable list of template arguments and returns the rendered template, with all the arguments replaced.

Under the covers, the render_template function invokes the Jinja2 templating engine that is part of the Flask framework. Jinja2 substitutes {{...}} blocks with the corresponding values provided as template arguments.

Control statements in templates

The Jinja2 templates also support control statements, given inside {%...%} blocks. Let's add an if statement to our template (file app/templates/index.html):

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

Now our template is a bit smarter. If the view function forgets to define a page title then instead of raising an exception the template will provide its own title. Feel free to remove the title argument in the render_template call of our view function to see how the if statement works.

Loops in templates

The logged in user in our microblog app will probably want to see recent posts from users in his or her contact list in the home page, so let's see how we can do that.

To begin, we use our handy fake object trick to create some users and some posts to show (file app/views.py):

def index():
    user = { 'nickname': 'Miguel' } # fake user
    posts = [ # fake array of posts
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]
    return render_template("index.html",
        title = 'Home',
        user = user,
        posts = posts)

To represent user posts we are using an array where each element has author and body fields. When we get to implement a real database we will preserve these field names, so we can design and test our template using the fake objects without having to worry about updating it when we move to a database.

On the template side we have to solve a new problem. The array can have any number of elements, it will be up to the view function to decide how many posts need to be presented. The template cannot make any assumptions about the number of posts, so it needs to be prepared to render as many posts as the view sends.

So let's see how we do this using a for control structure (file app/templates/index.html):

<html>
  <head>
    {% if title %}
    <title>{{title}} - microblog</title>
    {% else %}
    <title>microblog</title>
    {% endif %}
  </head>
  <body>
    <h1>Hi, {{user.nickname}}!</h1>
    {% for post in posts %}
    <p>{{post.author.nickname}} says: <b>{{post.body}}</b></p>
    {% endfor %}
  </body>
</html>

Simple, right? Give it a try, and be sure to play with adding more content to the posts array.

Template inheritance

We have one more topic to cover before we close for the day.

Our microblog web app will need to have a navigation bar at the top of the page with a few links. Here you will get the link to edit your profile, to logout, etc.

We can add a navigation bar to our index.html template, but as our application grows we will be needing more templates and this navigation bar will have to be copied to all of them. Then you will always have to keep all these copies in sync, and that could become a lot of work if you have a lot of templates.

Instead, we can use Jinja2's template inheritance feature, which allows us to move the parts of the page layout that are common to all templates and put them in a base template from which all other templates are derived.

So let's define a base template that includes the navigation bar and also the bit of title logic we implemented earlier (file app/templates/base.html):

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

In this template we use the block control statement to define the place where the derived templates can insert themselves. Blocks are given a unique name.

And now what's left is to modify our index.html template to inherit from base.html (file app/templates/index.html):

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

Since the base.html template will now take care of the general page structure we have removed those elements from this one and left only the content part. The extends block establishes the inheritance link between the two templates, so that Jinja2 knows that when it needs to render index.html it needs to include 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 into one. When we get to write new templates we will also create them as extensions to base.html.

Final words

If you want to save time, the microblog application in its current state is available here:

Download microblog-0.2.zip.

Note that the zip file does not include the flask virtual environment, you will need to create it following the instructions in the first chapter in the series before you can run the application.

If you have any questions or comments feel free to leave them below.

In the next chapter of the series we will be looking at web forms. I hope to see you then.

Miguel

21 comments

  • #1 Bob said :

    I don't have anything templatized under index.html, but I do have templatized code under some javascripts that gets included under <head> of index.html. How do I render_template on these javascripts have index.html read them?

  • #2 Miguel Grinberg said :

    You can easily work with javascript templates. Just put your javascript file in the app's template directory and add your template substitutions like you would on an HTML file. Then create a view function that renders this template. The view will be associated with a route, like a regular html based view. Finally, in the script tag that imports this javascript set the src argument to the javascript route instead of using a static javascript filename. I hope this helps!

  • #3 Bob said :

    This helps, but I'm totally new to Flask. A short extension to the current tutorial would definitely help, both me and other. Thanks

  • #4 Miguel Grinberg said :

    Well, the thing is I'm not really sure there is a good reason to use javascript templates, and that doesn't really fit well with this tutorial. It seems to me you could move the dynamic sections of your javascript to the parent HTML and then pass these as arguments to javascript functions. But in any case, if you ask this question in stackoverflow.com and send me the link I'll be happy to go into more detail regarding how to implement it.

  • #5 Catherine Penfold said :

    Hi Miguel, I have changed the <title> tags to <h1> as they don't seem to work. Is this a browser issue? Thanks

  • #6 Miguel Grinberg said :

    Catherine, the <title> tags work on all browsers. What is it that you think doesn't work? Browsers typically show the page title in the window's title bar, this is not part of the page.

  • #7 Catherine Penfold said :

    Hi Miguel, Ah yes, I see it now. Did you have any suggestions re the flask-wtf? thanks

  • #8 Catherine said :

    Oh, and I worked out the issue with the ./run.py not working, it's because you need to change the file permissions (chmod 755) - you might want to add that to your tutorial

  • #9 Miguel Grinberg said :

    I have added the chmod step for the run.py script, I've missed that. As far as the flask-wtf I don't know. You should not need to move anything, the pip installer should be putting everything in the right location. If you are invoking the Python interpreter from the virtual environment (by having the shebang line point to flask/bin/python) then all the installed modules should be reachable.

  • #10 Anthony said :

    Thanks for the tutorial series. I was a web developer in PHP/ASP a long time past, and I've been trying to get back into it for a project. It's been difficult jumping back in when so much has changed (yay, standards! yay, css3!). This is slowly getting me back on the path to production.

Leave a Comment

Note: all comments are screened before they are published. Thank you for your patience!