{"url":"/kida/docs/tutorials/upgrade-to-v0.7/","title":"Upgrade to 0.7","description":"Migrate to strict-by-default variable access and the new null-safe idioms","plain_text":"Upgrade to 0.7 Guide for moving a codebase from Kida 0.6.x to 0.7.x. :::note[Why this tutorial exists] Kida 0.7.0 flipped strict_undefined to True by default and added a parse-time check (K-TPL-004) that rejects templates which rely on the old silent-empty-string behavior. This is a breaking change in a 0.x series — this tutorial collects the migration patterns in one place so downstream libraries do not each have to rediscover them. ::: TL;DR Environment(strict_undefined=True) is now the default. Missing variables, attributes, and keys raise UndefinedError. Fix each site with one of: is defined, ?? (null-coalescing), | default(...), ?. (optional chaining), or | get(&quot;key&quot;, default). Need to unblock first, fix later? Pass strict_undefined=False on the Environment as a transitional shim. Prerequisites Python 3.14+ Kida 0.6.x codebase with templates that run today What changed strict_undefined=True is now the default In 0.6.x, Environment(strict_undefined=...) defaulted to False. Missing attributes rendered as &quot;&quot; silently — which contradicted the documented &quot;strict-by-default&quot; stance. In 0.7.x, the default is True: from kida import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader(&quot;templates/&quot;)) # strict_undefined=True — missing vars/attrs/keys raise UndefinedError UndefinedError now carries a kind (variable, attribute, or key) so error messages point at the right layer. Parse-time check K-TPL-004 Templates that previously relied on global variables being silently-present-but-missing now fail at parse time with a targeted hint. See the error-code reference for details. The three fix patterns All three patterns work under strict_undefined=True. Pick the one that reads best at each site. Pattern 1 — is defined {# Before (relied on silent empty string) #} &#123;% if user.nickname %&#125;Hello, {{ user.nickname }}!&#123;% end %&#125; {# After #} &#123;% if user.nickname is defined and user.nickname %&#125; Hello, {{ user.nickname }}! &#123;% end %&#125; is defined works on attribute chains, not just top-level variables — if any part of the chain is missing, the result is treated as undefined. Pattern 2 — ?? (null-coalescing) {# Before #} &lt;meta name=&quot;description&quot; content=&quot;{{ page.description }}&quot;&gt; {# After #} &lt;meta name=&quot;description&quot; content=&quot;{{ page.description ?? &#x27;&#x27; }}&quot;&gt; The ?? operator is short-circuit: it evaluates the right side only when the left is undefined or None. Pattern 3 — | default(...) {# Before #} &lt;title&gt;{{ page.title }}&lt;/title&gt; {# After #} &lt;title&gt;{{ page.title | default(&quot;Untitled&quot;) }}&lt;/title&gt; Use | default when you want a named fallback value that reads as documentation at the call site. Escape hatch If you need to unblock an upgrade now and fix sites over time: env = Environment( loader=FileSystemLoader(&quot;templates/&quot;), strict_undefined=False, ) Lenient mode is a transitional shim. Missing attributes return an _Undefined sentinel that stringifies to &quot;&quot;, is falsy, and is iterable as empty. This is the 0.6.x behavior. :::tip[Recommended path] Flip strict_undefined=False once to unblock the release, then fix sites one at a time and flip it back (or just delete the kwarg). Keeping the escape hatch long-term defeats the point of the upgrade. ::: Preferred idioms going forward 0.7.x ships with first-class null-safe operators. Prefer these over .get(&quot;key&quot;, &quot;&quot;) chains: ?. — optional attribute access (receiver-only) ?. short-circuits when the receiver is None or undefined. It does not suppress UndefinedError for a missing attribute/key on a defined receiver — strict mode still raises, by design. {# Receiver is None — yields &quot;&quot; #} {{ config?.theme }} {# config = None → &quot;&quot; #} {# Defined receiver, missing key — still raises under strict mode #} {{ config?.theme }} {# config = {} → UndefinedError #} {# Safe-in-both-directions forms: #} {{ config?.theme ?? &quot;&quot; }} {# catch missing key with ?? #} {{ config | get(&quot;theme&quot;, &quot;&quot;) }} {# or the get filter #} Chains short-circuit at the first None: {{ page?.author?.avatar }} {# any None in the chain → &quot;&quot; #} {{ page?.author?.avatar ?? &quot;/default.png&quot; }} {# with a named fallback #} ?[...] — optional item access (receiver-only) Mirror of ?.. Short-circuits only on a None/undefined receiver: {{ settings?[&quot;theme&quot;] }} {# settings is None → &quot;&quot; #} {{ settings?[&quot;theme&quot;] ?? &quot;light&quot; }} {# also handles missing key #} {{ items?[0] }} {# items is None → &quot;&quot; #} | get(key, default) — filter form (closest to dict.get) {# Drop-in replacement for dict.get(&quot;key&quot;, default) #} {{ config | get(&quot;theme&quot;, &quot;light&quot;) | upper }} The get filter handles dicts, objects, and None uniformly, and — unlike ?. alone — also catches missing keys. Prefer it when the value is a key lookup that may be missing, rather than a variable that may be None. Combining operators {# Null-safe chain with a named fallback #} {{ user?.profile?.bio ?? &quot;No bio yet&quot; }} {# Pipeline form #} {{ config ?| get(&quot;theme&quot;) ?? &quot;light&quot; }} Common surprises Warning &quot;My `&#123;% if x %&#125;` guard stopped working&quot; Under strict mode, &#123;% if x %&#125; raises if x is not defined. Use is defined: {# Before (worked in 0.6.x lenient mode) #} &#123;% if user.nickname %&#125;...&#123;% end %&#125; {# After (0.7.x strict mode) #} &#123;% if user.nickname is defined and user.nickname %&#125;...&#123;% end %&#125; Tag &quot;I was relying on empty-string fallback in attributes&quot; {# Before — rendered as &quot;&quot; when missing #} &lt;img src=&quot;{{ user.avatar }}&quot;&gt; {# After #} &lt;img src=&quot;{{ user.avatar ?? &#x27;/default-avatar.png&#x27; }}&quot;&gt; Code &quot;K-TPL-004 fires on my template&quot; The parse-time check catches templates that would have silently rendered empty under 0.6.x lenient mode. The error message names the variable and suggests a fix. Apply one of the three fix patterns above at the reported line. Terminal &quot;My test suite prints ~1000 `from_string()` warnings&quot; Fixed in 0.7.1. The warning now fires once per distinct source per Environment instead of on every call. If you are still on 0.7.0, you can also pass name= explicitly to from_string() to silence it and enable bytecode caching. Where to go next [[docs/troubleshooting/undefined-variable|Troubleshooting UndefinedError]] — per-error debugging Tests Reference — full is defined / is none / etc. list Variables &amp; Operators — ?., ?[, ??, ?|, |&gt; CHANGELOG 0.7.0 — full release notes","excerpt":"Upgrade to 0.7 Guide for moving a codebase from Kida 0.6.x to 0.7.x. :::note[Why this tutorial exists] Kida 0.7.0 flipped strict_undefined to True by default and added a parse-time check (K-TPL-004)...","metadata":{"draft":false,"variant":"standard","description":"Migrate to strict-by-default variable access and the new null-safe idioms","type":"doc","tags":["migration","upgrade","tutorial","strict-undefined"],"icon":"arrow-up-circle","keywords":["upgrade",0.7,"strict_undefined","migration","null-safe"],"weight":15.0,"lang":"en","title":"Upgrade to 0.7"},"section":"tutorials","tags":["migration","upgrade","tutorial","strict-undefined"],"word_count":968,"reading_time":5,"navigation":{"parent":"/kida/docs/tutorials/","prev":"/kida/docs/tutorials/component-comparison/","next":"/kida/docs/tutorials/flask-integration/","related":["/kida/docs/tutorials/migrate-from-jinja2/","/kida/docs/tutorials/component-comparison/","/kida/docs/get-started/coming-from-jinja2/","/kida/docs/get-started/quickstart/","/kida/docs/get-started/first-project/"]},"last_modified":"2026-04-20T22:50:44.682965","content_hash":"eaeffc64488736d8a3b2f5347251fecc85c1272d4d35205d69da534927eb6d6d","graph":{"nodes":[{"id":"4226361357275701409","label":"Jinja2 vs Kida: Components","url":"/kida/docs/tutorials/component-comparison/","type":"hub","tags":["tutorial","components","jinja2","migration"],"incoming_refs":15,"outgoing_refs":5,"connectivity":20,"reading_time":7,"size":33,"color":"var(--graph-node-hub)"},{"id":"1395480798510346663","label":"Upgrade to 0.7","url":"/kida/docs/tutorials/upgrade-to-v0.7/","type":"hub","tags":["migration","upgrade","tutorial","strict-undefined"],"incoming_refs":11,"outgoing_refs":6,"connectivity":17,"reading_time":5,"size":32,"color":"var(--graph-node-hub)","isCurrent":true},{"id":"4024928898734290807","label":"First Project","url":"/kida/docs/get-started/first-project/","type":"regular","tags":["tutorial","quickstart"],"incoming_refs":8,"outgoing_refs":6,"connectivity":14,"reading_time":2,"size":29,"color":"var(--graph-node-regular)"},{"id":"-8583829768471255680","label":"Quickstart","url":"/kida/docs/get-started/quickstart/","type":"regular","tags":["quickstart","tutorial"],"incoming_refs":8,"outgoing_refs":6,"connectivity":14,"reading_time":2,"size":29,"color":"var(--graph-node-regular)"},{"id":"-69645563692388952","label":"Migrate from Jinja2","url":"/kida/docs/tutorials/migrate-from-jinja2/","type":"regular","tags":["migration","jinja2","tutorial"],"incoming_refs":8,"outgoing_refs":5,"connectivity":13,"reading_time":3,"size":29,"color":"var(--graph-node-regular)"},{"id":"-5012698523729185470","label":"Tutorials","url":"/kida/docs/tutorials/","type":"regular","tags":["tutorials"],"incoming_refs":3,"outgoing_refs":9,"connectivity":12,"reading_time":1,"size":26,"color":"var(--graph-node-regular)"},{"id":"1188054322154556940","label":"Coming from Jinja2","url":"/kida/docs/get-started/coming-from-jinja2/","type":"regular","tags":["migration","jinja2"],"incoming_refs":6,"outgoing_refs":5,"connectivity":11,"reading_time":4,"size":27,"color":"var(--graph-node-regular)"},{"id":"-5716259158887807861","label":"Flask Integration","url":"/kida/docs/tutorials/flask-integration/","type":"regular","tags":["tutorial","flask","web"],"incoming_refs":4,"outgoing_refs":6,"connectivity":10,"reading_time":2,"size":24,"color":"var(--graph-node-regular)"}],"edges":[{"source":"1188054322154556940","target":"1395480798510346663","weight":2},{"source":"4024928898734290807","target":"1395480798510346663","weight":2},{"source":"-8583829768471255680","target":"1395480798510346663","weight":2},{"source":"-5012698523729185470","target":"1395480798510346663","weight":1},{"source":"4226361357275701409","target":"1395480798510346663","weight":2},{"source":"-5716259158887807861","target":"1395480798510346663","weight":2},{"source":"-69645563692388952","target":"1395480798510346663","weight":2}]},"chunks":[{"anchor":"","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7","title":"","level":1,"content":"Upgrade to 0.7 Guide for moving a codebase from Kida 0.6.x to 0.7.x. :::note[Why this tutorial exists] Kida 0.7.0 flipped strict_undefined to True by default and added a parse-time check (K-TPL-004) that rejects templates which rely on the old silent-empty-string behavior. This is a breaking change in a 0.x series — this tutorial collects the migration patterns in one place so downstream libraries do not each have to rediscover them. :::","content_hash":"ac33314a18797aa3cf6cf19e62ba52e1cd33a07e35d4e8ba70c0df3c63440df5"},{"anchor":"tldr","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#tldr","title":"TL;DR","level":2,"content":"TL;DR Environment(strict_undefined=True) is now the default. Missing variables, attributes, and keys raise UndefinedError. Fix each site with one of: is defined, ?? (null-coalescing), | default(...), ?. (optional chaining), or | get(\"key\", default). Need to unblock first, fix later? Pass strict_undefined=False on the Environment as a transitional shim.","content_hash":"123c3bfb2faf4bd4bc41586c0a4ae0529cdf9cc9c25c24b886e125d98db82ec8"},{"anchor":"prerequisites","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#prerequisites","title":"Prerequisites","level":2,"content":"Prerequisites Python 3.14+ Kida 0.6.x codebase with templates that run today","content_hash":"aa8a6a79078aa73a5bb560d047eb2e56ff7f8ad9f7dc8591942a645997ce708a"},{"anchor":"what-changed","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#what-changed","title":"What changed","level":2,"content":"What changed","content_hash":"1bcbf1dcad4ffe4950cdae6b868f8a81fabe5f9da2d92abc3cef577e25fee639"},{"anchor":"strict_undefinedtrue-is-now-the-default","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#strict_undefinedtrue-is-now-the-default","title":"strict_undefined=True is now the default","level":3,"content":"strict_undefined=True is now the default In 0.6.x, Environment(strict_undefined=...) defaulted to False. Missing attributes rendered as \"\" silently — which contradicted the documented \"strict-by-default\" stance. In 0.7.x, the default is True: from kida import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader(\"templates/\")) # strict_undefined=True — missing vars/attrs/keys raise UndefinedError UndefinedError now carries a kind (variable, attribute, or key) so error messages point at the right layer.","content_hash":"3ed17a1be99369f940e432813848c69a42a395888965558ad1d5cd23efec1238"},{"anchor":"parse-time-check-k-tpl-004","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#parse-time-check-k-tpl-004","title":"Parse-time check K-TPL-004","level":3,"content":"Parse-time check K-TPL-004 Templates that previously relied on global variables being silently-present-but-missing now fail at parse time with a targeted hint. See the error-code reference for details.","content_hash":"1cb0101618a4c2ae90bae01d73aa5197bf941f83e39e4d8aadc8fd287055d823"},{"anchor":"the-three-fix-patterns","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#the-three-fix-patterns","title":"The three fix patterns","level":2,"content":"The three fix patterns All three patterns work under strict_undefined=True. Pick the one that reads best at each site.","content_hash":"2ea0931a0ea8bc4e7a09517b1405f808b89aa6944b3d1258887fc2fbbe1666ea"},{"anchor":"pattern-1-is-defined","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#pattern-1-is-defined","title":"Pattern 1 — is defined","level":3,"content":"Pattern 1 — is defined {# Before (relied on silent empty string) #} {% if user.nickname %}Hello, {{ user.nickname }}!{% end %} {# After #} {% if user.nickname is defined and user.nickname %} Hello, {{ user.nickname }}! {% end %} is defined works on attribute chains, not just top-level variables — if any part of the chain is missing, the result is treated as undefined.","content_hash":"14ca967c6a7dae0ae5702551832ed359010015a72b7db815ba482161aef558fd"},{"anchor":"pattern-2-null-coalescing","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#pattern-2-null-coalescing","title":"Pattern 2 — ?? (null-coalescing)","level":3,"content":"Pattern 2 — ?? (null-coalescing) {# Before #} <meta name=\"description\" content=\"{{ page.description }}\"> {# After #} <meta name=\"description\" content=\"{{ page.description ?? '' }}\"> The ?? operator is short-circuit: it evaluates the right side only when the left is undefined or None.","content_hash":"2a5e202ff6add1fd3d467970fe67719a98b7db888e187ea6ac281839b65c60e5"},{"anchor":"pattern-3-default","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#pattern-3-default","title":"Pattern 3 — | default(...)","level":3,"content":"Pattern 3 — | default(...) {# Before #} <title>{{ page.title }}</title> {# After #} <title>{{ page.title | default(\"Untitled\") }}</title> Use | default when you want a named fallback value that reads as documentation at the call site.","content_hash":"e52bf88a10a4293cd30e96a145f574d078520df84847c43bd69c47456e54a184"},{"anchor":"escape-hatch","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#escape-hatch","title":"Escape hatch","level":2,"content":"Escape hatch If you need to unblock an upgrade now and fix sites over time: env = Environment( loader=FileSystemLoader(\"templates/\"), strict_undefined=False, ) Lenient mode is a transitional shim. Missing attributes return an _Undefined sentinel that stringifies to \"\", is falsy, and is iterable as empty. This is the 0.6.x behavior. :::tip[Recommended path] Flip strict_undefined=False once to unblock the release, then fix sites one at a time and flip it back (or just delete the kwarg). Keeping the escape hatch long-term defeats the point of the upgrade. :::","content_hash":"82f5bae5e5c79e43061b44b3d4a450d99954f836dc3455db4ac5753761ecf5f5"},{"anchor":"preferred-idioms-going-forward","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#preferred-idioms-going-forward","title":"Preferred idioms going forward","level":2,"content":"Preferred idioms going forward 0.7.x ships with first-class null-safe operators. Prefer these over .get(\"key\", \"\") chains:","content_hash":"4853b989bcc8af8e121f1e12a9abebf19f867e146f440ee5586fe20fb5494b2e"},{"anchor":"optional-attribute-access-receiver-only","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#optional-attribute-access-receiver-only","title":"?. — optional attribute access (receiver-only)","level":3,"content":"?. — optional attribute access (receiver-only) ?. short-circuits when the receiver is None or undefined. It does not suppress UndefinedError for a missing attribute/key on a defined receiver — strict mode still raises, by design. {# Receiver is None — yields \"\" #} {{ config?.theme }} {# config = None → \"\" #} {# Defined receiver, missing key — still raises under strict mode #} {{ config?.theme }} {# config = {} → UndefinedError #} {# Safe-in-both-directions forms: #} {{ config?.theme ?? \"\" }} {# catch missing key with ?? #} {{ config | get(\"theme\", \"\") }} {# or the get filter #} Chains short-circuit at the first None: {{ page?.author?.avatar }} {# any None in the chain → \"\" #} {{ page?.author?.avatar ?? \"/default.png\" }} {# with a named fallback #}","content_hash":"2c625e5da1150907db725c14e91b639d7c14585ea358403488b29b4355e423ea"},{"anchor":"optional-item-access-receiver-only","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#optional-item-access-receiver-only","title":"?[...] — optional item access (receiver-only)","level":3,"content":"?[...] — optional item access (receiver-only) Mirror of ?.. Short-circuits only on a None/undefined receiver: {{ settings?[\"theme\"] }} {# settings is None → \"\" #} {{ settings?[\"theme\"] ?? \"light\" }} {# also handles missing key #} {{ items?[0] }} {# items is None → \"\" #}","content_hash":"eadfab24a97ad0e9015bce4d004756958e934a54d737747b2ec11be5f3764bb6"},{"anchor":"getkey-default-filter-form-closest-to-dictget","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#getkey-default-filter-form-closest-to-dictget","title":"| get(key, default) — filter form (closest to dict.get)","level":3,"content":"| get(key, default) — filter form (closest to dict.get) {# Drop-in replacement for dict.get(\"key\", default) #} {{ config | get(\"theme\", \"light\") | upper }} The get filter handles dicts, objects, and None uniformly, and — unlike ?. alone — also catches missing keys. Prefer it when the value is a key lookup that may be missing, rather than a variable that may be None.","content_hash":"49dfc300d0362e2b8ec63473ceb90efb999ac47582699bcb18ba20b4d7913b37"},{"anchor":"combining-operators","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#combining-operators","title":"Combining operators","level":3,"content":"Combining operators {# Null-safe chain with a named fallback #} {{ user?.profile?.bio ?? \"No bio yet\" }} {# Pipeline form #} {{ config ?| get(\"theme\") ?? \"light\" }}","content_hash":"854ce0d881c3462d35d8585adb381e3ea3a8e391a38f904078cf7631e722b298"},{"anchor":"common-surprises","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#common-surprises","title":"Common surprises","level":2,"content":"Common surprises Warning \"My `{% if x %}` guard stopped working\" Under strict mode, {% if x %} raises if x is not defined. Use is defined: {# Before (worked in 0.6.x lenient mode) #} {% if user.nickname %}...{% end %} {# After (0.7.x strict mode) #} {% if user.nickname is defined and user.nickname %}...{% end %} Tag \"I was relying on empty-string fallback in attributes\" {# Before — rendered as \"\" when missing #} <img src=\"{{ user.avatar }}\"> {# After #} <img src=\"{{ user.avatar ?? '/default-avatar.png' }}\"> Code \"K-TPL-004 fires on my template\" The parse-time check catches templates that would have silently rendered empty under 0.6.x lenient mode. The error message names the variable and suggests a fix. Apply one of the three fix patterns above at the reported line. Terminal \"My test suite prints ~1000 `from_string()` warnings\" Fixed in 0.7.1. The warning now fires once per distinct source per Environment instead of on every call. If you are still on 0.7.0, you can also pass name= explicitly to from_string() to silence it and enable bytecode caching.","content_hash":"83bb84e3b53e5d0bc1fa78673f0a72cf08c2d85c0f46a64aa19a737494426d93"},{"anchor":"where-to-go-next","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.7#where-to-go-next","title":"Where to go next","level":2,"content":"Where to go next [[docs/troubleshooting/undefined-variable|Troubleshooting UndefinedError]] — per-error debugging Tests Reference — full is defined / is none / etc. list Variables & Operators — ?., ?[, ??, ?|, |> CHANGELOG 0.7.0 — full release notes","content_hash":"0fe18a12ba83ced1b485ca659bfa9e195b4c8c84ccf69d8277d4d6d2b56e8235"}]}