Skip to content

VNode

wybthon.vnode

vnode

Virtual node data structure and tree-building helpers.

This module defines VNode and the functions that build trees of them: h, Fragment, and hole. It has no browser or DOM dependencies, so VNode trees can be constructed and inspected anywhere CPython runs.

A reactive hole is a _hole VNode wrapping a reactive expression (an Accessor or a zero-argument function). The reconciler runs the expression inside its own render effect and patches only that region of the DOM when its dependencies change. Holes are created implicitly whenever a reactive expression appears in a child position, and explicitly with hole() when you want a key.

Example

Building a small subtree without a browser:

from wybthon import h, Fragment

view = h("section", {"class": "card"},
         h("h1", {}, "Hello"),
         Fragment(h("p", {}, "Body 1"), h("p", {}, "Body 2")))

Classes:

Name Description
VNode

Virtual node representing an element, text, component, fragment, or hole.

Functions:

Name Description
hole

Create a reactive hole explicitly, optionally with a key.

render_template

Render a t-string to text, calling reactive interpolations when call is true.

template_getter

Return a reactive expression rendering template, or None when it's static.

h

Create a VNode from a tag, props, and children.

Fragment

Group children without adding a wrapper element.

copy_vnode

Return a structural copy of an unmounted VNode tree (other values unchanged).

VNode

VNode(tag: str | Callable[..., Any] | None, props: PropsDict | None = None, children: list[Any] | None = None, key: str | int | None = None)

Virtual node representing an element, text, component, fragment, or hole.

Uses __slots__ for a compact layout. The renderer populates the internal attributes (el, subtree, render_effect, component_ctx, ns, _frag_end) when the VNode mounts.

Attributes:

Name Type Description
tag

Element tag name ("div"), a special tag ("_text", "_hole", "_fragment"), or a component.

props PropsDict

Mapping of prop names to values: attributes, event handlers, refs, and reactive bindings.

children list[Any]

Child VNodes (mixed values before normalization).

key

Optional stable identity for keyed reconciliation.

el int | None

Kernel node id once mounted (for fragments the start marker; for holes the end marker).

owner_scope Any

Optional reactive Owner to mount under. Set by list primitives so a cached row's effects belong to the row's scope rather than to the list's re-running effect.

to_text_vnode

to_text_vnode(value: Any) -> VNode

Convert an arbitrary value to a text VNode (None becomes "").

hole

hole(getter: Callable[[], Any], *, key: str | int | None = None) -> VNode

Create a reactive hole explicitly, optionally with a key.

Reactive expressions in child positions become holes automatically; reach for hole() when you need a stable key for keyed reconciliation inside a fragment, or to make a hole visually explicit.

The expression may return a VNode, a string, a list of either, or None.

Parameters:

Name Type Description Default
getter Callable[[], Any]

Zero-arg reactive expression evaluated inside its own render effect.

required
key str | int | None

Optional stable identity for keyed reconciliation.

None

Returns:

Type Description
VNode

A _hole VNode.

Example
div(hole(lambda: f"Hello, {name()}!"))
div(lambda: f"Hello, {name()}!")   # equivalent

render_template

render_template(template: Template, call: bool = True) -> str

Render a t-string to text, calling reactive interpolations when call is true.

template_getter

template_getter(template: Template) -> Callable[[], str] | None

Return a reactive expression rendering template, or None when it's static.

A t-string is reactive when at least one interpolation is an accessor or zero-argument function. The expression calls every reactive interpolation, so the whole string updates as one binding.

flatten_children

flatten_children(items: Iterable[Any]) -> list[Any]

Flatten nested child lists into a single list, dropping None entries.

normalize_children

normalize_children(children: list[Any]) -> list[VNode]

Normalize a mixed list of children into a flat list of VNodes.

  • VNode: kept as is; fragments are flattened into the parent list.
  • Reactive expression (accessor or zero-arg function): wrapped in a _hole VNode.
  • None, True, and False: skipped, so cond and Widget() renders nothing when the condition is false (as in SolidJS).
  • Anything else: coerced to a text VNode.

h

h(tag: str | Callable[..., Any] | None, props: PropsDict | None = None, *children: Any) -> VNode

Create a VNode from a tag, props, and children.

The low-level constructor behind the helpers in wybthon.html. Reactive expressions in child positions are wrapped as holes when the parent mounts; components receive their children verbatim through the children prop.

Parameters:

Name Type Description Default
tag str | Callable[..., Any] | None

An HTML tag name, a special tag ("_text", "_hole", "_fragment"), or a component.

required
props PropsDict | None

Mapping of prop names to values. May be None.

None
*children Any

Children to attach. Lists and tuples are flattened.

()

Returns:

Type Description
VNode

A new VNode.

Example
from wybthon import h

view = h("button", {"on_click": handle_click}, "Click me")

Fragment

Fragment(*args: Any) -> VNode

Group children without adding a wrapper element.

Fragments mount their children directly into the parent between two empty comment markers, so they never pollute selectors like :first-child or affect layout.

Parameters:

Name Type Description Default
*args Any

Children. Lists and tuples are flattened.

()

Returns:

Type Description
VNode

A _fragment VNode.

Example
Fragment(h1("Title"), p("Body text"))

copy_vnode

copy_vnode(value: Any) -> Any

Return a structural copy of an unmounted VNode tree (other values unchanged).

Mounting records DOM ids and normalized children on the VNodes it mounts, so a tree mounts once. Server rendering copies the root view for each render pass.

What's in this module

vnode defines the VNode data structure and the pure helpers that build trees of them. It has no browser dependency, so trees can be constructed and inspected anywhere CPython runs. A reactive hole is a _hole VNode wrapping an accessor or zero-arg function; the reconciler runs it in its own render effect and patches only that region when its reads change.

Name Description
VNode Element, text, component, fragment, or hole node; tag, props, children, key.
h Hyperscript constructor: h(tag, props, *children); components get children as the children prop.
Fragment Group children with no wrapper element.
hole Explicit reactive hole, optionally with a key.

Holes are created implicitly: any accessor or zero-arg callable in a child position becomes one, and so does a template string that interpolates one (t"Hello, {name}", a single hole for the whole string). Reach for hole() when you need a stable key or want the hole visually explicit.

VNode[...] sets children with item syntax and returns the node, so section(class_="card")[h1("Hello"), p("Body")] equals section(h1("Hello"), p("Body"), class_="card"). A component call supports the same syntax for its children prop.

A subtree mounted from a compiled shape keeps its VNodes, but only the root and its dynamic children get el assigned at mount; static descendants receive node ids lazily, when the reconciler first patches the subtree. Don't rely on .el of a static descendant.

from wybthon import Fragment, create_signal, h, hole

name, set_name = create_signal("Ada")

view = h(
    "section",
    {"class": "card"},
    h("h1", {}, "Hello, ", name),  # implicit hole
    hole(lambda: f"{len(name())} letters", key="count"),  # explicit hole with a key
    h("p", {}, t"Name: {name}"),  # t-string hole
    Fragment(h("p", {}, "Body 1"), h("p", {}, "Body 2")),
)

The wybthon.html helpers wrap h() with keyword props, so most application code writes section(h1("Hello, ", name), class_="card") instead.

See also