Undefined Variable

Debug undefined variable errors

6 min read 1158 words

DebugUndefinedErrorexceptions.

:::tip[Upgrading from 0.6.x?] strict_undefined=True became the default in 0.7.0. If you are hitting a wave of UndefinedErrorexceptions after an upgrade, see Upgrade to 0.7 for the three fix patterns and the escape hatch. :::

The Error

UndefinedError: Undefined variable 'usre' in page.html:5

Recent Kida diagnostics also carry structured fields for frameworks:

  • to_diagnostic().location— template name, line, and optional column
  • to_diagnostic().source_snippet— surrounding source lines
  • to_diagnostic().hints— ordered next actions
  • to_diagnostic().template_stack and .component_stack— render path context

Common Causes

Solutions

Hints are ordered by confidence. A close typo match appears first; optional value patterns such asdefault(...), ??, or null-safe access follow.

Use default Filter

{{ user.nickname | default("Anonymous") }}
{{ config.timeout | default(30) }}

Check with is defined

Theis defined test works on attribute chains, not just top-level variables. If any part of the chain is missing, the result is undefined:

{% if user is defined %}
    {{ user.name }}
{% else %}
    Guest
{% end %}

Attribute Chains

{# Checks if pokemon has a "name" attribute — not just if pokemon exists #}
{% if pokemon.name is defined %}
    {{ pokemon.name }}
{% end %}

{# Works with dict keys too #}
{% if settings.theme is defined %}
    Theme: {{ settings.theme }}
{% end %}

{# Deep chains #}
{% if page.author.avatar is defined %}
    <img src="{{ page.author.avatar }}">
{% end %}

Undefined Sentinel (lenient mode only)

Withstrict_undefined=False (opt-in), missing attribute access returns an _Undefinedsentinel. The sentinel is:

  • Falsy{% if pokemon.name %}works as a guard
  • Stringifies to""{{ pokemon.name }}renders nothing when undefined
  • Iterable — yields nothing, so{% for x in missing_attr %}produces no output

Under the default strict mode, missing attributes raiseUndefinedError. Use is defined, ??, or | default(...)to opt specific sites into lenient behavior. See Tests Reference for the full test list.

Optional Chaining Pattern

{% if post is defined and post.author is defined %}
    {{ post.author.name }}
{% end %}

Safe Navigation

{{ user.name ?? "Unknown" }}

Prefer Null-Safe Operators

Understrict_undefined=True, reaching for Python's .get("k", "")inside templates adds noise at every call site. Kida ships with first-class null-safe operators — prefer these:

Optional Chaining —?. and ?[...](Mapping-soft, object-strict)

Since v0.8.0,?. and ?[...] short-circuit to Nonewhen either:

  • The receiver isNoneor undefined, or
  • The receiver is a Mapping (dict or Mappingsubclass) and the key is missing.

Missing attributes on a non-Mapping object still raise under strict mode — that's the typo-detection value ofstrict_undefined:

{# Receiver-None — yields "" #}
{{ config?.theme }}        {# config = None → "" #}

{# Mapping miss — yields "" (dict.get() idiom) #}
{{ config?.theme }}        {# config = {}   → "" #}

{# Object attr miss — still raises, combine with ?? for safety #}
{{ user?.nickname ?? "" }}  {# user = User() with no .nickname → "" #}

{# Safe patterns #}
{{ config?.theme ?? "dark" }}
{{ settings?["theme"] ?? "light" }}

| get(key, default) Filter — drop-in for dict.get

{# Handles None receiver, missing dict key, AND missing object attr uniformly #}
{{ config | get("theme", "light") | upper }}

Use | getwhen the lookup must be safe across all receiver shapes in one expression.

Chaining

{# Deep access with a named fallback #}
{{ user?.profile?.bio ?? "No bio yet" }}

{# Null-safe filter pipeline #}
{{ config ?| get("theme") ?? "light" }}

:::tip[Coming from Jinja2?] Jinja2 lacks these operators, so a common Jinja2 pattern is{{ config.get("theme", "") | upper }} (using dict.get). That still works in Kida, but {{ config?.theme | upper }}is the preferred Kida idiom. :::

Debug Tips

# In Python
print(context.keys())

Use debug Filter

{{ user | debug }}

Output (to stderr):

DEBUG: <User>
  .name = 'Alice'
  .email = 'alice@example.com'

Check Template Context

def render_debug(template_name, **context):
    print(f"Rendering {template_name}")
    print(f"Context keys: {list(context.keys())}")
    return env.render(template_name, **context)

Strict Mode (Default)

As of 0.7.0,strict_undefined=True is the default. Missing variables and missing attributes raise UndefinedErrorwith a descriptive message distinguishing variable, attribute, and key lookups.

env = Environment(loader=FileSystemLoader("templates/"))
# strict_undefined=True by default

To guard optional access within a template, use one of:

{{ user.nickname ?? "Anonymous" }}              {# null-coalescing #}
{{ user.nickname | default("Anonymous") }}       {# default filter #}
{% if user.nickname is defined %}...{% end %}    {# explicit test #}

Opt Out (Lenient Mode)

If you are porting templates that rely on silent empty-string fallback for missing attributes:

env = Environment(
    loader=FileSystemLoader("templates/"),
    strict_undefined=False,
)

In lenient mode, missing attributes return an _Undefinedsentinel (see above). This is recommended only as a transitional shim — prefer fixing sites with the idioms above.

Prevention

Type Hints for Context

from dataclasses import dataclass

@dataclass
class PageContext:
    title: str
    user: User
    items: list[Item]

# IDE will catch missing fields
context = PageContext(title="Hello", user=user, items=items)
template.render(**asdict(context))

Template Validation

def validate_context(context, required):
    missing = [k for k in required if k not in context]
    if missing:
        raise ValueError(f"Missing: {missing}")

See Also