Frameworks
Your templates never find out which framework is running them. { render_partial(...) } looks identical in a Flask app from 2019 and a FastAPI app you started this morning. What changes is one registration line, and for the async frameworks, how you render the page that contains the partials. Register exactly once per app, at startup, and the rest of this page is about which line to write.
Flask
import flask
import jinja_partials
app = flask.Flask(__name__)
jinja_partials.register_extensions(app)Flask is the original home of this library, and it gets the richest behavior. This is the only path that renders partials through flask.render_template, so everything you expect in a page is also there in a partial: context processors, the template_rendered signal, g, request, and session. If a partial wants the current user from a context processor, this is the registration that gives it one.
Quart
from quart import Quart
import jinja_partials
app = Quart(__name__)
jinja_partials.register_quart_extensions(app)Quart templates are async, which is where things get interesting (see the page-level rule below and How it works for the full story). Registration adds before_serving and after_serving hooks that create and later shut down a dedicated thread pool for rendering partials. Pages render with await render_template(...) exactly as they always did.
FastAPI
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
import jinja_partials
app = FastAPI()
templates = Jinja2Templates(directory='templates')
jinja_partials.register_fastapi_extensions(app, templates)Registration wraps the app’s lifespan, so the render pool exists for exactly as long as the app is serving and not a moment longer. If you already pass lifespan= to FastAPI, it keeps running, startup and shutdown both, and whatever state it yields still reaches your routes.
Starlette
from starlette.applications import Starlette
from starlette.templating import Jinja2Templates
import jinja_partials
templates = Jinja2Templates(directory='templates')
app = Starlette(routes=[...])
jinja_partials.register_starlette_extensions(templates, app=app)Read that last line twice. templates comes first, and app is a keyword. It is the reverse of the FastAPI call, for the historical reason that Starlette support predates the app-aware version and the signature kept its original first argument. Passing the app is optional but recommended: with it you get a pool tied to the lifespan and a working max_workers; without it, async renders go through a shared module-level pool and max_workers is ignored.
The page-level rule for async environments
Quart, FastAPI, and Starlette apps usually create their Jinja environment with enable_async=True. jinja_partials handles partials in those environments without any template changes. The page that contains them is your job, and there is exactly one rule: render it with await template.render_async(...) and return an HTMLResponse.
from fastapi.responses import HTMLResponse
@app.get('/')
async def index() -> HTMLResponse:
template = templates.get_template('home/index.html')
return HTMLResponse(await template.render_async(items=items))The tempting alternative, templates.TemplateResponse(...), calls the synchronous render(). On an async template that means asyncio.run() being invoked from inside the event loop that is already running your request, and Python declines with RuntimeError: asyncio.run() cannot be called from a running event loop. The partials were never the problem; the page was. On a synchronous environment, TemplateResponse is perfectly fine and partials work the same way.
The FastAPI, Starlette, and Quart examples in the repository all follow this rule, so you can copy a working route rather than remembering it.
Plain Jinja2
No framework? A framework the library has not heard of? Rendering email templates from a worker process? Register with the environment directly:
from jinja2 import Environment, FileSystemLoader
import jinja_partials
env = Environment(loader=FileSystemLoader('templates'), autoescape=True)
jinja_partials.register_environment(env, markup=True)The markup=True is not decoration. register_environment is the one registration function whose markup defaults to False, kept that way for backwards compatibility. With autoescaping on, the default inserts your fragment as escaped text, and you get a page full of visible angle brackets. Pass markup=True and move on with your life.
The declarative extension
PartialsJinjaExtension performs the same registration (always with markup=True) from the environment’s extensions list. It suits frameworks and settings files that configure Jinja declaratively rather than in code:
from jinja2 import Environment, FileSystemLoader
env = Environment(
loader=FileSystemLoader('templates'),
extensions=['jinja_partials.PartialsJinjaExtension'],
)On a Flask app you can add it with app.jinja_env.add_extension('jinja_partials.PartialsJinjaExtension'), and it works, with one subtlety. Partials then render through the Jinja environment rather than flask.render_template, so context processor values and the template_rendered signal are not applied inside them. g, request, and session are still there, because Flask installs those as environment globals rather than through context processors. If your partials read context processor values, use register_extensions(app) and keep the extension for environments that are not Flask.
The same table, at a glance
| Registration | Partials render through | markup |
Flask context processors in partials |
|---|---|---|---|
register_extensions(app) |
flask.render_template |
always True |
yes |
register_quart_extensions(app) |
app’s Jinja environment, lifespan-managed pool | always True |
n/a |
register_fastapi_extensions(app, templates) |
templates.env, lifespan-managed pool |
always True |
n/a |
register_starlette_extensions(templates, app=app) |
templates.env, lifespan-managed pool |
always True |
n/a |
register_starlette_extensions(templates) |
templates.env, shared module pool |
always True |
n/a |
register_environment(env, markup=...) |
env |
defaults to False |
no |
| PartialsJinjaExtension | the environment it is loaded into | always True |
no |
Every row is built on the same primitive, generate_render_partial(renderer), and How it works shows how little code sits behind this table. When one of these paths misbehaves, Troubleshooting names the symptom and the fix.