Skip to content

Displaying a Table

Paragraphs work, but a list of people with multiple fields really belongs in a table. HTML has proper table tags built in — and combining them with the Jinja2 loop gives you a clean, readable result.


HTML table tags

A table is built from a few tags working together:

Tag What it does
<table> The table container
<thead> The header row section
<tbody> The data rows section
<tr> A single row (table row)
<th> A header cell (bold, centred by default)
<td> A data cell

A simple table with two rows looks like this:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Age</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alice</td>
      <td>24</td>
    </tr>
  </tbody>
</table>

Combining the table with the Jinja2 loop

The header row is fixed — you write it once. The data rows are where the loop goes. Each iteration of the loop produces one <tr> with the person's data in <td> cells.

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Age</th>
      <th>Gender</th>
    </tr>
  </thead>
  <tbody>
    {% for person in people %}
    <tr>
      <td>{{ person.name }}</td>
      <td>{{ person.age }}</td>
      <td>{% if person.gender == 1 %}Male{% else %}Female{% endif %}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

The loop sits inside <tbody>. Each time it runs, it writes one <tr> with three <td> cells. When it finishes, </tbody> closes.


The complete files

app.py

from flask import Flask, render_template

app = Flask(__name__)

people = [
    {"name": "Alice", "age": 24, "gender": 1},
    {"name": "Bob",   "age": 31, "gender": 2},
    {"name": "Carol", "age": 19, "gender": 1},
]

@app.route('/')
def home():
    return render_template('index.html', people=people)

if __name__ == '__main__':
    app.run(debug=True)

templates/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>People</title>
  </head>
  <body>

    <h1>People</h1>

    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Gender</th>
        </tr>
      </thead>
      <tbody>
        {% for person in people %}
        <tr>
          <td>{{ person.name }}</td>
          <td>{{ person.age }}</td>
          <td>{% if person.gender == 1 %}Male{% else %}Female{% endif %}</td>
        </tr>
        {% endfor %}
      </tbody>
    </table>

  </body>
</html>

Run the app and visit http://127.0.0.1:5000. You should see a proper table with one row per person.


Challenge

Add two more people to the people list in app.py. Save and refresh. The table grows automatically — you never touched the HTML.

Then add a fourth column to the table — <th>ID</th> in the header and <td>{{ loop.index }}</td> in the loop. loop.index is a Jinja2 built-in that gives you the current row number starting from 1. Does the table now show a numbered ID column?


← Looping in Your Template    Next: Building the Form →