{"url":"/kida/docs/tutorials/upgrade-to-v0.8/","title":"Upgrade to 0.8","description":"Optional chaining now treats missing Mapping keys like dict.get()","plain_text":"Upgrade to 0.8 Guide for moving a codebase from Kida 0.7.x to 0.8.0. :::note[Why this tutorial exists] Kida 0.8.0 changes the semantics of ?. and ?[...] on Mapping receivers: missing keys now short-circuit to None instead of raising UndefinedError under strict_undefined. This aligns ?. with the mental model every TS/Swift/JS user imports (and with Python's own dict.get() idiom), at the cost of one breaking semantic. Object-attribute strictness is preserved — ?.nickname on an object without that attribute still raises. ::: TL;DR {# v0.7.x — raises UndefinedError when key is missing #} {{ user?.nickname }} {# user = {} → UndefinedError #} {# v0.8.0 — returns None, renders as &quot;&quot; (like dict.get(&quot;nickname&quot;)) #} {{ user?.nickname }} {# user = {} → &quot;&quot; #} {# Object attribute access is unchanged — still strict #} {{ user?.nickname }} {# user = User() without .nickname → UndefinedError #} If this breaks code that relied on the v0.7 behavior (catching a missing dict key via ?.), the migration is one of: Drop the ?. and use strict access: &#123;&#123; user.nickname &#125;&#125; — raises on missing dict key, explicit. Use the get filter: &#123;&#123; user | get(\"nickname\") &#125;&#125; — same soft behavior as new ?., more explicit. Pin to 0.7: pip install 'kida-templates==0.7.*'. What changed Old rule (v0.7): receiver-only short-circuit ?. short-circuited only when the receiver was None. A missing key on a defined receiver raised. Expression Receiver v0.7 behavior user?.nickname None &quot;&quot; (short-circuit) user?.nickname {} UndefinedError user?.nickname User() (no attr) UndefinedError items?[5] [1, 2, 3] IndexError New rule (v0.8): Mapping-soft, object-strict Expression Receiver v0.8 behavior user?.nickname None &quot;&quot; (unchanged) user?.nickname {} &quot;&quot; (new — Mapping miss → None) user?.nickname User() (no attr) UndefinedError (unchanged, strict mode) items?[5] [1, 2, 3] UndefinedError (unchanged — Sequence out-of-range) cfg?[&quot;theme&quot;] {} &quot;&quot; (new — Mapping miss) cfg?[&quot;theme&quot;] MappingProxyType({}) &quot;&quot; (new) The dispatch rule: isinstance(obj, collections.abc.Mapping) decides. This covers dict, dict subclasses, MappingProxyType, ChainMap, and any user-defined Mapping ABC. __missing__ on dict subclasses is still honored (the slow path calls obj[key]). Why this change The v0.7 &quot;receiver-only&quot; rule was principled but out of step with every other language's optional-chaining mental model. TypeScript's foo?.bar returns undefined for a missing property — not because it short-circuits the access, but because JS treats missing properties as undefined. Swift similarly has no &quot;missing key raises&quot; concept. Users reaching for ?. in Kida expected TS/Swift semantics on dict-shaped data and got strict errors instead. The v0.8 split respects both intuitions: Dicts are schema-less (config, JSON, kwargs). Missing keys are expected. ?. → None matches dict.get(). Objects have schemas. A missing .nickname on a User object is almost always a typo. strict_undefined still catches it. You still get typo protection where it matters (object attributes, list out-of-range), with none of the ?? &quot;&quot; noise on every dict lookup. Migration Most code does not break If you were following the v0.7 &quot;recommended pattern&quot; of combining ?. with ?? (e.g. &#123;&#123; user?.nickname ?? \"\" &#125;&#125;), your code works unchanged in 0.8 — it just became less noisy in its intent. Code that relied on the raise Search for bare ?. on dict access without a ?? fallback: rg &#x27;\\?\\.[a-zA-Z_]+(?!\\s*\\?\\?)&#x27; templates/ For any hit where the receiver is a dict and you wanted a loud error on a missing key, change to strict access: {# Before (v0.7 — raised, perhaps intentionally) #} {{ user?.nickname }} {# v0.8 explicit strict access #} {{ user.nickname }} Pinning If you're not ready to upgrade: # pyproject.toml dependencies = [ &quot;kida-templates&gt;=0.7,&lt;0.8&quot;, ] Verification Your existing ?? &quot;fallback&quot; patterns still work. Your existing object-attr templates still raise on typos. The only surface-level difference you should see is: dict templates that previously threw under strict mode now render empty string. Run your test suite. If it passes, you're done.","excerpt":"Upgrade to 0.8 Guide for moving a codebase from Kida 0.7.x to 0.8.0. :::note[Why this tutorial exists] Kida 0.8.0 changes the semantics of ?. and ?[...] on Mapping receivers: missing keys now...","metadata":{"type":"doc","weight":14.0,"lang":"en","title":"Upgrade to 0.8","draft":false,"variant":"standard","keywords":["upgrade",0.8,"optional chaining","?.","Mapping","dict.get"],"tags":["migration","upgrade","tutorial","optional-chaining"],"description":"Optional chaining now treats missing Mapping keys like dict.get()","icon":"arrow-up-circle"},"section":"tutorials","tags":["migration","upgrade","tutorial","optional-chaining"],"word_count":624,"reading_time":3,"navigation":{"parent":"/kida/docs/tutorials/","prev":"/kida/docs/tutorials/migrate-from-jinja2/","next":"/kida/docs/tutorials/component-comparison/","related":["/kida/docs/tutorials/upgrade-to-v0.7/","/kida/docs/tutorials/component-comparison/","/kida/docs/tutorials/migrate-from-jinja2/","/kida/docs/get-started/coming-from-jinja2/","/kida/docs/tutorials/flask-integration/"]},"last_modified":"2026-04-24T18:49:55.672566","content_hash":"83f2c4c3621e76acf66e58f1f22468cc39d8c543fcca6b65b5c0ff94f70a0623","graph":{"nodes":[{"id":"8404419184678964017","label":"Upgrade to 0.8","url":"/kida/docs/tutorials/upgrade-to-v0.8/","type":"hub","tags":["migration","upgrade","tutorial","optional-chaining"],"incoming_refs":14,"outgoing_refs":5,"connectivity":19,"reading_time":3,"size":30,"color":"var(--graph-node-hub)","isCurrent":true},{"id":"7211771813129706274","label":"Flask Integration","url":"/kida/docs/tutorials/flask-integration/","type":"hub","tags":["tutorial","flask","web"],"incoming_refs":11,"outgoing_refs":6,"connectivity":17,"reading_time":2,"size":29,"color":"var(--graph-node-hub)"},{"id":"-2125691613972746557","label":"Refactor-Safe Templates","url":"/kida/docs/tutorials/refactor-safe-templates/","type":"regular","tags":["tutorial","refactoring","includes","aliases","relative-paths"],"incoming_refs":9,"outgoing_refs":6,"connectivity":15,"reading_time":7,"size":33,"color":"var(--graph-node-regular)"},{"id":"5707474523189967122","label":"Upgrade to 0.7","url":"/kida/docs/tutorials/upgrade-to-v0.7/","type":"regular","tags":["migration","upgrade","tutorial","strict-undefined"],"incoming_refs":9,"outgoing_refs":6,"connectivity":15,"reading_time":5,"size":32,"color":"var(--graph-node-regular)"},{"id":"7601377927222528406","label":"Jinja2 vs Kida: Components","url":"/kida/docs/tutorials/component-comparison/","type":"regular","tags":["tutorial","components","jinja2","migration"],"incoming_refs":9,"outgoing_refs":5,"connectivity":14,"reading_time":7,"size":33,"color":"var(--graph-node-regular)"},{"id":"-2061480945794108909","label":"Coming from Jinja2","url":"/kida/docs/get-started/coming-from-jinja2/","type":"regular","tags":["migration","jinja2"],"incoming_refs":7,"outgoing_refs":6,"connectivity":13,"reading_time":4,"size":30,"color":"var(--graph-node-regular)"},{"id":"-1564002565400625032","label":"First Project","url":"/kida/docs/get-started/first-project/","type":"regular","tags":["tutorial","quickstart"],"incoming_refs":7,"outgoing_refs":6,"connectivity":13,"reading_time":2,"size":29,"color":"var(--graph-node-regular)"},{"id":"-1614556202705980312","label":"Quickstart","url":"/kida/docs/get-started/quickstart/","type":"regular","tags":["quickstart","tutorial"],"incoming_refs":7,"outgoing_refs":6,"connectivity":13,"reading_time":2,"size":29,"color":"var(--graph-node-regular)"},{"id":"-2912552323781474479","label":"Tutorials","url":"/kida/docs/tutorials/","type":"regular","tags":["tutorials"],"incoming_refs":2,"outgoing_refs":11,"connectivity":13,"reading_time":1,"size":28,"color":"var(--graph-node-regular)"},{"id":"6168890416689244912","label":"Terminal Rendering","url":"/kida/docs/tutorials/terminal-rendering/","type":"regular","tags":["tutorial","terminal","cli"],"incoming_refs":6,"outgoing_refs":7,"connectivity":13,"reading_time":11,"size":36,"color":"var(--graph-node-regular)"},{"id":"6546376669811034733","label":"Migrate from Jinja2","url":"/kida/docs/tutorials/migrate-from-jinja2/","type":"regular","tags":["migration","jinja2","tutorial"],"incoming_refs":7,"outgoing_refs":5,"connectivity":12,"reading_time":3,"size":28,"color":"var(--graph-node-regular)"}],"edges":[{"source":"-2061480945794108909","target":"8404419184678964017","weight":2},{"source":"-1564002565400625032","target":"8404419184678964017","weight":1},{"source":"-1614556202705980312","target":"8404419184678964017","weight":1},{"source":"-2912552323781474479","target":"8404419184678964017","weight":1},{"source":"7601377927222528406","target":"8404419184678964017","weight":2},{"source":"7211771813129706274","target":"8404419184678964017","weight":2},{"source":"6546376669811034733","target":"8404419184678964017","weight":2},{"source":"-2125691613972746557","target":"8404419184678964017","weight":1},{"source":"6168890416689244912","target":"8404419184678964017","weight":1},{"source":"5707474523189967122","target":"8404419184678964017","weight":2}]},"chunks":[{"anchor":"","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8","title":"","level":1,"content":"Upgrade to 0.8 Guide for moving a codebase from Kida 0.7.x to 0.8.0. :::note[Why this tutorial exists] Kida 0.8.0 changes the semantics of ?. and ?[...] on Mapping receivers: missing keys now short-circuit to None instead of raising UndefinedError under strict_undefined. This aligns ?. with the mental model every TS/Swift/JS user imports (and with Python's own dict.get() idiom), at the cost of one breaking semantic. Object-attribute strictness is preserved — ?.nickname on an object without that attribute still raises. :::","content_hash":"1b08a927d9eafdddd63353503355ebb25f79663d7aa5250eb5fbc9e02eec2901"},{"anchor":"tldr","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#tldr","title":"TL;DR","level":2,"content":"TL;DR {# v0.7.x — raises UndefinedError when key is missing #} {{ user?.nickname }} {# user = {} → UndefinedError #} {# v0.8.0 — returns None, renders as \"\" (like dict.get(\"nickname\")) #} {{ user?.nickname }} {# user = {} → \"\" #} {# Object attribute access is unchanged — still strict #} {{ user?.nickname }} {# user = User() without .nickname → UndefinedError #} If this breaks code that relied on the v0.7 behavior (catching a missing dict key via ?.), the migration is one of: Drop the ?. and use strict access: {{ user.nickname }} — raises on missing dict key, explicit. Use the get filter: {{ user | get(\"nickname\") }} — same soft behavior as new ?., more explicit. Pin to 0.7: pip install 'kida-templates==0.7.*'.","content_hash":"6806673903caa0912f8b6cd05249c0cc3dceb8d7f1f1168d52651dfb1cbc2a93"},{"anchor":"what-changed","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#what-changed","title":"What changed","level":2,"content":"What changed","content_hash":"1bcbf1dcad4ffe4950cdae6b868f8a81fabe5f9da2d92abc3cef577e25fee639"},{"anchor":"old-rule-v07-receiver-only-short-circuit","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#old-rule-v07-receiver-only-short-circuit","title":"Old rule (v0.7): receiver-only short-circuit","level":3,"content":"Old rule (v0.7): receiver-only short-circuit ?. short-circuited only when the receiver was None. A missing key on a defined receiver raised. Expression Receiver v0.7 behavior user?.nickname None \"\" (short-circuit) user?.nickname {} UndefinedError user?.nickname User() (no attr) UndefinedError items?[5] [1, 2, 3] IndexError","content_hash":"f00004a14dcd5b9cba8c061dea1bb80f9a879fb760cdbc27b7aa85d4aefa1722"},{"anchor":"new-rule-v08-mapping-soft-object-strict","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#new-rule-v08-mapping-soft-object-strict","title":"New rule (v0.8): Mapping-soft, object-strict","level":3,"content":"New rule (v0.8): Mapping-soft, object-strict Expression Receiver v0.8 behavior user?.nickname None \"\" (unchanged) user?.nickname {} \"\" (new — Mapping miss → None) user?.nickname User() (no attr) UndefinedError (unchanged, strict mode) items?[5] [1, 2, 3] UndefinedError (unchanged — Sequence out-of-range) cfg?[\"theme\"] {} \"\" (new — Mapping miss) cfg?[\"theme\"] MappingProxyType({}) \"\" (new) The dispatch rule: isinstance(obj, collections.abc.Mapping) decides. This covers dict, dict subclasses, MappingProxyType, ChainMap, and any user-defined Mapping ABC. __missing__ on dict subclasses is still honored (the slow path calls obj[key]).","content_hash":"5c2f7384dca7ec3e98fd0f09845752688681138f889a4e52488d490d6ee71879"},{"anchor":"why-this-change","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#why-this-change","title":"Why this change","level":2,"content":"Why this change The v0.7 \"receiver-only\" rule was principled but out of step with every other language's optional-chaining mental model. TypeScript's foo?.bar returns undefined for a missing property — not because it short-circuits the access, but because JS treats missing properties as undefined. Swift similarly has no \"missing key raises\" concept. Users reaching for ?. in Kida expected TS/Swift semantics on dict-shaped data and got strict errors instead. The v0.8 split respects both intuitions: Dicts are schema-less (config, JSON, kwargs). Missing keys are expected. ?. → None matches dict.get(). Objects have schemas. A missing .nickname on a User object is almost always a typo. strict_undefined still catches it. You still get typo protection where it matters (object attributes, list out-of-range), with none of the ?? \"\" noise on every dict lookup.","content_hash":"5891e2cbd9244d09810e4674836ae00d118b617f81323186d4cb937226188450"},{"anchor":"migration","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#migration","title":"Migration","level":2,"content":"Migration","content_hash":"cc149190687e0c3fcc7a9ee1c83d2da2a0dbbd63a897daeb4f217f8f5b79388b"},{"anchor":"most-code-does-not-break","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#most-code-does-not-break","title":"Most code does not break","level":3,"content":"Most code does not break If you were following the v0.7 \"recommended pattern\" of combining ?. with ?? (e.g. {{ user?.nickname ?? \"\" }}), your code works unchanged in 0.8 — it just became less noisy in its intent.","content_hash":"e42b4e65f9b201f7531e2067e5151f8fa6257991b3e1e776fd4c49207ace3410"},{"anchor":"code-that-relied-on-the-raise","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#code-that-relied-on-the-raise","title":"Code that relied on the raise","level":3,"content":"Code that relied on the raise Search for bare ?. on dict access without a ?? fallback: rg '\\?\\.[a-zA-Z_]+(?!\\s*\\?\\?)' templates/ For any hit where the receiver is a dict and you wanted a loud error on a missing key, change to strict access: {# Before (v0.7 — raised, perhaps intentionally) #} {{ user?.nickname }} {# v0.8 explicit strict access #} {{ user.nickname }}","content_hash":"718119f906a29ef42446630d6654aba0c667a07655631277dd240fc37fb68587"},{"anchor":"pinning","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#pinning","title":"Pinning","level":3,"content":"Pinning If you're not ready to upgrade: # pyproject.toml dependencies = [ \"kida-templates>=0.7,<0.8\", ]","content_hash":"1b54dec78bfc08ba1906b2797a0865f8fa2722a1900fd8d3663eda67db2ae4b7"},{"anchor":"verification","anchor_url":"/kida/docs/tutorials/upgrade-to-v0.8#verification","title":"Verification","level":2,"content":"Verification Your existing ?? \"fallback\" patterns still work. Your existing object-attr templates still raise on typos. The only surface-level difference you should see is: dict templates that previously threw under strict mode now render empty string. Run your test suite. If it passes, you're done.","content_hash":"3bbc784abe95186c4810fcb14148c59d1235522cb819fd5b816aee3e041d995a"}]}