A filter transforms a value inside a template with the pipe syntax —{{ value | filter }}. A global is a function or value available in every template without passing it through the route's context. Chirp ships a handful of web-specific built-in filters and globals, and lets you register your own with @app.template_filter() and @app.template_global().
Register a custom filter
Decorate a function with@app.template_filter(). The function name becomes the filter name, and the piped value is the first argument:
from chirp import App
app = App()
@app.template_filter()
def currency(value: float) -> str:
return f"${value:,.2f}"
Use it in a template with the pipe:
<span class="price">{{ product.price | currency }}</span>
{# → <span class="price">$1,299.00</span> #}
Extra arguments after the piped value are passed in the parentheses:
@app.template_filter()
def excerpt(value: str, length: int = 50, suffix: str = "...") -> str:
if len(value) <= length:
return value
return value[:length].rsplit(" ", 1)[0] + suffix
<p>{{ post.body | excerpt(120) }}</p>
Filters are ordinary Python functions, so your type annotations give your IDE autocomplete and type-checking on the arguments.
Common patterns
Named filters
By default the function name is the filter name. Pass a string to override it — useful when the Python name and the template name should differ:
@app.template_filter("type_color")
def type_color(type_name: str) -> str:
"""Return the CSS color for a Pokemon type."""
return TYPE_COLORS.get(type_name.lower(), "#777")
@app.template_filter("sprite")
def sprite_url(pokemon_id: int, variant: str = "default") -> str:
"""Return the PokeAPI sprite URL for a Pokemon ID."""
if variant == "artwork":
return f"{SPRITE_BASE}/other/official-artwork/{pokemon_id}.png"
return f"{SPRITE_BASE}/{pokemon_id}.png"
Source: examples/standalone/pokedex/app.py.
<span style="color: {{ pokemon.type | type_color }}">{{ pokemon.type }}</span>
<img src="{{ pokemon.id | sprite('artwork') }}" alt="{{ pokemon.name }}">
Template globals
Globals are functions or values callable from any template without being passed in the route context:
from datetime import datetime
@app.template_global()
def site_name() -> str:
return "My App"
@app.template_global()
def current_year() -> int:
return datetime.now().year
<footer>© {{ current_year() }} {{ site_name() }}</footer>
Like filters, globals take an optional name argument: @app.template_global("year").
Built-in filters
Chirp registers these web-specific filters on every template environment — no import or registration needed. They complement the filters that come from Kida, the template engine.
| Filter | Does | Example |
|---|---|---|
field_errors |
Returns the list of validation messages for one form field, or an empty list when there are none. | {% for m in errors | field_errors("email") %}…{% end %} |
qs |
Builds a query string on a URL path. Falsy values (None, "", 0, False) are dropped. |
{{ '/search' | qs(q=query, page=page) }} → /search?q=hello&page=2 |
attr |
Emits an HTML attribute only when the value is truthy, else nothing. The value is HTML-escaped. | <a href="/x"{{ cls | attr("class") }}> → class="active" when clsis truthy |
url |
Safelists a URL for anhref: returns it when the scheme is safe (http, https, relative), else a fallback (default #). Use on user or external data. |
<a href="{{ link | url(fallback='/') }}"> |
field_errorspairs with the errors dict returned by form validation:
<label>Email</label>
<input name="email" value="{{ form.email ?? "" }}">
{% for msg in errors | field_errors("email") %}
<span class="field-error">{{ msg }}</span>
{% end %}
Island filters (advanced)
For island mount attributes, Chirp also ships theisland_props filter plus the island_attrs and primitive_attrsglobals. Reach for these only when mounting a client-side island.
island_props serializes a value to HTML-escaped JSON for a data-island-propsattribute:
<div data-island="editor" data-island-props="{{ state | island_props }}">
Fallback editor UI.
</div>
The island_attrsglobal builds the full mount attribute string in one call:
<div{{ island_attrs("editor", props=state, mount_id="editor-root") }}>
Fallback editor UI.
</div>
Use primitive_attrswhen the mount needs stricter primitive metadata:
<div{{ primitive_attrs("grid_state", props={"stateKey": "team", "columns": ["name", "role"]}) }}>
...
</div>
Escaping and thesafefilter
WhenAppConfig(autoescape=True) (the default), {{ x }} is HTML-escaped automatically — the main defense against XSS. The escaping filters come from Kida: use | e (or | escape) to escape explicitly when chaining filters that might drop escaping, and | safe(reason="...")to mark output as trusted HTML so it is not escaped.
To render markdown, register themarkdown filter once during setup. It sanitizes unsafe HTML and URLs by default; pass sanitize=Falseonly for fully trusted markdown:
from chirp.markdown import register_markdown_filter
register_markdown_filter(app) # adds the `markdown` filter
{{ post.body | markdown }}
Next steps
- Rendering — how a template is rendered into a response
- App lifecycle — when filters and globals are frozen into the environment
- API reference — the complete API surface