PyBladePyBlade

PyBlade Directives

Learn how to add logic, conditions, loops, includes, and other template behaviors with PyBlade directives.

So far, you've learned how to bring data into your templates and display it with PyBlade. But templates don't just display data—they can also use it to control what gets rendered.

With PyBlade Directives, you can add conditions, repeat content, include other templates, handle empty states, and structure your markup with simple, expressive syntax.

In this section, you'll discover the directives PyBlade provides and learn how to use them to build more dynamic and maintainable templates.

Conditional directives

PyBlade provides directives for conditionally rendering parts of a template. The most common ones are @if, @elif, and @else.

These directives follow a familiar structure, while giving you a template-oriented syntax for deciding which content should be rendered.

@if, @elif, and @else

Use @if to conditionally render a block of markup. You can optionally add one or more @elif branches and an @else fallback.

For example, suppose in your template context you have passed a variable status that can hold values like 'active', 'pending', or 'inactive'. You can display different messages based on the value of status using conditional directives:

@if (status == 'active')
    <p>Your account is active.</p>
@elif (status == 'pending')
    <p>Your account is pending approval.</p>
@else
    <p>Your account is inactive.</p>
@endif

The expression passed to the directive is evaluated by PyBlade, and only the matching block is rendered.

You can use the same expressions supported by python, including comparisons and logical operators:

@if (user.is_authenticated and user.is_admin)
    <p>Welcome, Admin {{ user.name }}!</p>
@endif

@if can be used on its own, or combined with @elif and @else. Always close the conditional block with @endif.

@unless

@unless provides a convenient way to render content when a condition is not satisfied. It is essentially the inverse form of @if.

@unless (user.is_admin)
    <p>You do not have admin privileges.</p>
@endunless

Like @if, @unless must be closed with its corresponding @endunless directive.

You can put one condition inside another

When one decision depends on another, you can place an @if block inside another conditional block. Each block still needs its own closing directive. Keep nesting shallow when you can, so the template stays easy to read.

@if (user.is_authenticated)
    @if (user.is_admin)
        <p>Welcome, Admin {{ user.name }}!</p>
    @else
        <p>Welcome, {{ user.name }}!</p>
    @endif
@else
    <p>Please log in to continue.</p>
@endif

Nested directives are processed independently, allowing you to compose more detailed rendering conditions directly in your template.

Authentication directives

For authentication-related conditions, PyBlade provides dedicated directives that are more expressive than checking authentication state manually.

The @auth directive renders its content for authenticated users:

@auth
    <p>Welcome back, {{ user.name }}!</p>
    <a href="/dashboard">Go to Dashboard</a>
@else
    <p>You must log in to access this section.</p>
    <a href="/login">Log in here</a>
@endauth

The @guest directive does the opposite: it renders its content when the current user is not authenticated.

@guest
    <p>Welcome! Please <a href="/login">log in</a> or <a href="/register">sign up</a>.</p>
@else
    <p>Hello {{ user.name }}, you are already logged in.</p>
@endguest

PyBlade also provides @anonymous as an alias for @guest.

Use @auth, @guest, or @anonymous when working with authentication state. These directives make the intent of your template immediately clear without requiring an explicit authentication check.

Match directives

When several template branches depend on the value of the same expression, PyBlade provides the @match directive.

A @match block contains one or more @case directives and can optionally include a @default fallback.

Pass the expression you want to evaluate to @match, then define the possible values with @case.

@match(status)
    @case('active')
        <p>Your account is active.</p>
    @case('pending')
        <p>Your account is pending approval.</p>
    @case('inactive')
        <p>Your account is inactive.</p>
    @default
        <p>Status unknown.</p>
@endmatch

PyBlade evaluates status and renders the content of the matching @case. If no case matches, the @default block is rendered.

The @default directive is optional:

@match(status)
    @case('active')
        <span>Active</span>
    @case('pending')
        <span>Pending</span>
@endmatch

If status does not match either case, nothing is rendered.

PyBlade also provides @switch as an alias for @match. Both directives behave identically, so you can use whichever syntax is more natural for your project.

@switch(status)
    @case('active')
        <p>Your account is active.</p>
    @case('pending')
        <p>Your account is pending approval.</p>
    @case('inactive')
        <p>Your account is inactive.</p>
    @default
        <p>Status unknown.</p>
@endswitch

The body of an @case currently does not support other PyBlade directives. This also means that @match blocks cannot be nested inside a @case.

Loops

PyBlade supports looping through lists, dictionaries, and other iterable data structures using the @for directive. These loops are similar to Python’s for loops and make it easy to display repeated elements in your templates.

@for loop example

If you have a list of items, you can use @for to loop through them:

views.py
from django.shortcuts import render

def show_fruits(request):
    context = {'fruits': ['Apple', 'Banana', 'Cherry']}
    return render(request, 'fruits', context)
fruits.html
<ul>
    @for (fruit in fruits)
        <li>{{ fruit }}</li>
    @endfor
</ul>

Output

<ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Cherry</li>
</ul>

The @empty directive

Sometimes, a list or other iterable may contain no items. In that case, your loop has nothing to display.

PyBlade provides the @empty directive for exactly this situation. It lets you define what should be displayed when a @for loop has no items to iterate over.

