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 to None on any collections.abc.Mapping receiver (dict, MappingProxyType, ChainMap, dict subclasses), mirroring Python's dict.get(key) idiom and the TS/Swift optional-chaining mental model. Object attribute misses on non-Mapping receivers still raise UndefinedError under strict_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 the get filter {{ user | get("nickname") }}. Code using the recommended {{ x?.y ?? "" }}pattern is unaffected.

    See [docs/tutorials/upgrade-to-v0.8.md](

    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" %} from pages/about.html resolves to pages/_card.html, and the same template works unchanged after moving pages/anywhere in the tree. Absolute names render byte-identical to before; path-traversal protection still fires on walks above every loader search root.
  • Namespace aliasesEnvironment(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 raise TemplateNotFoundErrorwith 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" %} from pages/about.html) and suggests the refactor-safe ./card.html form. References that already use ./, ../, or @alias/are ignored.
  • Refactor-safe templates tutorialdocs/tutorials/refactor-safe-templates.md walks through when to use ./ / ../ vs @alias/, plus a step-by-step migration recipe using ripgrep.

Component validation diagnostics

  • ComponentWarning + K-CMP-001 / K-CMP-002 — Component call validation now carries a dedicated ComponentWarning class (exported from kida) 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 tutorialdocs/tutorials/upgrade-to-v0.8.md documents the ?. / ?[...]Mapping-soft semantics, the rationale (schema-less dicts vs. schema-ful objects), and the migration path.
  • v0.7 upgrade tutorialdocs/tutorials/upgrade-to-v0.7.md collects the strict_undefined=True migration patterns: TL;DR, three fix patterns (is defined, ??, | default), escape hatch, and null-safe idioms. Cross-linked from README.md, tutorials index, and the UndefinedErrortroubleshooting page.
  • Null-safe hint onUndefinedError — Attribute/key errors now include a second Hint: line pointing users at x?.y, x?.y ?? "", and x | get("y", ''). Wording reflects 0.8 semantics (x?.yalone is sufficient on Mappings).
  • Docs: optional chaining indocs/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 atkida check — Orphan {% end %}, mismatched closing tags, and typed-end mismatches now append a tip telling the reader to run kida 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-rc remains an alias. New docs/stability-gate.mddocuments the pre-1.0 checklist.
  • Public API snapshot + diagnostics contract teststests/test_public_api_snapshot.py and tests/test_diagnostics_contract.py lock down the exported surface and ErrorCodecoverage so accidental renames/removals fail loudly.
  • Core benchmark regression probesbenchmarks/test_benchmark_regression_core.py plus a new scripts/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 structuredK-CMP-* diagnostics — Existing call-signature and type-mismatch warnings now carry K-CMP-001 / K-CMP-002 codes and are filtered as ComponentWarning rather than generic UserWarning.

Performance

  • Compile hot path trimmed — Reduced per-template overhead incompiler/core.pyand slimmed generated async compile output. The compiler hot path is the primary pytest-benchmark baseline going forward.
  • Dict attribute fast pathcompiler/expressions.py and template/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 cachemax_output_sizelookup is cached per-template rather than re-read on every render.

Fixed

  • from_string() without name= warning dedups per-source — The UserWarning fired when a bytecode cache is configured and from_string() is called without name= previously fired on every call (~1000× per test suite downstream). It now fires once per distinct source per Environment, keyed by bytecode_cache.hash_source(source). First emission and text unchanged; two Environmentinstances warn independently; editing the template source re-arms the warning.

Upgrade Notes

  1. Dict-shaped data gets cleaner{{ config?.theme }} on config={} used to raise; it now renders "". Any template that relied on the raise (rare) should switch to strict access ThemeSection(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") }}.
  2. Object attributes are unchanged{{ user?.nickname }} on a User() instance without .nicknamestill raises in strict mode. That's the typo-catching win we're keeping.
  3. ?[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.
  4. Component warnings now filterable — Downstream code that didwarnings.simplefilter("ignore", UserWarning) to silence component contract warnings should narrow to ComponentWarning (or the K-CMP-*codes via the error-code API) to keep other signals intact.
  5. Move templates freely — New./, ../, and @alias/ forms mean moving pages/ 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.
  6. Pin if neededpip 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.?.None matches dict.get().
  • Objects have schemas. A missing.nickname on a Userobject 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.