Kida 0.8.0
Optional chaining soft-lands on Mappings, plus relative/alias template paths, component validation diagnostics, and a stability gate
Released 2026-04-24.
Kida 0.8.0 refines the v0.7 strict-by-default story with one focused semantic fix, then layers on the first batch of pre-1.0 stabilization work:?. / ?[...] now soften on Mappings, template references get refactor-safe ./, ../, and @alias/ forms, component call validation surfaces structured K-CMP-* diagnostics, and the compile hot path is noticeably faster. A new stability gate (make verify-stability) makes the release discipline runnable locally.
Breaking
-
?.and?[...]soften on Mapping receivers — Optional chaining now short-circuits missing keys toNoneon anycollections.abc.Mappingreceiver (dict,MappingProxyType,ChainMap, dict subclasses), mirroring Python'sdict.get(key)idiom and the TS/Swift optional-chaining mental model. Object attribute misses on non-Mapping receivers still raiseUndefinedErrorunderstrict_undefined; Sequence out-of-range on?[i]also still raises in strict mode.Migration: if you were relying on
?.to raise on a missing dict key (the v0.7 behavior), drop the?.and use strict access{{ user.nickname }}, or use thegetfilter{{ user | get("nickname") }}. Code using the recommended{{ x?.y ?? "" }}pattern is unaffected.See [
docs/tutorials/upgrade-to-v0.8.md](
).
Added
Template references stop being path-coupled
- Relative paths —
{% include %},{% extends %},{% embed %}, and{% from ... import ... %}now accept./and../paths that resolve against the calling template's directory.{% include "./_card.html" %}frompages/about.htmlresolves topages/_card.html, and the same template works unchanged after movingpages/anywhere in the tree. Absolute names render byte-identical to before; path-traversal protection still fires on walks above every loader search root. - Namespace aliases —
Environment(template_aliases={"components": "ui/components", "layouts": "ui/layouts"})maps short@name/prefixes to root-relative subtrees so cross-cutting component libraries stop being path-coupled. Templates can write{% include "@components/card.html" %}regardless of where the caller lives. Unknown aliases raiseTemplateNotFoundErrorwith the list of configured aliases. kida check --lint-fragile-paths— Opt-in lint rule that flags cross-template references whose target lives in the same folder as the caller (e.g.{% include "pages/card.html" %}frompages/about.html) and suggests the refactor-safe./card.htmlform. References that already use./,../, or@alias/are ignored.- Refactor-safe templates tutorial —
docs/tutorials/refactor-safe-templates.mdwalks through when to use.//../vs@alias/, plus a step-by-step migration recipe usingripgrep.
Component validation diagnostics
ComponentWarning+K-CMP-001/K-CMP-002— Component call validation now carries a dedicatedComponentWarningclass (exported fromkida) and structured error codes (COMPONENT_CALL_SIGNATURE,COMPONENT_TYPE_MISMATCH) so downstream filters can target component contract violations specifically.- Cross-template call validation — Literal
{% from "card.html" import card %}followed by{% call card(...) %}validates arguments against the imported def's metadata, not just locally-defined defs.
Optional-chaining upgrade kit
- v0.8 upgrade tutorial —
docs/tutorials/upgrade-to-v0.8.mddocuments the?./?[...]Mapping-soft semantics, the rationale (schema-less dicts vs. schema-ful objects), and the migration path. - v0.7 upgrade tutorial —
docs/tutorials/upgrade-to-v0.7.mdcollects thestrict_undefined=Truemigration patterns: TL;DR, three fix patterns (is defined,??,| default), escape hatch, and null-safe idioms. Cross-linked fromREADME.md, tutorials index, and theUndefinedErrortroubleshooting page. - Null-safe hint on
UndefinedError— Attribute/key errors now include a secondHint:line pointing users atx?.y,x?.y ?? "", andx | get("y", ''). Wording reflects 0.8 semantics (x?.yalone is sufficient on Mappings). - Docs: optional chaining in
docs/syntax/variables.md— Dedicated?.and?[...]subsections with the Mapping-vs-object rule and a pointer to the v0.8 upgrade tutorial. getitem_preserve_none/strict_getitem_preserve_none— Runtime helpers powering?[...]. The compiler routes?[key]through these helpers instead of emitting direct subscript, giving Mapping-soft behavior under strict mode with no impact on None-receiver short-circuit.
Agent UX
- K-PAR-001 end-tag errors point at
kida check— Orphan{% end %}, mismatched closing tags, and typed-end mismatches now append a tip telling the reader to runkida check <templates-dir> --strictto surface every mismatch across the directory at once. Agents doing bulk template edits in downstream repos were discovering these one rendered route at a time.
Stability gate
make verify-stability— Runs lint, format,ty, full pytest with coverage gate, GIL-disabled render/sandbox/concurrency safety tests, wheel/sdist build, and a clean-venv package smoke test (scripts/package_smoke.py).make verify-rcremains an alias. Newdocs/stability-gate.mddocuments the pre-1.0 checklist.- Public API snapshot + diagnostics contract tests —
tests/test_public_api_snapshot.pyandtests/test_diagnostics_contract.pylock down the exported surface andErrorCodecoverage so accidental renames/removals fail loudly. - Core benchmark regression probes —
benchmarks/test_benchmark_regression_core.pyplus a newscripts/benchmark_suites.shwrap the compile/render/stream/inherited-blocks/concurrent suites with consistent thresholds (fail on >5% compile/render/stream regression, >10% concurrency).
Changed
- Component call validation emits structured
K-CMP-*diagnostics — Existing call-signature and type-mismatch warnings now carryK-CMP-001/K-CMP-002codes and are filtered asComponentWarningrather than genericUserWarning.
Performance
- Compile hot path trimmed — Reduced per-template overhead in
compiler/core.pyand slimmed generated async compile output. The compiler hot path is the primary pytest-benchmark baseline going forward. - Dict attribute fast path —
compiler/expressions.pyandtemplate/core.pyavoid redundant casts on Mapping attribute access. - Render scaffold bypass — Render accumulator and scope management skip context-manager overhead on the fast path.
- Sandbox output-limit cache —
max_output_sizelookup is cached per-template rather than re-read on every render.
Fixed
from_string()withoutname=warning dedups per-source — TheUserWarningfired when a bytecode cache is configured andfrom_string()is called withoutname=previously fired on every call (~1000× per test suite downstream). It now fires once per distinct source perEnvironment, keyed bybytecode_cache.hash_source(source). First emission and text unchanged; twoEnvironmentinstances warn independently; editing the template source re-arms the warning.
Upgrade Notes
- Dict-shaped data gets cleaner —
{{ config?.theme }}onconfig={}used to raise; it now renders"". Any template that relied on the raise (rare) should switch to strict accessThemeSection(name='default', default_appearance='light', default_palette='brown-bengal', features=('navigation.breadcrumbs', 'navigation.toc', 'navigation.toc.sticky', 'navigation.prev_next', 'navigation.back_to_top', 'content.code.copy', 'content.reading_time', 'graph.contextual', 'search', 'search.suggest', 'search.highlight', 'accessibility.skip_link'), show_reading_time=True, show_author=True, show_prev_next=True, show_children_default=True, show_excerpts_default=True, max_tags_display=10, popular_tags_count=20)or{{ config | get("theme") }}. - Object attributes are unchanged —
{{ user?.nickname }}on aUser()instance without.nicknamestill raises in strict mode. That's the typo-catching win we're keeping. ?[i]on lists is unchanged — Sequence out-of-range still raises in strict mode. Off-by-one on a list is almost always a bug.- Component warnings now filterable — Downstream code that did
warnings.simplefilter("ignore", UserWarning)to silence component contract warnings should narrow toComponentWarning(or theK-CMP-*codes via the error-code API) to keep other signals intact. - Move templates freely — New
./,../, and@alias/forms mean movingpages/no longer cascades into a grep-replace job.kida check --lint-fragile-pathsfinds existing same-folder-by-absolute-path references so you can migrate incrementally. - Pin if needed —
pip install 'kida-templates==0.7.*'to stay on v0.7 semantics.
Why this change
The v0.7 "receiver-only short-circuit" rule for?. was principled but out of step with every other language's mental model. TypeScript's foo?.bar returns undefined for missing property access — not because it "short-circuits the access," but because JS itself 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 at once:
- Dicts are schema-less (config, JSON, kwargs). Missing keys are expected.
?.→Nonematchesdict.get(). - Objects have schemas. A missing
.nicknameon aUserobject is almost always a typo. Strict mode still catches it.
You get typo protection where it matters (object attributes, list out-of-range), without the?? ""noise on every dict lookup.