fruits.html
<ul>
    @for (fruit in fruits)
        <li>{{ fruit }}</li>
    @empty
        <li>No fruits available.</li>
    @endfor
</ul>

If fruits contains items, PyBlade renders the loop normally:

<ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Orange</li>
</ul>

But if fruits is empty, the @empty block is rendered instead:

<ul>
    <li>No fruits available.</li>
</ul>

Think of @empty as a fallback for your loop: "Show these items if there are any; otherwise, show this message."

This is especially useful for lists such as search results, notifications, products, or users, where an empty list is a normal situation that should still give the user useful feedback.

The loop variable

When you use a @for loop, PyBlade gives you a special loop variable. It contains useful information about the loop you are currently inside.

For example, you can use it to know whether you are displaying the first or last item:

@for (user in users)
    @if (loop.first)
        This is the first user.
    @endif

    @if (loop.last)
        This is the last user.
    @endif

    <p>This is user {{ user.name }}.</p>
@endfor

You can also use loop to get the current position, count the remaining items, or determine whether the current iteration is even or odd.

Think of loop as a small helper that follows you through the loop and tells you where you are.

For example, loop.index starts at 0, while loop.iteration starts at 1:

@for (user in users)
    <p>
        {{ loop.iteration }}. {{ user.name }}
    </p>
@endfor

If users contains three users, this produces:

<p>1. Alice</p>
<p>2. Bob</p>
<p>3. Charlie</p>

Nested loops

Loops can also be nested. When you are inside a nested loop, the loop variable refers to the innermost loop.

To access the loop variable of the parent loop, use loop.parent:

@for (user in users)
    @for (comment in user.comments)
        @if (loop.parent.first)
            This user is the first user in the list.
        @endif

        <p>{{ comment.text }}</p>
    @endfor
@endfor

Here, loop refers to the comment loop, while loop.parent refers to the user loop.

Available properties

PyBlade provides the following properties on the loop variable:

PropertyReturn typeDescription
loop.indexintThe index of the current iteration, starting at 0.
loop.iterationintThe current iteration number, starting at 1.
loop.firstboolTrue when this is the first iteration.
loop.lastboolTrue when this is the last iteration.
loop.countintThe total number of items in the loop.
loop.remainingintThe number of items remaining after the current iteration.
loop.evenboolTrue when the current iteration is even.
loop.oddboolTrue when the current iteration is odd.
loop.depthintThe nesting depth of the current loop, starting at 0.
loop.parentLoopContextThe loop variable of the parent loop when inside a nested loop.

The loop variable is available only inside a @for loop. Once the loop ends, the variable is no longer available.

Skipping and ending loop iterations

Sometimes, you don't want to process every item in a loop.

PyBlade provides two directives for controlling a loop:

  • @continue skips the current iteration and moves to the next item.
  • @break stops the loop completely.

Let's see how they work with a simple example.

Suppose you have a list of fruits, but you don't want to display Banana. You also want to stop displaying fruits once you reach Date.

views.py
from django.shortcuts import render

def show_fruits(request):
    context = {
        'fruits': ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']
    }

    return render(request, 'fruits.html', context)

In your template, you can use @continue to skip Banana and @break to stop the loop when you reach Date:

fruits.html
<ul>
    @for (fruit in fruits)

        @if (fruit == 'Banana')
            @continue
        @endif

        <li>{{ fruit }}</li>

        @if (fruit == 'Date')
            @break
        @endif

    @endfor
</ul>

The result is:

<ul>
    <li>Apple</li>
    <li>Cherry</li>
    <li>Date</li>
</ul>

Here is what happens step by step:

  • Apple is displayed normally.
  • Banana matches the condition, so @continue skips the rest of that iteration. The <li> for Banana is never rendered.
  • Cherry is displayed normally.
  • Date is displayed, and then @break stops the loop.

Because the loop has ended, Elderberry is never processed.

When your @continue or @break only needs a simple condition, you can put the condition directly inside the directive. This avoids writing a separate @if block.

The previous example can therefore be shortened to:

fruits.html
<ul>
    @for (fruit in fruits)

        @continue(fruit == 'Banana')

        <li>{{ fruit }}</li>

        @break(fruit == 'Date')

    @endfor
</ul>

@continue and @break can only be used inside a @for loop. Using either directive outside of a loop will result in an error.

Including partials

As your templates grow, you may find yourself writing the same pieces of HTML in several places. A header, footer, navigation menu, or small section can be moved into its own template file and included wherever you need it.

PyBlade calls these smaller reusable template files partials.

Use the @include directive to insert a partial into your template:

index.html
<!DOCTYPE html>
<html>
<head>
    @include("header")
</head>
<body>
    <main class="content">
        The main content goes here!
    </main>

    @include("footer")
</body>
</html>

The path to the partial is written as a string, without the .html extension. Directories are separated using dot notation:

@include("shared.header")
@include("partials.navigation")

Partials are useful when you want to reuse a piece of a template without repeating its HTML in every file.

Partials and components are not the same thing. Partials are simply template sections that you include where needed. We'll look at components later and see how they provide a more structured way to build reusable pieces of your UI.

Conditional classes with @class

The @class directive lets you conditionally apply CSS classes to an HTML element.

It accepts positional and keyword-like arguments. Positional arguments are always included in the class list, while keyword arguments are included only if their condition evaluate to True.

Syntax

