2025, Nov 04 07:00
How to pass database query results to Flask Jinja2 templates without tuple-to-int TypeErrors
Resolve Flask Jinja2 TypeError from treating a row tuple as an int. Learn to pass fetchall query results safely, shape data for render_template, and loop fields
Passing database query results from Flask to a Jinja2 template looks straightforward until the template tries to treat a row as a scalar. The symptom is a confusing TypeError about int() receiving a tuple, even though the SQL query itself runs fine. Below is a concise walkthrough of what’s going on, how to reproduce it, and a clean way to render the result set in HTML.
Reproducing the issue
The database query works and returns rows, but handing the raw result to the template ends with an error in the rendering stage. The logic is simple: run a parametrized SELECT, fetch all rows, and pass them to render_template.
import sqlite3 as sql
conn = sql.connect('./database.db')
cur = conn.cursor()
cur.execute(
    'SELECT year, unit, facilities, workforce, vehicles FROM units WHERE unit = ?',
    (u_value,)
)
rows = cur.fetchall()
conn.close()
return render_template('analysis.html', rows=rows)
The error observed during rendering is:
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'tuple'
Converting to list does not change anything because the inner structure remains a tuple of column values, and dumping the result to JSON only turns the whole thing into a string, which then triggers another error in the template:
ValueError: invalid literal for int() with base 10: '['
What actually happens
fetchall() returns a list of tuples, where each tuple represents a row. Jinja2 can iterate these rows without any special handling, but the template must address each row and its fields explicitly. If any part of the template tries to coerce the entire row (a tuple) to an int, the renderer raises the TypeError about int() receiving a tuple. Turning rows into a list does not change the nested structure, and JSON-encoding replaces structured data with a string, which makes type coercion errors more opaque.
A minimal, predictable data shape for the template
A reliable way to prevent these issues is to pass a clean, explicit structure to the template and access it consistently there. If the table needs only some columns, prepare a new list with just those fields and render it directly.
import sqlite3 as sql
conn = sql.connect('./database.db')
c = conn.cursor()
c.execute(
    'SELECT year, unit, facilities, workforce, vehicles FROM units WHERE unit = ?',
    (u_value,)
)
result_set = c.fetchall()
conn.close()
# prepare rows for the view: pick the first three columns per row
view_rows = []
for idx in range(len(result_set)):
    view_rows.append((result_set[idx][0], result_set[idx][1], result_set[idx][2]))
return render_template('analysis.html', view_rows=view_rows)
In the template, iterate over the rows and index the tuple elements directly, keeping the structure aligned with what you passed in.
<tbody>
    {% for rec in view_rows %}
    <tr>
        <td>{{ rec[0] }}</td>
        <td>{{ rec[1] }}</td>
        <td>{{ rec[2] }}</td>
    </tr>
    {% endfor %}
</tbody>
Why this matters
Templates are strict about data shapes. A list of tuples is easy to iterate, but only if the template treats each tuple as a row and each index as a column. Accidental coercion or name collisions obscure the actual issue and produce hard-to-read errors during rendering. Keeping a clear separation between the query parameter, the raw result set, and the view-ready structure makes your code less fragile and your templates easier to reason about.
Practical takeaways
Query results from fetchall() are sequence-of-tuples and can be rendered directly when the template loops over rows and indexes columns. If you only need specific columns, prepare a dedicated list of tuples with exactly those fields and pass it to the template, then iterate and display by index. Avoid reusing the same variable name for different roles, such as the query parameter and the result set, and prefer descriptive names for clarity. If you want keys instead of indexes, consider returning dict-like rows so the template can access fields by name. When something goes wrong in rendering, print or log the actual data structure and include the full traceback; it quickly shows whether the template is operating on the expected shape.
Keep your data shape explicit and your template logic plain. With a predictable interface between your database layer and Jinja2, rendering tables remains straightforward and error-free.
The article is based on a question from StackOverflow by QuestionsAndAnswers and an answer by QuestionsAndAnswers.