Flask jinja2.exceptions.TemplateNotFound (tested)

jinja2.exceptions.TemplateNotFound: index.html means none of Flask's template loaders found that exact name. In our tests the working directory did not matter; what did: the name's case, a templates/ prefix, a templates folder outside the package, and blueprints without template_folder.

Setup for block 1: /w/app/b1.py next to /w/app/templates/index.html, run from / (a different working directory) on purpose.

1. The name you pass is wrong (case, prefix)

from flask import Flask, render_template
app = Flask(__name__)
print("root_path:", app.root_path, "| template_folder:", app.template_folder)
with app.test_request_context():
    print(render_template("index.html", name="Ada"))
    for name in ("Index.html", "templates/index.html", "missing.html"):
        try:
            render_template(name)
        except Exception as e:
            print(f"{name!r}: {type(e).__name__}: {e}")

Output:

root_path: /w/app | template_folder: templates
<h1>hi Ada</h1>
'Index.html': TemplateNotFound: Index.html
'templates/index.html': TemplateNotFound: templates/index.html
'missing.html': TemplateNotFound: missing.html

Flask looks in templates/ next to the module that creates the app (app.root_path), not in the current working directory, so running from another directory was fine here. What broke it: a different case (Index.html; Linux filesystems are case-sensitive, so code that works on Windows or macOS can fail in a Linux container) and a templates/ prefix. Pass the path relative to the templates folder.

2. templates/ is in the wrong place (package layout)

Layout: proj/myapp/__init__.py creates the app, but the templates live in proj/templates/.

import os
from flask import render_template
from myapp import app
print("root_path:", app.root_path)
print("looking in:", os.path.join(app.root_path, app.template_folder))
with app.test_request_context():
    try:
        render_template("dash.html")
    except Exception as e:
        print(f"{type(e).__name__}: {e}")

# fix: point template_folder at the real directory
from flask import Flask
here = os.path.dirname(os.path.abspath(__file__))
app2 = Flask("myapp", template_folder=os.path.join(here, "templates"))
with app2.test_request_context():
    print(render_template("dash.html"))

Output:

root_path: /w/proj/myapp
looking in: /w/proj/myapp/templates
TemplateNotFound: dash.html
<p>dash</p>

With a package, root_path is the package directory, so Flask looks in myapp/templates/. Either move the folder inside the package, or pass template_folder as an absolute path built from __file__. A relative template_folder is relative to root_path, not to the working directory.

3. Blueprints need template_folder (and a subfolder)

Layout: bp/templates/index.html (app) plus bp/shop/templates/index.html and bp/shop/templates/shop/list.html (blueprint).

from flask import Flask, Blueprint, render_template
app = Flask(__name__)
shop = Blueprint("shop", "shop", template_folder="templates")
app.register_blueprint(shop)
with app.test_request_context():
    print(render_template("shop/list.html"))
    print(render_template("index.html"), "<- app's templates/ wins over the blueprint's")
app2 = Flask(__name__)
app2.register_blueprint(Blueprint("shop", "shop"))  # no template_folder
with app2.test_request_context():
    try:
        render_template("shop/list.html")
    except Exception as e:
        print(f"without template_folder: {type(e).__name__}: {e}")

Output:

<p>shop list</p>
<p>APP index</p> <- app's templates/ wins over the blueprint's
without template_folder: TemplateNotFound: shop/list.html

A blueprint's templates are only searched if you pass template_folder. Its folder is searched after the app's, so a blueprint index.html is shadowed by the app's index.html. Put blueprint templates in a subfolder named after the blueprint (shop/templates/shop/list.html) and render "shop/list.html".

4. Ask Flask where it looked

import logging
from flask import Flask, render_template
app = Flask(__name__)
app.config["EXPLAIN_TEMPLATE_LOADING"] = True
app.logger.setLevel(logging.INFO)  # the explanation is logged at INFO
print("templates Flask can see:", app.jinja_env.list_templates())
with app.test_request_context():
    try:
        render_template("Index.html")
    except Exception as e:
        print(f"{type(e).__name__}: {e}")

Output:

templates Flask can see: ['index.html']
[2026-10-23 04:47:04,181] INFO in debughelpers: Locating template 'Index.html':
    1: trying loader of application '__main__'
       class: jinja2.loaders.FileSystemLoader
       encoding: 'utf-8'
       followlinks: False
       searchpath:
         - /w/app/templates
       -> no match
Error: the template could not be found.
TemplateNotFound: Index.html

app.jinja_env.list_templates() lists every name Flask can load; compare it with the name you pass. EXPLAIN_TEMPLATE_LOADING = True logs each loader and its searchpath, but at INFO level: outside debug mode you also need app.logger.setLevel(logging.INFO), or nothing is printed.

Checklist

Frequently asked questions

Where does Flask look for templates?

In the templates folder next to the module or package that calls Flask(__name__) (app.root_path), plus each blueprint's template_folder. Not the current working directory.

Why does render_template work on Windows but not in Docker?

Linux filesystems are case-sensitive: 'Index.html' does not match index.html. Use the exact file name.

How do I see where Flask searched for a template?

Set app.config['EXPLAIN_TEMPLATE_LOADING'] = True and app.logger.setLevel(logging.INFO), or print app.jinja_env.list_templates().

Written with AI assistance. Measured on 2026-10-23 with Flask 3.1.3, Jinja2 3.1.6 and Python 3.14.7 (python:3-alpine in Docker). Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python and Flask are free.