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.

Note: These are correct, working Flask and Jinja2 examples, but this website's own project doesn't run a Flask server, so nothing here executes live on the page — read each one alongside the explanation, then try it in your own project.

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 filterTransforms a value the template already has — you still pass the original data in yourself, the filter just reshapes it for display
Context processorMakes 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.

Next up: Templates so far have only dealt with data and markup — Static Files & Assets covers serving Bookshelf's CSS, JavaScript, and book cover images alongside these templates.