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
- The folder is named
templates(plural, lowercase) and sits next to the module or inside the package that callsFlask(__name__). - The name matches the file exactly, including case, with no
templates/prefix. - Blueprints:
template_folder="templates"passed, templates in a subfolder named after the blueprint. - In Docker or a package install: the templates were actually copied (check
list_templates()inside the container).
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.