Custom Filters & Context Processors
Jinja2's built-in filters from the earlier lesson —
upper, length, truncate, default — cover
general-purpose formatting, but they can't possibly know about anything specific to your
app's own data. Bookshelf needs a few things no built-in filter can express: turning a
book's page progress into a percentage, or showing how many books a user currently has "in
progress" on every single page without querying for it in every single view. Flask covers
both needs — custom filters and context processors — and this lesson covers each, plus how
to tell them apart.
Why You'd Write A Custom Filter
A filter's job is to transform a value that's already in the template into something more presentable. Jinja2 ships plenty of generic ones, but "convert this book's current page and total page count into a reading-progress percentage" is specific enough to Bookshelf that no built-in filter will ever cover it — so you write your own, once, and use it anywhere a template needs that exact transformation.
Registering A Custom Filter
@app.template_filter() turns an ordinary Python function into something
usable with the | syntax inside any template the app renders:
# app.py
@app.template_filter('reading_progress')
def reading_progress(total_pages, current_page):
if not total_pages:
return '0%'
percent = int((current_page / total_pages) * 100)
return f'{percent}%'
The name passed to @app.template_filter() is what templates call it by. Used
in a template — plain Jinja2, no Python highlighting needed since it's just the filter
syntax from the earlier lesson:
<p>Progress: {{ book.pages|reading_progress(book.current_page) }}</p>
The value before the pipe (book.pages) becomes the filter function's first
argument, and anything inside the parentheses after the filter name becomes its remaining
arguments — so reading_progress(total_pages=book.pages, current_page=book.current_page)
runs behind the scenes and its returned string is what actually gets printed.
Making A Variable Available Everywhere With Context Processors
A filter only helps once you already have a value in the template to transform. Sometimes
you need the opposite: a piece of data that has nothing to do with any single view's own
variables, but that every template should be able to reach — like a "books in progress"
badge count sitting in the shared nav bar on every page. Passing reading_count=...
into every single render_template() call across the whole app would work, but
it means remembering to add it everywhere, forever. A context processor
injects a variable into every template's context automatically, from one place:
# app.py
@app.context_processor
def inject_reading_count():
count = Book.query.filter_by(
user_id=current_user.id, status='reading'
).count()
return dict(reading_count=count)
Every key in the dictionary this function returns becomes a variable in every
template rendered anywhere in the app — including base.html's nav bar, with no
view needing to know or care that reading_count exists:
<nav>
<a href="{{ url_for('book_list') }}">My Books</a>
<a href="{{ url_for('shelf_list') }}">Shelves</a>
<span class="badge">{{ reading_count }} in progress</span>
</nav>
Because base.html is the shared shell every page extends, that badge now shows
up on the book list, the shelf page, the add-book form — everywhere — from this one
function.
Filters vs. Context Processors: The Practical Difference
| What it does | |
|---|---|
| Custom filter | Transforms a value the template already has — you still pass the original data in yourself, the filter just reshapes it for display |
| Context processor | Makes a brand-new value available in every template automatically, without any view passing it explicitly |
A quick way to decide which one fits: if the template already has the data and just needs it formatted differently, write a filter. If no view currently has the data available at all, and you don't want to sprinkle the same query into every single view function, write a context processor instead.
A Brief Look At jinja_env.globals
One more mechanism worth knowing exists, even if this track doesn't lean on it heavily:
app.jinja_env.globals lets you make an entire function directly callable
inside any template — not just a filter applied to a value, a genuine function call with
its own parentheses. A classic use is a footer copyright helper:
# app.py
from datetime import datetime
app.jinja_env.globals['current_year'] = lambda: datetime.utcnow().year
<footer>© {{ current_year() }} Bookshelf</footer>
Unlike a context processor, which injects a fixed value computed once per request, a global exposes a callable — the template itself decides when to invoke it and can even pass it arguments. Keep this one filed away as conceptual awareness for now; most projects reach for a context processor or a filter far more often than a raw template global.