|
| 1 | +--- |
| 2 | +title: Upgrade to 0.8 |
| 3 | +description: Optional chaining now treats missing Mapping keys like dict.get() |
| 4 | +draft: false |
| 5 | +weight: 14 |
| 6 | +lang: en |
| 7 | +type: doc |
| 8 | +tags: |
| 9 | +- migration |
| 10 | +- upgrade |
| 11 | +- tutorial |
| 12 | +- optional-chaining |
| 13 | +keywords: |
| 14 | +- upgrade |
| 15 | +- 0.8 |
| 16 | +- optional chaining |
| 17 | +- ?. |
| 18 | +- Mapping |
| 19 | +- dict.get |
| 20 | +icon: arrow-up-circle |
| 21 | +--- |
| 22 | + |
| 23 | +# Upgrade to 0.8 |
| 24 | + |
| 25 | +Guide for moving a codebase from Kida 0.7.x to 0.8.0. |
| 26 | + |
| 27 | +:::note[Why this tutorial exists] |
| 28 | +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. |
| 29 | +::: |
| 30 | + |
| 31 | +## TL;DR |
| 32 | + |
| 33 | +```kida |
| 34 | +{# v0.7.x — raises UndefinedError when key is missing #} |
| 35 | +{{ user?.nickname }} {# user = {} → UndefinedError #} |
| 36 | +
|
| 37 | +{# v0.8.0 — returns None, renders as "" (like dict.get("nickname")) #} |
| 38 | +{{ user?.nickname }} {# user = {} → "" #} |
| 39 | +
|
| 40 | +{# Object attribute access is unchanged — still strict #} |
| 41 | +{{ user?.nickname }} {# user = User() without .nickname → UndefinedError #} |
| 42 | +``` |
| 43 | + |
| 44 | +If this breaks code that **relied on** the v0.7 behavior (catching a missing dict key via `?.`), the migration is one of: |
| 45 | + |
| 46 | +1. Drop the `?.` and use strict access: `{{ user.nickname }}` — raises on missing dict key, explicit. |
| 47 | +2. Use the `get` filter: `{{ user | get("nickname") }}` — same soft behavior as new `?.`, more explicit. |
| 48 | +3. Pin to 0.7: `pip install 'kida-templates==0.7.*'`. |
| 49 | + |
| 50 | +## What changed |
| 51 | + |
| 52 | +### Old rule (v0.7): receiver-only short-circuit |
| 53 | + |
| 54 | +`?.` short-circuited only when the receiver was `None`. A missing key on a defined receiver raised. |
| 55 | + |
| 56 | +| Expression | Receiver | v0.7 behavior | |
| 57 | +|---|---|---| |
| 58 | +| `user?.nickname` | `None` | `""` (short-circuit) | |
| 59 | +| `user?.nickname` | `{}` | `UndefinedError` | |
| 60 | +| `user?.nickname` | `User()` (no attr) | `UndefinedError` | |
| 61 | +| `items?[5]` | `[1, 2, 3]` | `IndexError` | |
| 62 | + |
| 63 | +### New rule (v0.8): Mapping-soft, object-strict |
| 64 | + |
| 65 | +| Expression | Receiver | v0.8 behavior | |
| 66 | +|---|---|---| |
| 67 | +| `user?.nickname` | `None` | `""` (unchanged) | |
| 68 | +| `user?.nickname` | `{}` | **`""` (new — Mapping miss → `None`)** | |
| 69 | +| `user?.nickname` | `User()` (no attr) | `UndefinedError` (unchanged, strict mode) | |
| 70 | +| `items?[5]` | `[1, 2, 3]` | `UndefinedError` (unchanged — Sequence out-of-range) | |
| 71 | +| `cfg?["theme"]` | `{}` | **`""` (new — Mapping miss)** | |
| 72 | +| `cfg?["theme"]` | `MappingProxyType({})` | **`""` (new)** | |
| 73 | + |
| 74 | +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]`). |
| 75 | + |
| 76 | +## Why this change |
| 77 | + |
| 78 | +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. |
| 79 | + |
| 80 | +The v0.8 split respects both intuitions: |
| 81 | + |
| 82 | +- **Dicts are schema-less** (config, JSON, kwargs). Missing keys are expected. `?.` → `None` matches `dict.get()`. |
| 83 | +- **Objects have schemas.** A missing `.nickname` on a `User` object is almost always a typo. `strict_undefined` still catches it. |
| 84 | + |
| 85 | +You still get typo protection where it matters (object attributes, list out-of-range), with none of the `?? ""` noise on every dict lookup. |
| 86 | + |
| 87 | +## Migration |
| 88 | + |
| 89 | +### Most code does not break |
| 90 | + |
| 91 | +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. |
| 92 | + |
| 93 | +### Code that relied on the raise |
| 94 | + |
| 95 | +Search for bare `?.` on dict access without a `??` fallback: |
| 96 | + |
| 97 | +```bash |
| 98 | +rg '\?\.[a-zA-Z_]+(?!\s*\?\?)' templates/ |
| 99 | +``` |
| 100 | + |
| 101 | +For any hit where the receiver is a dict and you *wanted* a loud error on a missing key, change to strict access: |
| 102 | + |
| 103 | +```kida |
| 104 | +{# Before (v0.7 — raised, perhaps intentionally) #} |
| 105 | +{{ user?.nickname }} |
| 106 | +
|
| 107 | +{# v0.8 explicit strict access #} |
| 108 | +{{ user.nickname }} |
| 109 | +``` |
| 110 | + |
| 111 | +### Pinning |
| 112 | + |
| 113 | +If you're not ready to upgrade: |
| 114 | + |
| 115 | +```toml |
| 116 | +# pyproject.toml |
| 117 | +dependencies = [ |
| 118 | + "kida-templates>=0.7,<0.8", |
| 119 | +] |
| 120 | +``` |
| 121 | + |
| 122 | +## Verification |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | +Run your test suite. If it passes, you're done. |
0 commit comments