@class("class1 class2", "class3", "class4 class5"=boolean_expression)

Behavior

  • Each positional argument is evaluated and added as a class if it has a truthy value (not None, False or an empty string)
  • Each keyword argument maps:
    • key as class name
    • value as condition
  • Duplicate class names are automatically removed

Example

In this example, we apply the list-item class to each item and favorite class to items marked as a favorite:

views.py
from django.shortcuts import render

def show_fruits(request):
    context = {
        'fruits': [
            {'name': 'Apple', 'is_favorite': True},
            {'name': 'Banana', 'is_favorite': False},
            {'name': 'Cherry', 'is_favorite': True},
        ]
    }
    return render(request, 'fruits', context)
fruits.html
<ul>
    @for (fruit in fruits)
        <li @class("list-item", "favorite"=fruit.is_favorite)>{{ fruit.name }}</li>
    @endfor
</ul>

Output

<ul>
    <li class="list-item favorite">Apple</li>
    <li class="list-item">Banana</li>
    <li class="list-item favorite">Cherry</li>
</ul>

Conditional inline styles with @style

The @style directive works similarly to @class, but instead of controlling CSS class names, it works with CSS properties.

Example

Here, we set a red color for fruits that are not favorites:

<ul>
    @for (fruit in fruits)
        <li @style("background-color:#000", "color: red"=not fruit.is_favorite)>{{ fruit.name }}</li>
    @endfor
</ul>

Output

<ul>
    <li style="backgroud-color:#000>Apple</li>
    <li style="backgroud-color:#000; color: red">Banana</li>
    <lis tyle="backgroud-color:#000>Cherry</li>
</ul>

Building forms

Forms are a common part of almost every web application, but keeping them clean and user-friendly can require a lot of small details.

PyBlade provides a few directives that make working with forms simpler. They help you connect your HTML form to the data and validation handled by your backend, while keeping the template easy to read.

Let's see how these directives can make a form easier to build and maintain.

Protecting submissions with @csrf

When a form sends data that changes something on the server, your backend may require a CSRF token to protect the request.

PyBlade provides the @csrf directive to add this token to your form:

subscribe.html
<form action="/subscribe" method="POST">

    @csrf

    <label>
        Email
        <input type="email" name="email">
    </label>

    <button type="submit">Subscribe</button>

</form>

When PyBlade renders the template, @csrf becomes the hidden token expected by your backend. You don't need to create the hidden input yourself.

The exact token name and value depend on the framework you are using. PyBlade handles this through its framework integration.

Keep the token with the form

Use @csrf for state-changing form submissions whenever your framework expects CSRF protection. A missing or invalid token should be rejected by the server.

Sometimes you may want to send CSRF token with JavaScript, but browsers do not automatically add a CSRF token to fetch() requests.

If your application sends JSON or another request with JavaScript, expose the token in the page and send it in the header your framework expects.

<meta name="csrf-token" content="{{ csrf_token }}">
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/submit-data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': csrfToken,
    },
    body: JSON.stringify({ message: 'Hello' }),
});

Check your framework's documentation for the precise header name; X-CSRFToken is a common Django convention.

Conditional input attributes

HTML has many attributes that are only needed in certain situations. A checkbox might need checked, a button might need disabled, or an input might need required.

You could use an @if block to decide whether to render each attribute, but that quickly becomes cumbersome. PyBlade provides attribute directives that let you put the condition directly on the element.

For example, @checked adds the checked attribute when its condition is true:

<input
    type="checkbox"
    name="newsletter"
    value="yes"
    @checked(preferences.newsletter)
>

If preferences.newsletter is true, PyBlade renders:

<input
    type="checkbox"
    name="newsletter"
    value="yes"
    checked
>

When the condition is False, the checked attribute is omitted.

The same idea works with other attributes. For example, @selected can be used to select the appropriate <option>:

<select name="version">
    @for (version in product.versions)
        <option
            value="{{ version }}"
            @selected(preferences.version == version)
        >
            v{{ version }}
        </option>
    @endfor
</select>

Here, PyBlade evaluates the condition for each option and adds selected to the matching one.

You can use the same pattern with other common boolean HTML attributes:

<button type="submit" @disabled(form.errors)>
    Save
</button>

<input
    type="email"
    name="email"
    @readonly(user.is_not_admin())
>

<input
    type="text"
    name="title"
    @required(user.is_admin())
>

<input
    type="text"
    name="search"
    @autofocus(is_search_page)
>

<select
    name="labels"
    @multiple(user.can_choose_many)
></select>

Each directive corresponds to the HTML attribute with the same name:

Pyblade DirectiveHTML Attribute
@checkedchecked
@selectedselected
@disableddisabled
@readonlyreadonly
@requiredrequired
@autofocusautofocus
@multiplemultiple

You can also use these directives without a condition when the attribute should always be present:

<input type="text" name="username" @disabled>

PyBlade renders it as:

<input type="text" name="username" disabled>

Browser attributes are not server-side validation

Attributes such as @required and @disabled affect the browser's behavior, but they are not security mechanisms. Always validate submitted data and enforce permissions in your Python application.

Rendering a framework form field with @field

Some frameworks provide form objects that can render their fields as HTML. These fields often come with sensible default attributes, but you may still want to customize the resulting HTML from your template.

This is where PyBlade's @field directive comes in.

