Flask
Jinja2
HTML
Template Rendering
Web Development

Passing HTML to template using Flask/Jinja2

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Passing HTML into a Flask template is easy mechanically, but the security implications matter more than the syntax. Jinja2 escapes variables by default, so the real question is not just how to render HTML, but when a value is trusted enough to be rendered as markup instead of plain text.

Why Jinja2 Escapes HTML by Default

In a normal Flask template, this expression is escaped:

jinja2
{{ body }}

If body contains "<strong>Hello</strong>", the browser will display the tags as text instead of rendering bold output. That behavior is intentional. Autoescaping protects your app from cross-site scripting, which is exactly what happens when untrusted content is injected into a page as live HTML.

A small Flask example shows the default behavior:

python
1from flask import Flask, render_template_string
2
3app = Flask(__name__)
4
5@app.route("/")
6def index():
7    body = "<strong>Hello</strong> from Flask"
8    return render_template_string("{{ body }}", body=body)
9
10if __name__ == "__main__":
11    app.run(debug=True)

Visit the route and you will see the literal markup characters, not bold text. That is usually what you want for raw user input.

Rendering Trusted HTML Intentionally

If the HTML was generated by your own application and you know it is safe, you can mark it as trusted. The most direct approach is Markup from markupsafe.

python
1from flask import Flask, render_template
2from markupsafe import Markup
3
4app = Flask(__name__)
5
6@app.route("/profile")
7def profile():
8    bio_html = Markup("<p><strong>Ada</strong> writes about compilers.</p>")
9    return render_template("profile.html", bio=bio_html)
10
11if __name__ == "__main__":
12    app.run(debug=True)

Template:

jinja2
<h1>Author Profile</h1>
<div class="bio">{{ bio }}</div>

Because bio is marked safe, Jinja2 will render it as HTML rather than escaping it.

You can also use the safe filter in the template:

jinja2
<div class="bio">{{ bio|safe }}</div>

That works, but in many codebases it is better to make the trust decision in Python rather than sprinkling |safe through templates. It is easier to review and easier to test.

Sanitizing User-Provided HTML

If the content comes from users, a database field, or a CMS, “safe” and “works” are not the same thing. Untrusted HTML should be sanitized before rendering.

A common option is bleach:

python
1from flask import Flask, render_template
2from markupsafe import Markup
3import bleach
4
5app = Flask(__name__)
6
7ALLOWED_TAGS = ["p", "strong", "em", "a", "ul", "li"]
8ALLOWED_ATTRIBUTES = {"a": ["href", "title"]}
9
10@app.route("/post")
11def post():
12    raw_html = '<p>Welcome <strong>reader</strong>. <script>alert(1)</script></p>'
13    cleaned = bleach.clean(
14        raw_html,
15        tags=ALLOWED_TAGS,
16        attributes=ALLOWED_ATTRIBUTES,
17        strip=True,
18    )
19    return render_template("post.html", body=Markup(cleaned))
20
21if __name__ == "__main__":
22    app.run(debug=True)

This keeps allowed formatting while stripping dangerous tags and attributes. That is a much safer pattern than marking user data as safe directly.

Passing Structured Data Is Often Better

A lot of applications do not need to pass HTML at all. If the content is really data, pass the data and let the template decide how to render it.

For example, instead of assembling an HTML list in Python:

python
# less ideal
items_html = "<ul><li>Flask</li><li>Jinja2</li></ul>"

pass structured values:

python
1from flask import Flask, render_template
2
3app = Flask(__name__)
4
5@app.route("/topics")
6def topics():
7    items = ["Flask", "Jinja2", "Escaping"]
8    return render_template("topics.html", items=items)

Template:

jinja2
1<ul>
2  {% for item in items %}
3    <li>{{ item }}</li>
4  {% endfor %}
5</ul>

This is safer, easier to maintain, and keeps presentation logic where it belongs.

When Markup Is Appropriate

Markup is appropriate when your code generates the HTML itself or when you have sanitized the content already. It is not a license to bypass escaping because “the string looks harmless.”

A good rule is:

  • trusted application-generated markup can be wrapped in Markup
  • user-generated markup should be sanitized first
  • plain text should be left to autoescape normally

That rule keeps the default safe behavior intact while still allowing rich content when the data model really needs it.

Common Pitfalls

The biggest mistake is using |safe on raw user input. That turns a formatting problem into a security bug.

Another common problem is building large HTML fragments in Python when the data should have been passed as structured values and rendered in Jinja2.

Developers also sometimes assume that “internal” data is automatically safe. If the content ultimately came from an editor, import job, or database field, it still needs a trust decision.

Finally, make the safe-or-unsafe choice in one place. Mixing Markup in some views and |safe in some templates makes audits harder.

Summary

  • Jinja2 escapes variables by default to prevent cross-site scripting.
  • Use Markup or |safe only for content you trust or have sanitized.
  • Sanitize user-provided HTML before rendering it as markup.
  • Prefer passing structured data to templates instead of assembling HTML in Python.
  • Treat HTML rendering as both a template concern and a security decision.

Course illustration
Course illustration

All Rights Reserved.