Flask’s render_template function renders a Jinja2 template with a context (the variables passed to the template). The right answer is to use template inheritance for the layout ({% extends "base.html" %}), the context for the data (render_template('user.html', user=current_user)), and the autoescape for safety (Jinja2 autoescapes HTML by default in Flask). The mistake every team makes: the team disables autoescape, the team’s template renders user-supplied data as HTML, the team’s site has an XSS hole.
Table of contents
- The Jinja2 syntax — variables, control structures, filters
- The context — the variables passed to the template
- The template inheritance — the layout pattern
- The autoescape — the XSS prevention
- The includes — the partial pattern
- The macros — the function pattern
- The filters — the built-in transformations
- The one mistake that breaks the production server
- How this fits the rest of the stack
- FAQ
The Jinja2 syntax — variables, control structures, filters
Jinja2 has three syntax elements: variables ({{ user.name }}), control structures ({% if user %} … {% endif %}), and filters ({{ user.name | upper }}). The right answer is to use the right element for the right job: variables for output, control structures for logic, filters for transformation. The wrong answer is to use a control structure for a value transformation — the team should use a filter or do the transformation in the view function.
The context — the variables passed to the template
The context is the dict passed to render_template. The right answer is to pass the exact variables the template needs, not a giant g object. The right answer is to name the variables in the context to match the template’s usage (render_template('user.html', user=current_user) so the template can use {{ user.name }}). The wrong answer is to pass locals() or request — the template is coupled to the view, the team loses the abstraction.
The template inheritance — the layout pattern
The right answer for the layout is template inheritance. The base template (base.html) defines the blocks ({% block content %}{% endblock %}), the child template extends the base ({% extends "base.html" %}) and overrides the blocks. The right answer is one base template per layout, multiple child templates per page. The wrong answer is to copy the layout into every template — the team has 30 templates with the same <head> block, the team changes the nav and misses 29 templates.
The autoescape — the XSS prevention
Jinja2 autoescapes HTML in Flask templates by default. The {{ user_input }} is escaped to <script>...</script>, the browser does not execute the script. The right answer is to keep autoescape on, always. The wrong answer is to disable autoescape ({% autoescape false %}) — the team has full control over the output, but the team is responsible for escaping user input, and the team will miss one.
The right answer for trusted HTML is to use the |safe filter ({{ trusted_html | safe }}). The right answer is to mark only the specific variable as safe, not the whole template.
The includes — the partial pattern
The right answer for a partial is {% include "_nav.html" %}. The partial is rendered with the current context, the partial is reusable across templates. The right answer is includes for a partial that is the same across pages (a nav, a footer, a sidebar). The wrong answer is includes for a partial that varies per page — the team should pass the partial in the context.
The macros — the function pattern
The right answer for a reusable template fragment with arguments is a macro. The macro is defined with {% macro field(name) %}...{% endmacro %}, the macro is called with {{ field('email') }}. The right answer is macros for a form field, a UI component, anything that is repeated with different inputs.
The filters — the built-in transformations
Jinja2 has built-in filters: |upper, |lower, |title, |trim, |length, |default(value), |join(separator), |tojson, and more. The right answer is to use the built-in filter instead of writing the transformation in the view. The right answer for a list of items joined with a comma is {{ items | join(', ') }}.
The one mistake that breaks the production server
The mistake: the team’s template has an unhandled exception (a None value where the template expects a string, a missing variable, a syntax error in the template). The production server returns a 500, the team’s pages are down, the team’s on-call gets paged. The right answer is to validate the context before passing it to the template, to handle the None case in the template ({{ user.name | default('Anonymous') }}), and to add error handlers for the production case.
How this fits the rest of the stack
The infrastructure question is a small piece of a larger pattern: the team’s runtime, storage, database, secret store, logs, and deployment platform are all parts of the same platform. The right answer is to model the full stack before the project ships, not after. The RunxBuild hosting calculator is the right place to do that exercise — pick the runtime, the memory tier, the storage, the secret store, and the egress, and the calculator shows what the deploy actually costs at the team’s actual usage.
Useful related references:
FAQ
What is Flask’s render_template?
A function that renders a Jinja2 template with a context. The result is an HTML string that the view returns.
How do I pass variables to a Flask template?
Use the context dict: render_template('user.html', user=current_user, posts=posts). The right answer is to pass the exact variables the template needs.
How do I extend a base template in Jinja2?
{% extends "base.html" %} at the top of the child template, then {% block content %}...{% endblock %} to override the blocks.
How do I include a partial in Jinja2?
{% include "_nav.html" %} renders the partial with the current context.
How do I define a macro in Jinja2?
{% macro field(name) %}...{% endmacro %}. Call with {{ field('email') }}.
Does Jinja2 autoescape by default in Flask?
Yes. The {{ user_input }} is escaped to safe HTML. The right answer is to keep autoescape on and use |safe for trusted HTML.
How do I handle a None value in a Jinja2 template?
Use the default filter: {{ user.name | default('Anonymous') }}.
What is the difference between include and extends?
Include renders a partial with the current context. Extends inherits the base template’s blocks, the child overrides the blocks. Use include for a partial, extends for a layout.