Pass the framework field to @field, then provide any additional HTML attributes you need:

<form method="POST">
    @csrf

    @field(form.name, class="form-control" placeholder="Your name" required)
    @field(form.email, class="form-control" placeholder="you@example.com")
</form>

PyBlade keeps the HTML generated by the framework and adds your custom attributes to it.

For example, a Django CharField named name might normally produce:

<input type="text" name="name" id="id_name">

With @field, you can customize it directly:

@field(
    form.name,
    class="form-control"
    placeholder="Your name"
    required
)

Which produces:

<input
    type="text"
    name="name"
    id="id_name"
    class="form-control"
    placeholder="Your name"
    required
>

Attributes passed to @field take priority over attributes already defined by the framework.

This gives you the best of both worlds: the framework can generate the field, while your template remains in control of its HTML attributes.

Showing field errors with @error

When a form is submitted with invalid data, you usually want to show the user what went wrong.

The @error directive lets you display a field's validation error:

@field(form.email, class="form-control")

@error(form.email)
    <small class="text-red-500">{{ message }}</small>
@enderror

If the field has an error, PyBlade renders the @error block and makes the error message available through the message variable.

If there is no error, nothing is rendered.

For example, if the email field is required but the user leaves it empty:

<input
    type="email"
    name="email"
    id="id_email"
    class="form-control"
>

<small class="text-red-500">
    This field is required.
</small>

This lets you keep the field and its validation feedback together without having to manually check whether an error exists.

Working with values

PyBlade provides some useful directives for handling variables dynamically within templates. These directives allow efficient data manipulation without writing additional logic in the backend. Let’s go through them one by one.

The @with directive

The @with directive allows you to assign a temporary variable within a block. This is useful when a value needs to be referenced multiple times within a small section of the template.

Basic usage

@with(variable=value)
    ...
    {{ variable }}
@endwith

Let's take a meaningful example: imagine a web application where customers have profiles with first_name and last_name, instead of concatenating them every time you want to display the customer's full name, @with can be used to assign a full_name variable to be used throughout the block.

@with(full_name=customer.first_name + ' ' + customer.last_name)
    <h3>Welcome again, {{ full_name }} !</h3>

    <p>Hey {{ full_name }}, we have a surprise for you.</p>
@endwith

Another use-case way of the @with directive is to store the result of a complex expression in a simpler variable. This is particularly helpful when dealing with operations that require significant processing, such as querying the database multiple times.

For example:

@with(total=business.employees.count)
    <p>Number of employees : {{ total }}</p>
@endwith

Note

The assigned variables (like total in the example) are only accessible within the @with .... @endwith block and will not be available outside of it.

You can also define multiple variables at once:

@with(alpha=1, beta=2)
    ...
@endwith

The @ratio directive

The @ratio directive is useful for generating proportional values based on a given scale. This is commonly seen in progress indicators, performance metrics, or any scenario where values need to be mapped to a percentage or a fixed range.

Consider a dashboard that displays the progress of a software project based on completed tasks. If there are 40 completed tasks out of 120 total, and we want to scale this to 100 for a percentage value, @ratio makes it easy:

<p>Project Completion: @ratio(40, 120, 100) %</p>

PyBlade will calculate (40 ÷ 120) × 100, which results in 33 %, dynamically adjusting the percentage based on the provided values.

Sometimes, you may need to store the calculated value in a variable for later use. This can be helpful in translated text or other contexts where you need to reference the value multiple times:

@ratio(this_value, max_value, max_width as width)
    <img src="bar.png" alt="Bar" height="10" width="{{ width }}">
@endratio

In this example, @ratio is used to calculate the width of a progress bar based on the provided values. It stores the calculated value in the width variable for later use.

If this_value is 175, max_value is 200, and max_width is 100, the resulting width will be 88 pixels. This is because 175 ÷ 200 = 0.875 and 0.875 × 100 gives 87.5, which rounds up to 88.

So, the @ratio directive calculates the proportion of a given value relative to a maximum value and scales it according to a fixed constant.

Tip

For those accustomed to Django's syntax, PyBlade provides the @widthratio directive which can be used as a convenient alias for the @ratio directive, performing the same function.


The @cycle directive

When displaying elements in a repeating structure, alternating between values can improve readability and usability.

The @cycle directive allows you to alternate between a set of values each time it is encountered. It cycles through the given arguments one by one, and when it reaches the end, it starts over from the first argument.

This is particularly useful inside loops.

A common use case is in a table, where each row should have alternating background colors to enhance readability.

<table>
    @for (book in books)
        <tr class="@cycle('bg-gray-100', 'bg-white')">
            <td>{{ book.title }}</td>
            <td>{{ book.author }}</td>
        </tr>
    @endfor
</table>

In this example, each row will alternate between bg-gray-100 and bg-white, so, the first row gets the class bg-gray-100, the second gets bg-white, the third gets bg-gray-100 again, and so on.

Using variables in @cycle

You can also cycle through variables instead of fixed values. Suppose you have two CSS classes stored in variables:

@with(row1='highlight', row2='normal')
    @for (product in products)
        <tr class="@cycle(row1, row2)">
            <td>{{ product.name }}</td>
            <td>{{ product.price }}</td>
        </tr>
    @endfor
@endwith

