Jinja2 Templates

Every render_template() call we've used so far hands a template file and some data over to Jinja2, the templating engine Flask uses by default. Jinja2's job is to take an HTML file sprinkled with small placeholders and logic tags, combine it with the Python values your view passed in, and produce the final HTML that gets sent to the browser. This lesson covers Jinja2's own mini-language: variable output, its core tags, its built-in filters, and how data travels from a view into a template in the first place.

Jinja2: Deliberately Simpler Than Python

Jinja2's syntax looks a little like Python — it has if, for, and expressions — but it is intentionally a much smaller language. You cannot define a class in a template, import an arbitrary module, or run a multi-line function. This isn't a limitation to work around; it's a design choice. Templates are meant to describe presentation — how data is arranged and displayed — while business logic (querying the database, deciding what a "reading" book even means, checking permissions) belongs in your view functions. Keeping that boundary enforced by the language itself means a template can never quietly grow into a second copy of your application logic; if you find yourself wanting to write something Jinja2 makes hard, that's usually a sign the computation belongs in the view instead.

The templates/ Folder Convention

render_template() looks for files inside a folder named templates/ sitting next to your application module — no configuration needed for that default location to work. A typical Bookshelf project organizes it by feature:

bookshelf/
  app.py
  templates/
    base.html
    books/
      list.html
      detail.html
      add.html
    partials/
      book_card.html
    errors/
      404.html
      500.html

Passing 'books/list.html' to render_template() refers to that path relative to templates/ — the subfolders are just organization and carry no special meaning to Flask beyond that.

Note: Every template shown on this page is correct, working Jinja2, but this website's own project doesn't run a Flask server, so nothing renders live here — read each one alongside the explanation, then try it in your own project.

Outputting Variables With {{ }}

Double curly braces print a value into the HTML. Anything passed into render_template() as a keyword argument becomes available under that name inside the template, and dotted attribute access works the same way it does on the underlying Python object:

<p>{{ book.title }}</p>
<p>Shelf: {{ book.shelf.name }}</p>

book.shelf.name walks from the Book instance to its related Shelf (via the shelf_id foreign key relationship) and prints that shelf's name — Jinja2 follows attribute access exactly like Python does, it's just written with dots instead of separate lookups.

Core Template Tags Reference

Logic in a template — conditionals and loops — is written between {% %} delimiters rather than {{ }}, since these tags don't output a value directly:

TagPurpose
{% if %} ... {% elif %} ... {% else %} ... {% endif %}Conditional branching, same logic as Python's if/elif/else
{% for item in list %} ... {% endfor %}Loop over an iterable — a list of books, tags, shelves
loop.indexInside a for loop, the special loop variable gives the current position — loop.index is 1-based, loop.index0 is 0-based

Both patterns together, filtering and numbering a list of books:

{% if books %}
  <ol>
  {% for book in books %}
    <li>{{ loop.index }}. {{ book.title }} by {{ book.author }}</li>
  {% endfor %}
  </ol>
{% else %}
  <p>No books on this shelf yet.</p>
{% endif %}

Built-in Filters Reference

A filter transforms a value on its way to being printed, applied with a pipe character: {{ value|filter_name }}. Filters can be chained, and some accept arguments:

FilterEffect
{{ value|default('Untitled') }}Prints 'Untitled' if value is undefined or falsy
{{ book.title|upper }}Converts text to uppercase
{{ description|truncate(100) }}Cuts long text to roughly 100 characters, adding an ellipsis
{{ books|length }}The number of items in a list — equivalent to Python's len()

Comments

Anything between {# #} is stripped out entirely and never reaches the rendered HTML — unlike an HTML comment (<!-- -->), which still ships to the browser and is visible in "View Source":

{# TODO: add a "mark as finished" button here once ratings are in #}

Passing Data From A View To A Template

Every extra keyword argument to render_template() becomes a variable the template can reference by that exact name:

# app.py
from flask import render_template

@app.route('/shelves/<int:shelf_id>')
def shelf_detail(shelf_id):
    shelf = Shelf.query.get_or_404(shelf_id)
    books = Book.query.filter_by(shelf_id=shelf.id).all()
    return render_template('books/list.html', books=books, shelf_name=shelf.name)

Full Worked Example: books/list.html

Combining variable output, dotted lookups, the if/for tags, the loop variable, several filters, and a comment into one real template for the Bookshelf book list:

{# templates/books/list.html #}
<h1>{{ shelf_name|default('All Books') }}</h1>

{% if books %}
  <p>{{ books|length }} book(s) on this shelf.</p>
  <ul>
  {% for book in books %}
    <li>
      {{ loop.index }}. {{ book.title|upper }} by {{ book.author }}
      {% if book.rating %}
        — rated {{ book.rating }}/5
      {% else %}
        — not rated yet
      {% endif %}
      <br>
      <small>{{ book.author }}'s notes: {{ book.description|default('No notes yet')|truncate(100) }}</small>
    </li>
  {% endfor %}
  </ul>
{% else %}
  <p>No books on this shelf yet.</p>
{% endif %}

Passed books=books, shelf_name=shelf.name from the view above, this template renders a numbered list, falls back to a friendly message when the shelf is empty, shows each book's rating only when one exists, and truncates a long description — all without a single line of that logic living inside the view function itself.

Next up: This template stands alone right now, with no shared header or navigation. Template Inheritance & Includes covers pulling shared layout into a base template so every page in Bookshelf shares one nav bar without copy-pasting it everywhere.