The Flask Mega-Tutorial, Part II: Templates (2012)

Posted by
on under

(Great news! There is a new version of this tutorial!)

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.

NOTE: This article was revised in September 2014 to be in sync with current versions of Python and Flask.

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 applications of this kind. Ignore for now the fact that we have no way to log a user in, 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 application 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 application that depend on parts of the system that haven't been built yet.

I hope you agree with me that the solution used above to deliver HTML to the browser 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 filename 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 showing an empty title 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 conditional statement works.

Loops in templates

The logged in user in our microblog application will probably want to see recent posts from followed users 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 a list, 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 list 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>Welcome to microblog</title>
    {% endif %}
  </head>
  <body>
    <h1>Hi, {{ user.nickname }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.nickname }} says: <b>{{ post.body }}</b></p></div>
    {% 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 application 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 login, logout, etc.

We can add a navigation bar to our index.html template, but as our application grows we will be needing to implement more pages, and this navigation bar will have to be copied to all of them. Then you will have to keep all these identical copies of the navigation bar in sync, and that could become a lot of work if you have a lot of pages and 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>Welcome to 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 their content can be replaced or enhanced in derived templates.

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

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!

45 comments
  • #26 Paolo said

    the line "from app import db" did not work for me in db_create.py

    I had to copy the code to set the db variable from init.py to get db_create.py to work.

  • #27 Wendy said

    Hi Miguel,

    I'm using your O'Reilly book, and I'm stuck trying to get my favicon to render. I put the file (called "favicon.ico" in a directory called "static" in the app's root folder and implemented the code you prescribe in Example 3-10 in templates/base.html. But no go--just the plain old little blank page with a folded corner is showing up. Any thoughts? Thanks for the great information!

  • #28 Miguel Grinberg said

    @Wendy: if you are running with Flask's development web server then note that browsers will request the favicon.ico file as /favicon.ico, while static files go in /static/filename. In production I use nginx, and I add a special location block that redirects /favicon.ico to /static/favicon.ico. For development I just ignore the problem, but one way to make it work is to add a specific <link> tag that shows the correct URL for the favicon.ico in the static folder. I hope this helps.

  • #29 Pranav said

    how does the "render_template" command know where to look for templates.Is the "templates" folder default for it?

  • #30 Miguel Grinberg said

    @Pranav: The Flask class takes many optional arguments, one is to specify the location of the templates folder. The default is, as you suspected, a folder called "templates" located where the application is.

  • #31 Armen Babakanian said

    Very well documented tutorial Miguel, thanks for sharing these.

  • #32 Brett Gerry said

    Thanks for this tutorial Miguel. I notice you didn't include <div> tags around the content for the posts initially, but added it in the second example. Any reason for this?

  • #33 Miguel Grinberg said

    @Brett: no reason, something that I missed. I'll add the <div> in the first example as well.

  • #34 Jim said

    I get an error that 'render_template' is not defined. I tried reinstalling all of the flask framework to no avail. Any advice?

  • #35 Miguel Grinberg said

    @Jim: make sure you have a "from flask import render_template" at the top of the file.

  • #36 Firdaus said

    Hi James,

    It could be spelling error --> render_template(~/tempaltes/index.html)

    Check your spelling of temPLates

  • #37 David said

    Hey Miguel,
    Thanks for making this, I'm also slowly working through the book.

    Quick question: for the nav bar inheritance, how would one have style tethered to a nav bar. Would base.html reference the style sheet (in /statc?) or the new page itself.

  • #38 Miguel Grinberg said

    @David: base.html can include any stylesheets that apply to the page as a whole. Then any individual pages can add their own stylesheets if necessary.

  • #39 Vic said

    Thanks for rebooting this tutorial in python3 which I have hardly touched! The Flask documentation is excellent, but going through a full tutorial helps tie things together.

  • #40 Nuno said

    Men Thx so much , i m new in this flask world and thx to you site a learn a lot!!

  • #41 Harry John said

    Hi, I'm getting the error, NoneType not callable. And it seems to be happening after I revisit the login url after a successful login.
    I have asked a question on stackoverflow with the link http://stackoverflow.com/questions/29126964/flask-login-user-callback-function-resets-to-none-after-login

  • #42 Subhash Gupta said

    Hi Miguel
    This tutorial on flask is very informative and very easy as compared to other.
    Thank you for it.
    I am facing problem after adding posts to the views.py
    this is the error i get each time i try to run it
    SyntaxError: invalid syntax
    Subhashs-MacBook-Pro:microblog Subhash$ ./run.py
    Traceback (most recent call last):
    File "./run.py", line 2, in <module>
    from app import app
    File "/Users/Subhash/Desktop/microblog/app/init.py", line 4, in <module>
    from app import views
    File "/Users/Subhash/Desktop/microblog/app/views.py", line 15
    'body' : 'I love Jasmeen..:-*!'
    ^
    SyntaxError: invalid syntax

    kindly help me out
    thanks

  • #43 Miguel Grinberg said

    @Subhash: not sure what the problem is. Likely in the line above or below line 15 of views.py. Check your syntax carefully.

  • #44 Marcus Yeagle said

    Wow this tutorial is fantastic. Really good stuff here! Thanks Miguel!

  • #45 Rajesh said

    If you are not getting any thing from HTML, its because of template path.

    tmpl_dir = os.path.join(os.path.dirname(os.path.abspath(file)), 'templates')
    print tmpl_dir
    app = Flask("name", template_folder=tmpl_dir)