You’re not limited to just variables or just strings — you can mix them as well:

@for (product in products)
    <tr class="@cycle('first', row1, 'last')">
        <td>{{ product.name }}</td>
        <td>{{ product.price }}</td>
    </tr>
@endfor

This will alternate between "first", the value of row2, and "last".

Good to know

Values in the @cycle will be automatically escaped for safety. If you want to disable escaping for certain reasons, you can use the .safe filter.

@cycle(var1, var2.safe, var3)

Storing and reusing a Cycle

If you need to reference the current value of a cycle without advancing it, you can assign it a name using the as keyword:

@cycle('red', 'blue' as color)
<p style="color: @cycle(color)">This text alternates between red and blue.</p>

Later in the template, you can use @cycle(color) to advance to the next value:

<p style="color: @cycle(color)">This text will continue cycling through the same colors.</p>

Using silent to define a Cycle without output

When using the @cycle directive with the as keyword, the cycle automatically produces the first value from the list of provided values. However, this behavior might not always be desirable, especially if you want to store the cycle's value for later use but don’t want it to be output immediately. That's where the silent keywork comes into play.

This keyword prevents the cycle from displaying its value at the point where it's declared, but still allows you to use the cycle in subsequent code.

For example, if you're using the cycle within a loop and want to store the current value in a variable without outputting the first value right away, you can add the silent keyword.

@cycle('row1', 'row2' as rowcolors silent) 
@for (item in products)
    <tr class="@cycle(rowcolors)">
        <td>{{ item.name }}</td>
        <td>{{ item.price }}</td>
    </tr>
@endfor

In this example, the first cycle value ('row1') is not output immediately up to the <tr> tag. Instead, it is stored in the rowcolors variable, which is then used in the <tr> tag. The included subtemplate will also have access to the rowcolors variable in its context and the value will match the current cycle value.

For the above code, the output would look something like this:

<tr class="row1"> 
    <td>Product 1</td>
    <td>$10</td>
</tr>
<tr class="row2"> 
    <td>Product 2</td>
    <td>$20</td>
</tr>
<tr class="row1"> 
    <td>Product 3</td>
    <td>$30</td>
</tr>

But without the silent keyword, row1 would be output right away as normal text before the loop, resulting in something like this:

row1 
<tr class="row2">
    <td>Product 1</td>
    <td>$10</td>
</tr>
<tr class="row1"> 
    <td>Product 2</td>
    <td>$20</td>
</tr>
<tr class="row2">
    <td>Product 3</td>
    <td>$30</td>
</tr>

Restarting a Cycle with @resetcycle

If needed, you can reset a cycle so that it starts from the first value again the next time it is used:

@cycle('section-a', 'section-b' as section_class)

@for (section in sections)
    @if (loop.last)
        @resetcycle(section_class)  <!-- Reset cycle to start from the first value -->
    @endif

    <div class="@cycle(section_class)">
        <h2>{{ section.title }}</h2>
        <p>{{ section.content }}</p>
    </div>
    
@endfor

The @firstof directive

Data often comes from different sources, and sometimes, multiple fields might store the same type of information with different levels of availability.

In PyBlade, the @firstof directive helps you select and display the first variable that holds a meaningful value. This means it will output the first argument that exists and is not empty, is not a False boolean value, is not None and is not a 0 numeric value.

If all the provided variables are "falsy" (empty, None, False, or 0), nothing is shown.

For example, consider this usage:

@firstof(var1, var2, var3)

This is equivalent to writing:

@if (var1)
    {{ var1 }}
@elif(var2)
    {{ var2 }}
@elif(var3)
    {{ var3 }}
@endif

You may also provide a fallback value that will be displayed if none of the variables hold a valid value.

For example, let's say you're building an online job portal. Job seekers may provide multiple ways for employers to contact them, such as a phone number, LinkedIn profile, or a general support email. Instead of manually checking each option, @firstof helps streamline the process:

<p>Contact: @firstof(user.phone, user.linkedin, 'support@jobportal.com')</p>

If user.phone is available, it will be displayed. If not, it falls back to user.linkedin, and if both are missing, 'support@jobportal.com' ensures there's always a valid contact option shown.

Note

By default, PyBlade automatically escapes output for safety. If you want to disable escaping for certain reasons, you can use the .safe filter.

@firstof(var1, var2.safe, var3, "<strong>Fallback Value</strong>".safe)

Sometimes, you may need to store the selected value in a variable for later use. You can assign it a name using the as keyword:

@firstof(var1, var2, var3 as chosen_value)
<p>The selected value is: {{ chosen_value }}</p>

This will make the chosen_value variable available in the context and you may use it later in the template.

The @regroup directive

When dealing with categorized data, grouping is often necessary for clarity and organization. The @regroup directive is used to categorize a list of similar objects based on a shared attribute. Instead of manually organizing the data, this directive automatically groups related items together for easier presentation.

This complex directive is best illustrated by way of an example. Let's say that cities is a list of cities represented by dictionaries containing "name", "population", and "country" keys:

cities = [
    {"name": "Calcutta", "population": "15,000,000", "country": "India"},
    {"name": "Chicago", "population": "7,000,000", "country": "USA"},
    {"name": "Mumbai", "population": "19,000,000", "country": "India"},
    {"name": "New York", "population": "20,000,000", "country": "USA"},
    {"name": "Tokyo", "population": "33,000,000", "country": "Japan"},
]

