Loops and Conditionals
======================
Planners are repetitive by nature - twelve month pages, up to 31 day pages per
month, seven weekday headers in every table. Loops let you generate all that
HTML from a single block of code. Conditionals let you change what appears based
on the data - for example highlighting weekends in red.
**Key topics**
* ``for`` loops and how to iterate over months, days, weeks.
* The ``loop`` helper variable (index, first, last).
* ``if`` / ``elif`` / ``else`` conditionals.
* The ``is none`` test for empty calendar cells.
For loops
---------
A ``for`` loop repeats a block of HTML once for every item in a list.
**Template:**
.. code-block:: html+jinja
%% for month in year.months
- {{ month }}
%% endfor
**Output:**
.. code-block:: html
- January
- February
- March
- April
- May
- June
- July
- August
- September
- October
- November
- December
``year.months`` is a list of twelve Month objects. On each pass through the loop
the variable ``month`` holds the current item. The name before ``in`` is up to
you - ``month``, ``m`` or anything else.
Nested loops
~~~~~~~~~~~~
Loops can be placed inside other loops. The planner template uses this to build
calendar tables - an outer loop over weeks and an inner loop over days in each
week.
**Template:**
.. code-block:: html+jinja
%% for month in year.months
{{ month }}
%% for week in month.table
%% for day in week
| {{ day }} |
%% endfor
%% endfor
%% endfor
**Output** (January 2026 excerpt, first two weeks):
.. code-block:: html
January
|
|
|
1 |
2 |
3 |
4 |
| 5 |
6 |
7 |
8 |
9 |
10 |
11 |
...
Some cells are empty because January 2026 starts on a Thursday. We will handle
those empty cells with a conditional below.
The ``loop`` variable
---------------------
Inside every ``for`` block Jinja2 provides a special variable called ``loop``
with useful information about the current iteration.
.. list-table::
:header-rows: 1
:widths: 30 45
* - Property
- Description
* - ``loop.index``
- Current iteration, starting at 1.
* - ``loop.index0``
- Current iteration, starting at 0.
* - ``loop.first``
- ``true`` on the first iteration.
* - ``loop.last``
- ``true`` on the last iteration.
* - ``loop.length``
- Total number of items.
**Template:**
.. code-block:: html+jinja
%% for month in year.months
- {{ loop.index }}. {{ month }}
%% endfor
**Output:**
.. code-block:: html
- 1. January
- 2. February
- 3. March
...
- 12. December
Using ``loop.index0`` for tuple access
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a planner has twelve month pages, each with its own background image, the
images can be stored in a tuple and accessed by position using ``loop.index0``:
**Template:**
.. code-block:: html+jinja
%% set month_backgrounds = (
"assets/jan.png",
"assets/feb.png",
"assets/mar.png",
)
%% for month in year.months
{{ month }}
%% endfor
On the first iteration ``loop.index0`` is 0, so it picks
``assets/jan.png``. On the second it is 1, and so on.
.. tip::
Use ``loop.index0`` (zero-based) when indexing into a list or tuple. Use
``loop.index`` (one-based) when displaying a number to the reader.
If / elif / else
----------------
Conditionals let you show or hide HTML based on a condition.
**Template:**
.. code-block:: html+jinja
%% for day in calendar.weekdays
%% if day.is_off_day
{{ day.short_name }} |
%% else
{{ day.short_name }} |
%% endif
%% endfor
**Output:**
.. code-block:: html
Mon |
Tue |
Wed |
Thu |
Fri |
Sat |
Sun |
Saturday and Sunday have ``is_off_day`` set to ``true``, so they get the red
style.
Inline conditionals
~~~~~~~~~~~~~~~~~~~
For short checks you can write the condition on one line:
.. code-block:: html+jinja
{{ day.short_name }}
|
This produces the same result as the block form above but is more compact.
The ``is none`` test
--------------------
``month.table`` is a grid of weeks and days. Some cells are empty - for example
January 2026 starts on Thursday, so Monday through Wednesday of the first week
have no day. These cells contain ``None``.
Use ``is none`` (or ``is not none``) to check:
**Template:**
.. code-block:: html+jinja
%% for week in month.table
%% for day in week
%% if day is not none
| {{ day }} |
%% else
|
%% endif
%% endfor
%% endfor
**Output** (January 2026, first week):
.. code-block:: html
|
|
|
1 |
2 |
3 |
4 |
.. warning::
Always use ``is none`` or ``is not none`` instead of ``== None`` or
``!= None``. The ``is`` form is the correct Jinja2 way.
**Do:**
.. code-block:: html+jinja
%% if day is not none
**Don't:**
.. code-block:: html+jinja
%% if day != None
Do and don't summary
--------------------
.. list-table::
:header-rows: 1
:widths: 50 50
* - Do
- Don't
* - ``%% if day is not none``
- ``%% if day != None``
* - ``loop.index0`` for tuple/list access.
- ``loop.index`` when you need zero-based indexing.
* - Close every ``for`` with ``endfor``.
- Forget ``%% endfor`` - the template will fail to render.
* - Close every ``if`` with ``endif``.
- Forget ``%% endif``.
Update the Demo Planner
-----------------------
Add a second page that shows a calendar table for the month. Open
``planners/demo/demo.html`` and add the following after the cover page div
(before ``