Create a base template with Jinja2
Learn how to create a base template with Jinja2 in this hands-on Python web development tutorial. Build reusable layouts, reduce duplication, and prepare for more advanced templating techniques.
Focus: create a base template with jinja2
You’ve probably copied the same navigation bar, footer, and CSS links into every HTML file in your project — and you’re already dreading the next update. That duplication isn’t just annoying; it’s a maintenance trap where one typo in your <head> becomes a debugging session across ten pages. It’s time to create a base template with Jinja2, the pattern that lets you define that shared layout once and inherit it everywhere.
The problem this lesson solves
Imagine your Flask or FastAPI app has grown to a handful of routes: /, /about, /dashboard. Each one renders its own HTML file. You’ve got three separate copies of the same header, footer, and even the same JavaScript bundle tag. When you decide to change your site’s branding, you have to edit every single file — and miss one, and suddenly your site looks inconsistent.
That’s the core problem: duplication in templates. It violates the DRY principle, wastes time, and increases the chance of bugs. The solution is template inheritance — a base layout that contains the common skeleton, with child templates that fill in only the parts that change. This is a fundamental skill in Python web development, and in this lesson you’ll learn exactly how to create a base template with Jinja2.
Core concept / mental model
Think of your base template as a blueprint for your site. It defines the overall structure — the <html>, <head>, navigation, footer, and any global CSS/JS includes — but leaves gaps (called blocks) where each page can inject its own content.
A child template then says: “I’m based on that blueprint, and I’ll fill in the content block with my own HTML.” The Jinja2 engine merges them at render time, producing a complete page.
Key vocabulary:
- Base template – the parent layout with common markup.
- Child template – extends the base and overrides blocks.
- Block – a named placeholder, defined with
{% block name %}…{% endblock %}. extends– the tag that links a child to its base.
This is similar to inheritance in object-oriented programming: the base class defines shared methods, and subclasses override specific ones.
How it works step by step
Step 1: Create the base template
In your project, create a file called base.html (usually in a templates/ folder). Inside, write the standard HTML skeleton and mark the spots that will vary from page to page.
Step 2: Define blocks
The most common block is content, but you’ll also want a title block so each page can set its own browser tab name. You might also define a head block for page-specific meta tags or scripts.
Step 3: Extend from child templates
In each page template — e.g., about.html — start with {% extends "base.html" %}. Then override the blocks you need using {% block content %} … {% endblock %}.
Step 4: Render with Flask or FastAPI
When your route calls render_template("about.html"), Jinja2 finds the child, sees the extends tag, loads the base, and substitutes the block contents.
Hands-on walkthrough
Let’s create a minimal but complete example you can run right now.
First, set up a small Flask app to test our templates:
# app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("home.html", user="Ada")
@app.route("/about")
def about():
return render_template("about.html")
if __name__ == "__main__":
app.run(debug=True)
Now create the base template under templates/base.html:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2025 My Site</p>
</footer>
</body>
</html>
Now create two child templates:
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home — My Site{% endblock %}
{% block content %}
<h1>Welcome, {{ user }}!</h1>
<p>This is the home page.</p>
{% endblock %}
<!-- templates/about.html -->
{% extends "base.html" %}
{% block title %}About — My Site{% endblock %}
{% block content %}
<h1>About Us</h1>
<p>We build awesome things.</p>
{% endblock %}
Run python app.py and visit http://127.0.0.1:5000/. You’ll get a full page with the navigation, footer, and the home content injected. The browser tab will show “Home — My Site”.
Pro tip: Always use
url_forfor URLs inside templates — it stays correct even if you change route paths later.
Adding structure with {% block %}, {% extends %}, {% include %}
Aside from inheritance, Jinja2 offers {% include %} to embed one template inside another. This is useful for truly reusable components like a sidebar without a full inheritance chain.
Compare options / when to choose what
| Approach | When to use | Pros | Cons |
|---|---|---|---|
Template inheritance (extends) |
Most pages share a common layout | DRY, central control | Overriding can get complex for deep nesting |
{% include %} |
Need a reusable snippet in multiple unrelated pages | Simple, good for small components | Can lead to duplication if overused |
Macros ({% macro %}) |
Reusable functions to generate HTML | Powerful for generated markup | Overkill for simple layouts |
When to choose what: Start with inheritance for the overall layout. Use includes for headers/sidebars that appear in some but not all pages. Use macros when you need to render repeated HTML with dynamic data (like form fields or pagination).
Troubleshooting & edge cases
1. TemplateNotFound: base.html
Cause: Your child template references base.html but Jinja2 can’t find it. This usually happens because the file isn’t in the templates directory or the path is wrong.
Fix: Ensure base.html is in the same directory as your child templates, and use the relative path (e.g., {% extends "base.html" %}) without a leading slash.
2. “Block not overridden” or empty block
Cause: You defined a block in the base but didn’t fill it in the child, and you didn’t use {% super %}. The block renders nothing (or base content if you added default content).
Fix: Add default content inside the block in the base: {% block content %}Default content{% endblock %}, or make sure every needed block is overridden in the child.
3. Content appears outside the intended area
Cause: You often forget to add the {% block %} tags around your content in the child, so the child’s top-level HTML gets placed outside the base’s layout.
Fix: Always wrap child-specific HTML inside {% block ... %} and end with {% endblock %}.
4. Static files not loading
Cause: You hardcoded paths instead of using url_for.
Fix: Use {{ url_for('static', filename='...') }} — Flask will serve them correctly from your static folder.
What you learned & what's next
You now know how to create a base template with Jinja2 — you’ve built a reusable layout, used {% extends %} and {% block %} to inject content, and explored when to use inheritance versus includes or macros. This eliminates duplication, makes global changes a one-edit operation, and scales to any page count.
Next lesson: You’ll learn how to pass dynamic data from your Python routes to these templates using variables and control structures — turning static layouts into interactive views.
Now, try this: refactor your own multipage Flask project into a base template + child templates. Start by identifying common elements (nav, footer) and move them into base.html. Then watch your maintenance effort shrink.
Practice recap
Create a two-page Flask app (home and about) with a base template containing a shared nav and footer. Override the title and content blocks in each child template. Then add a page_class block in the base but only override it on the home page — observe how the class appears only there. This solidifies your understanding of block overriding and template flexibility.
Common mistakes
- Forgetting the
{% endblock %}tag, which raises a template syntax error. - Hardcoding URLs like
/aboutinstead of usingurl_for, breaking links when routes change. - Using a single base template for absolutely everything, forcing complex block overrides instead of splitting layouts.
- Putting CSS or JS in child templates instead of in the base, duplicating includes.
Variations
- For FastAPI, use
Jinja2Templatesfrom Starlette — same syntax, but you set up the templates folder differently. - You can use multiple base templates (e.g., a public base and an admin base) to handle different layout needs.
- Use
{% block body %}with custom classes in a child block when you need page-specific body styles.
Real-world use cases
- A SaaS dashboard where every authenticated page needs the same sidebar and topbar; only the core content changes.
- A corporate website with many pages that share a consistent header/footer and SEO meta tags, with each page setting its own title.
- An e-commerce store where product listing and detail pages extend a common layout but add their own scripts (like a product image gallery).
Key takeaways
- Template inheritance with
{% extends %}and{% block %}removes duplication across pages. - Use
{% block title %}to let each page define its own<title>for SEO and user experience. - Always reference assets with
url_forto keep links working when you change routes or static paths. - Choose inheritance for layouts, includes for reusable snippets, and macros for dynamic HTML generation.
- A single edit in the base template updates every page that extends it, drastically cutting maintenance cost.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.