… and you’d like to display a hierarchical list that is ordered by country, like this:

  • India
    • Mumbai: 19,000,000
    • Calcutta: 15,000,000
  • USA
    • New York: 20,000,000
    • Chicago: 7,000,000
  • Japan
    • Tokyo: 33,000,000

You can use the @regroup directive to group the list of cities by country. The following snippet of template code would accomplish this:

@regroup(cities by country as countries)

<ul>
@for (country in countries)
    <li>{{ country.grouper }}
    <ul>
        @for (city in country.list)
          <li>{{ city.name }}: {{ city.population }}</li>
        @endfor
    </ul>
    </li>
@endfor
</ul>

Let’s walk through this example. @regroup takes three arguments: the list you want to regroup, the attribute to group by, and the name of the resulting list. Here, we’re regrouping the cities list by the country attribute and calling the result contries.

@regroup produces a list (in this case, contries) of group objects. Group objects are instances of namedtuple() with two fields:

  • grouper – the item that was grouped by (e.g., the string "India" or "Japan").
  • list – a list of all items in this group (e.g., a list of all cities with country='India').

The @ifchanged directive

The @ifchanged directive in PyBlade is used only inside loops and helps you detect when a value changes between iterations. You might find it very useful to conditionally render content based on whether a variable has changed compared to its previous state.

When used without arguments, @ifchanged checks its own rendered contents against its previous state and only displays the content if it has changed. For example, this displays a list of days, only displaying the month if it changes:

<h1>Archive for {{ year }}</h1>

@for (date in dates)
    @ifchanged
        <h3>{{ date.strftime('%B') }}</h3>
    @endifchanged
    <a href="/{{ date.strftime('%m/%d').lower() }}">{{ date.strftime('%d') }}</a>
@endfor

If given one or more variables as arguments to @ifchanged directive, it checks whether any variable has changed. For example, the following shows the date every time it changes, while showing the hour if either the hour or the date has changed:

@for (date in dates)
    @ifchanged (date.date)
        <strong>{{ date.date }}</strong>
    @endifchanged
    
    @ifchanged (date.hour, date.date)
        <span>{{ date.hour }}</span>
    @endifchanged
@endfor

You can use an @else clause inside @ifchanged to define an alternative output when the value has not changed. For example, the following alternates between red and blue colors when ballot_id changes, otherwise uses gray:

@for (match in matches)
    <div style="background-color:
        @ifchanged(match.ballot_id)
            @cycle('red', 'blue')
        @else
            gray
        @endifchanged
    ">
        {{ match }}
    </div>
@endfor

Capturing the present with @now

The @now directive is used to display the current date and time in a template. It takes a string format time based on Python’s standard datetime module syntax.

For example, if you want to show the full date and time, you can write:

It is @now("%Y-%m-%d %H:%M:%S")

If the current date and time is March 12, 2025, at 14:30:45, the output will be:

It is 2025-03-12 14:30:45

Sometimes, you may need to store the output of @now in a variable to reuse it in different parts of the template. This can be done using the as keyword.

@now("%B" as current_month)

@if (current_month == "December")
    <p>Happy holidays! It's {{ current_month }}, the festive season.</p>
@else
    <p>Welcome to {{ current_month }}! Hope you have a great month.</p>
@endif

When the as keyword is used within the @now directive to store the current date and time in a variable, the directive does not produce any output on that line.

The table below outlines some of the most commonly used datetime format specifiers from Python’s datetime module:

FormatDescriptionExample Output (March 12, 2025, 14:30:45)
%YFull year2025
%yShort year (last two digits)25
%mMonth (zero-padded)03
%BFull month nameMarch
%bShort month nameMar
%dDay (zero-padded)12
%AFull weekday nameWednesday
%aShort weekday nameWed
%HHour (24-hour format)14
%IHour (12-hour format)02
%MMinutes30
%SSeconds45
%pAM/PMPM

URLs and static files

Generating URLs with @url

The @url directive generates an absolute path (a URL without the domain name) based on a named route and optional parameters. Any special characters in the resulting URL are automatically encoded.

To generate a URL, pass the route name as the first argument, followed by any required parameters. The parameters should be comma-separated.

<a href="@url('some-url-name', post.id, post.slug)">Read the post</a>

In this example, the generated URL will dynamically include the values of post.id and post.slug in the appropriate placeholders defined in the url pattern configuration.

Instead of positional arguments, keyword arguments can be used for better clarity:

<a href="@url('some-url-name', pk=product.id)">Show product</a>

Warning

It is important to note that positional and keyword arguments cannot be mixed within the same @url directive. Additionally, all required parameters must be provided to avoid errors.

Example with dynamic route in Django

Consider a view function client() inside the views.py file, which requires a client ID as a parameter. Its corresponding route might look like this:

urls.py
path("client/<int:id>/", views.client, name="client-detail")

The correct way to generate a link in a template would be:

<a href="@url('client-detail', client.id)">View Client</a>

If client.id is 123, the generated URL would be:

/client/123/

If the URL you’re reversing doesn’t exist, you’ll get an NoReverseMatch exception raised, which will cause your site to display an error page.

When working with Django, if you’d like to retrieve a namespaced URL, specify the fully qualified name:

