Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Apple’s Foldable Is Getting Magnets. Samsung Still Puts Them in the Case.

    September 4, 2026

    How to Integrate Social Media With Your Marketing Strategy

    September 4, 2026

    How to Set Up a Facebook Ad Campaign Step by Step

    September 4, 2026
    Facebook X (Twitter) Instagram Threads
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • Gaming
    • Smartwatch
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Unpack Multiple Variables in a Jinja2 Loop
    Blog

    How to Unpack Multiple Variables in a Jinja2 Loop

    Ethan CaldwellBy Ethan CaldwellSeptember 4, 20269 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    Jinja2 unpacks multiple variables in a loop the same way Python does: put a comma separated list of names in the for target and iterate something that yields tuples. {% for key, value in mapping.items() %} is the form you will write most often. What surprises people coming from Python is that zip and enumerate do not exist in a Jinja template by default, and that a variable you assign inside a loop is gone by the time the loop ends.

    Quick answer: Use {% for a, b in pairs %} for any iterable of tuples, and {% for key, value in mydict.items() %} or {% for key, value in mydict|items %} for a mapping. For an index use loop.index rather than enumerate. To pair two lists, either zip them in Python before rendering, or register the builtin with env.globals["zip"] = zip.

    The examples below were run against Jinja 3.1, including the error messages, so the failure text you see in your traceback should match. Everything here works in the standalone library and in Flask, since Flask just configures a Jinja environment for you.

    Tuple unpacking in a for loop

    Any iterable whose items are sequences of the right length can be unpacked. The arity has to match exactly.

    {# rows = [("Alice", 31, "NY"), ("Bo", 24, "LA")] #}
    {% for name, age, city in rows %}
      <tr><td>{{ name }}</td><td>{{ age }}</td><td>{{ city }}</td></tr>
    {% endfor %}

    Nested unpacking works too, with parentheses exactly as in Python.

    {# nested = [(1, (2, 3))] #}
    {% for a, (b, c) in nested %}{{ a }}{{ b }}{{ c }}{% endfor %}
    {# renders: 123 #}

    You can filter in the same statement, which saves an inner if block and keeps loop.first and loop.last meaningful for the filtered set rather than the raw one.

    {% for key, value in counts.items() if value > 1 %}{{ key }} {% endfor %}

    Iterating dictionaries

    A bare {% for k, v in mydict %} does not work, because iterating a mapping yields keys, and a key is a single value. Jinja raises the Python error directly.

    {% for k, v in mydict %}...{% endfor %}
    ValueError: not enough values to unpack (expected 2, got 1)

    There are three correct forms, and they differ in ordering and in portability.

    FormOrderNotes
    mydict.items()Insertion orderCalls the Python method. Fails on objects without .items().
    mydict|itemsInsertion orderJinja filter. Portable to Jinja ports in other languages. Returns an empty iterator if the value is undefined.
    mydict|dictsortSorted by keyTakes reverse, case_sensitive and by arguments to sort by value instead.
    {# mydict = {"b": 2, "a": 1} #}
    {% for k, v in mydict.items() %}{{ k }}={{ v }};{% endfor %}   {# b=2;a=1; #}
    {% for k, v in mydict|items %}{{ k }}={{ v }};{% endfor %}     {# b=2;a=1; #}
    {% for k, v in mydict|dictsort %}{{ k }}={{ v }};{% endfor %}  {# a=1;b=2; #}
    {% for k, v in mydict|dictsort(reverse=true) %}{{ k }}{% endfor %}

    Recommended for you:

    Blog·Sep 4, 2026

    Best State to Buy a Car: Alabama or New Hampshire?

    zip and enumerate are not there

    This is the single most common stumble. Jinja’s default global namespace does not include zip or enumerate, so calling either raises an undefined error.

    {% for i, x in enumerate(items) %}...{% endfor %}
    jinja2.exceptions.UndefinedError: 'enumerate' is undefined

    You have three ways out, in rough order of preference.

    Use loop.index instead of enumerate. It is one indexed, and loop.index0 is zero indexed. There is no reason to reach for enumerate at all.

    {% for x in items %}{{ loop.index0 }}: {{ x }}{% endfor %}

    Zip in Python and pass a list of tuples. This keeps the template dumb, which is the point of a template. Pass list(zip(names, scores)) into the context and unpack it in the loop.

    Register the builtins as globals if you genuinely need them in the template, for example in a reporting template you cannot change the view for.

    # Standalone Jinja
    from jinja2 import Environment, PackageLoader
    
    env = Environment(loader=PackageLoader("myapp"))
    env.globals["zip"] = zip
    env.globals["enumerate"] = enumerate
    
    # Flask
    app.jinja_env.globals.update(zip=zip, enumerate=enumerate)

    Modify globals before any template is loaded. With both registered, the fully nested form works: {% for i, (a, b) in enumerate(zip(x, y)) %} renders as you would expect.

    Tip: If you only need to walk two lists in step and cannot change the context, {% for i in range(a|length) %}{{ a[i] }} {{ b[i] }}{% endfor %} works with no configuration, since range is a Jinja global.

    set with multiple targets

    The set tag takes tuple targets, so you can assign several names at once or destructure a value from the context.

    {% set a, b = 1, 2 %}{{ a }}{{ b }}       {# 12 #}
    {% set low, high = bounds %}{{ low }}-{{ high }}

    Loop scope, and why your counter stays at zero

    Jinja’s documentation states it directly: it is not possible to set variables inside a block and have them show up outside of it, and this applies to loops. So the obvious accumulator does nothing.

    {% set total = 0 %}
    {% for i in [1, 2] %}{% set total = total + i %}{% endfor %}
    {{ total }}      {# renders 0, not 3 #}

    The fix, available since Jinja 2.10, is a namespace object. Assignments to its attributes do propagate out of the loop.

    {% set ns = namespace(total=0, found=false) %}
    {% for i in [1, 2, 3] %}
      {% set ns.total = ns.total + i %}
      {% if i == 2 %}{% set ns.found = true %}{% endif %}
    {% endfor %}
    {{ ns.total }} {{ ns.found }}    {# 6 True #}

    Note that the attribute notation in a set tag is only allowed for namespace objects. You cannot use it to poke a value into an arbitrary object from the context.

    Grouping, batching and the loop variable

    Several filters hand you tuples ready to unpack, which is often cleaner than nesting loops by hand. groupby sorts the values first, so exactly one group comes back per unique key.

    {% for city, rows in users|groupby("city") %}
      <h3>{{ city }}</h3>
      {% for u in rows %}{{ u.name }} {% endfor %}
    {% endfor %}
    
    {% for row in items|batch(3, "&nbsp;") %}
      <tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
    {% endfor %}

    The loop object carries more than an index, and several of its attributes replace bookkeeping variables you would otherwise need a namespace for.

    AttributeWhat it gives you
    loop.index / loop.index0Position, one indexed and zero indexed
    loop.first / loop.lastBooleans for the edges of the sequence
    loop.previtem / loop.nextitemNeighboring items, undefined at the ends
    loop.changed(value)True when the value differs from the previous call, ideal for section headers
    loop.cycle("odd", "even")Rotates through the arguments each iteration

    Troubleshooting

    not enough values to unpack (expected 2, got 1). You are iterating a mapping directly, or a list of plain strings. Add .items() or the items filter for a mapping.

    too many values to unpack (expected 2). Your rows have three elements and your target has two. Jinja will not silently drop the extra, so either add a name or slice in Python.

    ‘zip’ is undefined. Register it on the environment, or zip in the view. Turning on StrictUndefined makes this class of mistake fail loudly instead of rendering an empty string.

    A counter set in a loop reads back as its initial value. Loop scope. Switch to namespace().

    Groups come out in the wrong order. groupby sorts by the grouping key first. If you need source order, sort the data in Python and group there.

    Frequently asked questions

    Can I unpack in a nested loop?

    Yes, and each loop gets its own loop object. To reach the outer one from inside, bind it first with {% set outer_loop = loop %} before the inner loop starts, then refer to outer_loop.index. Otherwise loop always refers to the innermost loop.

    Recommended for you:

    Blog·Sep 4, 2026

    How to Recover a Hacked Facebook Account (2026)

    What is the difference between the items filter and calling .items()?

    Behaviorally almost nothing in Python. The filter is portable to Jinja implementations in other languages whose mapping type has no .items() method, and it returns an empty iterator rather than raising when handed an undefined value.

    How do I iterate two lists together?

    Preferably zip them in the view and pass a list of tuples. If you must do it in the template, either register zip on the environment globals or index both lists with {% for i in range(a|length) %}, since range is available by default.

    Does namespace work in older versions?

    Namespace objects arrived in Jinja 2.10. On anything older you have to restructure: compute the aggregate in Python, or use a filter such as sum or selectattr combined with length to get the number without accumulating in the template.

    Is there a for else in Jinja?

    Yes, and it is unlike Python’s. The {% else %} block in a Jinja for loop runs when the sequence was empty, which makes it a tidy way to render an empty state message without a separate length check.

    The bottom line

    Unpacking itself is Python’s rules with Python’s error messages. The parts that are genuinely different are the missing globals and the loop scope, and both have clean answers: loop.index replaces enumerate, zipping belongs in the view, and namespace() exists for the rare case where you must accumulate inside a loop.

    When something behaves oddly, check the two references directly: the Jinja template designer documentation for loops and filters, and the API documentation for the environment globals. If you also work in Django templates, our guide to marking strings as safe in Django covers the escaping model in the other engine, and for the underlying data shaping our notes on joining two vectors and on explaining a solution clearly are worth a look.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleMarketing Is Changing Fast: What the Consultancies Say
    Next Article Best State to Buy a Car: Alabama or New Hampshire?
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem - iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      9 Mins Read

      How to Integrate Social Media With Your Marketing Strategy

      11 Mins Read

      How to Set Up a Facebook Ad Campaign Step by Step

      10 Mins Read

      How to Create a Content Calendar for Consistent Publishing

      9 Mins Read

      How to Add a Call to Action Button on a Facebook Page

      10 Mins Read

      How to Download a Copy of Your Facebook Data

      10 Mins Read

      Is Pennsylvania a Good State to Raise a Family?

      Top Posts

      How to Change HEIC to JPG on iPhone, Mac, Android and Windows (No Software Needed)

      September 3, 20263 Views

      How to Spot AI Generated Images in 2026 (The Old Tricks Stopped Working)

      September 3, 20262 Views

      Check Which Apps Can Read Your Gmail, and Cut Them Off in 60 Seconds

      September 3, 20262 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      How to Change HEIC to JPG on iPhone, Mac, Android and Windows (No Software Needed)

      September 3, 20263 Views

      The EU AI Act Just Became Enforceable, and Most AI Companies Are Not Ready

      August 6, 20263 Views

      How to Spot AI Generated Images in 2026 (The Old Tricks Stopped Working)

      September 3, 20262 Views
      Our Picks

      Apple’s Foldable Is Getting Magnets. Samsung Still Puts Them in the Case.

      September 4, 2026

      How to Integrate Social Media With Your Marketing Strategy

      September 4, 2026

      How to Set Up a Facebook Ad Campaign Step by Step

      September 4, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      HEICJPG.online - Convert HEIC to JPG online
      Facebook
      • About Us
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      • Editorial Policy
      • Cookie Policy
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.