<a href="@url('myapp:view-name')">Visit MyApp</a>

This will follow the normal namespaced URL resolution strategy, including using any hints provided by the context as to the current application.

Checking the active URL with @urlis

The @urlis directive checks whether the current URL matches a given named route and returns True if it does, or False otherwise. This can be particularly useful for defining active states in navigation menus or conditionally rendering elements based on the active page.

To use @urlis, pass the name of the route as the first argument.

@if (@urlis('home'))
    <span>You are on the homepage!</span>
@endif

The abve code is equivalent to :

@if (request.resolver_match.url_name == 'home')
    <!-- Display content for the home page -->
@endif

If the current URL matches the home route, the message will be displayed. Otherwise, it will not render anything.

A common use case is applying an active class to navigation links. You can achieve this by adding a second string parameter to the @urlis directive. When a second parameter is passed, the provided value is returned instead of True if the current page matches the given route name, otherwise, nothing is returned instead of False.

<li class="@urlis('dashboard', 'active')">
    <a href="@url('dashboard')">Dashboard</a>
</li>
<li class="@urlis('profile', 'active')">
    <a href="@url('profile')">Profile</a>
</li>

If the current page is dashboard, @urlis('dashboard', 'active') returns "active", making the <li> element have the class active. Else, nothing is returned, meaning no additional class is applied.

The @querystring directive

PyBlade provides an intuitive way to generate and manipulate query strings dynamically.

The @querystring directive constructs a URL-encoded query string by adding or modifying parameters in the current query string.

@querystring(color="green", size="M")

Each keyword argument will be added to the current query string, replacing any existing value for that key. For instance, if the currrent query string is ?color=red, The above code would output :

?color=green&size=M

Setting a query parameter to None will remove it from the query string.

If a parameter is a list, @querystring will generate multiple key-value pairs, maintaining the structure.

@querystring(color=my_list)

If my_list = ["red", "blue"], the output will be:

?color=red&color=blue

Note

If no parameters are provided, the querystring directive outputs the current query string verbatim exactly as it appears in the request, or a leading ? if the query string is empty.

A common example of using this directive is to preserve the current query string when displaying a page of paginated results, while adding a link to the next and previous pages of results. For example, if the paginator is currently on page 3, and the current query string is ?color=blue&size=M&page=3, the following code :

@querystring(page=page.next_page_number)

... would output:

?color=blue&size=M&page=4

For efficiency, you may also store the generated query string in a variable for reuse:

@querystring(page=page.next_page_number as next_page)

Then use it multiple times in your template:

<a href="/products/{{ next_page }}">Next Page</a>

The @static directive

In PyBlade, the @static directive provides a convenient way to link to static files such as images, stylesheets, JavaScript files, and other resources that are served from a static folder.

To link to a static file, use the @static directive with the path to the file. This will generate the appropriate URL for the static resource. The format of the URL is typically determined by the project's static settings.

<img src="@static('images/hi.jpg')" alt="Hi!">

This will resolve to the appropriate URL for the image, ensuring it is properly linked according to your static file configuration.

You can also pass context variables to the @static directive. For example, if you have a user_stylesheet variable passed from the view, you can use it to dynamically link to a user's custom stylesheet:

<link rel="stylesheet" href="@static(user_stylesheet)" media="screen">

This way, the value of user_stylesheet will be treated as the path to the stylesheet, which can be modified based on the current user’s preferences or other dynamic conditions.

Sometimes, you may want to retrieve a static URL without displaying it immediately. You may store the URL in a variable for later use. You can do this by using the as keyword, similar to other directives that assign values:

@static('images/hi.jpg' as my_photo)
<img src="{{ my_photo }}">

This stores the static URL for the image in the my_photo variable, which you can then use anywhere in the template.

The @get_static_prefix directive

If you need more control over how the static URL is injected into the template, you can use the @get_static_prefix directive. This will give you the static URL prefix, which is typically the base URL for static files.

<img src="@get_static_prefix images/hi.jpg" alt="Hi!">

This allows you to explicitly append the path to the static resource to the URL prefix.

Alternatively, you can store the static prefix in a variable to avoid repeated processing of the same value:

@get_static_prefix(as STATIC_PREFIX)
<img src="{{ STATIC_PREFIX }}images/hi.jpg" alt="Hi!">
<img src="{{ STATIC_PREFIX }}images/hi2.jpg" alt="Hello!">

By doing this, the static prefix is stored in the STATIC_PREFIX variable, making it easier to use the same prefix multiple times throughout the template.

The @get_media_prefix directive

Similar to @get_static_prefix, the @get_media_prefix directive provides the media URL prefix. This is useful when you need to link to media files, which are typically served separately from static files.

For example, you can use @get_media_prefix to set a data attribute for media URLs:

<body data-media-url="@get_media_prefix">

Internationalization in templates

Internationalization within PyBlade templates are performed by the use of two directives and a slightly different syntax than in Python code to translate some parts of rendered text.

Translated strings will not be escaped when rendered in a template. This allows you to include HTML in translations, for example for emphasis, but potentially dangerous characters (e.g., " ) will also be rendered unchanged.

The @translate directive

The @translate directive allows you to translate either a constant string (enclosed in single or double quotes) or variable content:

<title>@translate("This is the title.")</title>
<title>@translate(myvar)</title>

If the noop option is present, variable lookup still takes place but the translation is skipped. This is useful when "stubbing out" content that will require translation in the future:

<title>@translate("myvar", noop=True)</title>

It’s not possible to mix a template variable inside a string within @translate. If your translations require strings with variables (placeholders), use @blocktranslate instead.

If you’d like to retrieve a translated string without displaying it, you can use the following syntax:

@translate("This is the title" as the_title)

<title>{{ the_title }}</title>
<meta name="description" content="{{ the_title }}">

In practice, you’ll use this to get a string you can use in multiple places in a template or so you can use the output as an argument for other PyBlade directives:

@translate("starting point as start)
@translate("end point" as end)
@translate("La Grande Boucle" as race)

<h1>
  <a href="/" title="@blocktranslate Back to '{{ race }}' homepage @endblocktranslate">{{ race }}</a>
</h1>
<p>
    
@for (stage in tour_stages)
    @cycle(start, end): {{ stage }}@if (loop.index % 2 == 0)<br>@else, @endif
@endfor
</p>

The @translate directive also supports contextual markers using the context keyword argument:

@translate("May", context="month name")

Pro tip

You may use the @trans directive which is a shorthand of the @translate directive.

The @blocktranslate directive

Unlike @translate, the @blocktranslate directive allows marking complex sentences that include both literals and variable content by using placeholders:

@blocktranslate
    This string will have {{ value }} inside.
@endblocktranslate

To translate a template expression, such as accessing object attributes or calling object methods, bind the expression to a local variable within the translation block:

@blocktranslate(amount=article.price)
    That will cost $ {{ amount }}.
@endblocktranslate

You can use multiple expressions inside a single @blocktranslate:

@blocktranslate(book_t=book.title, author_t=author.title() )
    This is {{ book_t }} by {{ author_t }}.
@endblocktranslate

If resolving a block argument fails, @blocktranslate falls back to the default language by temporarily deactivating the current language.

Warning

Other PyBlade directives like @for or @if are not allowed inside @blocktranslate.

Pluralization in @blocktranslate

To use pluralization:

  1. Bind a counter variable as counter.
  2. Specify both singular and plural forms using the @plural directive.

Example:

@blocktranslate(counter=list.length)
    There is only one {{ name }} object.
@plural
    There are {{ counter }} {{ name }} objects.
@endblocktranslate

A more complex example:

@blocktranslate(amount=article.price, counter=i.length)
    That will cost $ {{ amount }} per year.
@plural
    That will cost $ {{ amount }} per {{ counter }} years.
@endblocktranslate

If the counter variable stores a number greater than 1, the singular form will be translated, otherwise, the pluram form will be translated.

It is important to call this counter variable counter.

You can retrieve a translated string without displaying it:

@blocktranslate(as the_title)
    The title is {{ the_title }}.
@endblocktranslate

<title>{{ the_title }}</title>
<meta name="description" content="{{ the_title }}">

The @blocktranslate directive also supports contextual markers using the context keyword argument:

@blocktranslate(name=user.username, context="greeting")
    Hi {{ name }}
@endblocktranslate

The trimmed argument can be used to remove unnecessary whitespace and newlines:

@blocktranslate(trimmed=True)
    First sentence.
    Second paragraph.
@endblocktranslate()

In .po files, this results in

"First sentence. Second paragraph."

rather than:

"\n  First sentence.\n  Second paragraph.\n"

Generating translation files

PyBlade provides CLI commands to help you manage translation files for your internationalized templates.

To generate translation files for a specific locale, use the pyblade make:messages command with the --locale argument:

pyblade make:messages --locale fr

Or using the short form:

pyblade make:messages -l fr

This command will scan your templates for translatable strings and create or update .po files for the specified locale.

Compiling translation files

After translating the strings in your .po files, you need to compile them into .mo files for use in your application. Use the pyblade messages:compile command:

pyblade messages:compile

This command compiles all .po files into their corresponding .mo files, making the translations available to your application.

Other translation utilities

PyBlade also provides a few small utilities that can be useful when working with translations.

@lang

The @lang directive gives you the current language code. You can store it in a template variable and use it wherever you need it.

A common use case is setting the lang attribute of your HTML document:


<!DOCTYPE html>
<html lang="@lang">
<head>
...
</head>
<body>
...
</body>
</html>

If the current language is en-us, PyBlade renders:

<html lang="en-us">

You can also use the language code anywhere else in your template:

<p>Current language: @lang</p>

@languages

The @languages directive gives you the languages configured for your application. This is particularly useful when building a language selector.

@lang(as CURRENT_LANGUAGE)
@languages(as LANGUAGES)

<select name="language">
    @for (language in LANGUAGES)
    <option value="{{ language.code }}" @selected(language.code == CURRENT_LANGUAGE)>
    {{ language.name }}
    </option>
    @endfor
</select>

Each language provides information such as its code and display name. The exact languages available depend on your application's translation configuration.

This keeps language selection framework-agnostic: whether your application uses Django, Flask, Quart, or another supported framework, @languages provides the same PyBlade interface.

Django projects

In Django projects, if the django.template.context_processors.i18n context processor is enabled, Django already provides LANGUAGE_CODE, LANGUAGES, and LANGUAGE_BIDI in the template context.

You can therefore access them directly without using @lang or @languages.

On this page