Skip to content

Core API

This section documents the core reactivity primitives and the application entry point.

pymiro

pymiro: Signal-native, declarative GUI framework for Python.

App

The main application entry point for pymiro.

This class sets up the Qt application, event loop, and theme, and mounts your root component into the main window.

Parameters:

Name Type Description Default
title str

The window title.

'pymiro'
width int

The initial window width.

800
height int

The initial window height.

600
dev bool

If True, enables hot-reloading and development logging.

False
theme str | Theme

The theme to use ("default", "dark", or a custom Theme object).

'default'
Example
from pymiro import App, component
from pymiro.components import Text

@component
def HelloWorld():
    return Text("Hello, world!")

if __name__ == "__main__":
    app = App(title="My App", width=400, height=300)
    app.run(HelloWorld)
Source code in pymiro/app.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
class App:
    """
    The main application entry point for pymiro.

    This class sets up the Qt application, event loop, and theme, and mounts
    your root component into the main window.

    Args:
        title: The window title.
        width: The initial window width.
        height: The initial window height.
        dev: If True, enables hot-reloading and development logging.
        theme: The theme to use ("default", "dark", or a custom Theme object).

    Example:
        ```python
        from pymiro import App, component
        from pymiro.components import Text

        @component
        def HelloWorld():
            return Text("Hello, world!")

        if __name__ == "__main__":
            app = App(title="My App", width=400, height=300)
            app.run(HelloWorld)
        ```
    """
    def __init__(
        self,
        title: str = "pymiro",
        width: int = 800,
        height: int = 600,
        dev: bool = False,
        theme: str | Theme = "default",
    ) -> None:
        self.title = title
        self.width = width
        self.height = height
        self.dev = dev
        self.renderer: QtRenderer | None = None
        self.current_tree: VNode | None = None
        self.logger = DevLogger(enabled=dev)
        self.reloader: HotReloader | None = None

        if isinstance(theme, str):
            self.theme = DARK_THEME if theme == "dark" else DEFAULT_THEME
        else:
            self.theme = theme

        if self.dev:
            install_error_handler()
            version = sys.version.split(" ")[0]
            print(f"  [pymiro] v0.1.0 | Python {version} | dev mode on")

    def run(self, component: Callable[[], VNode]) -> None:
        # 1. create QApplication
        app = QApplication.instance()
        if app is None:
            app = QApplication(sys.argv)

        ThemeProvider.set(self.theme)

        # 2. create main QMainWindow
        main_window = QMainWindow()
        main_window.setWindowTitle(self.title)
        main_window.resize(self.width, self.height)

        central_widget = QWidget()
        central_layout = QVBoxLayout()
        central_widget.setLayout(central_layout)
        main_window.setCentralWidget(central_widget)

        # 3. setup qasync event loop
        loop = qasync.QEventLoop(app)
        asyncio.set_event_loop(loop)

        # 4. setup renderer
        self.renderer = QtRenderer(root_widget=central_widget, logger=self.logger)

        if self.dev:
            self.logger.mount(component.__name__)
            self.reloader = HotReloader(
                root_component=component,
                reconciler_fn=lambda old, new: reconcile(old, new, logger=self.logger),
                renderer=self.renderer,
                watch_dir=".",
                logger=self.logger,
            )

        is_render_pending = False

        def render_cycle() -> None:
            nonlocal is_render_pending
            is_render_pending = False
            new_tree = component()
            patches = reconcile(self.current_tree, new_tree, logger=self.logger)
            if patches and self.renderer is not None:
                self.renderer.commit(patches)
            self.current_tree = new_tree
            if self.reloader:
                self.reloader.current_tree = new_tree

        def schedule_render() -> None:
            nonlocal is_render_pending
            if not is_render_pending:
                is_render_pending = True
                loop.call_soon_threadsafe(render_cycle)

        # initial render
        self._root_dispose = effect(schedule_render)

        if self.reloader:
            self.reloader.start()

        # Cleanup on exit
        if hasattr(app, "aboutToQuit"):
            app.aboutToQuit.connect(self._root_dispose)
            if self.reloader:
                app.aboutToQuit.connect(self.reloader.stop)

        main_window.show()

        # 6. start event loop
        with loop:
            loop.run_forever()

Computed

Bases: Protocol[T_co]

Protocol for a derived reactive value.

Source code in pymiro/core/signals.py
58
59
60
61
class Computed(Protocol[T_co]):
    """Protocol for a derived reactive value."""

    def __call__(self) -> T_co: ...

Signal

Bases: Protocol[T]

Protocol for a reactive signal.

Source code in pymiro/core/signals.py
50
51
52
53
54
55
class Signal(Protocol[T]):
    """Protocol for a reactive signal."""

    def __call__(self) -> T: ...
    def set(self, value: T) -> None: ...
    def peek(self) -> T: ...

batch

batch(fn: Callable[[], T]) -> T
batch() -> contextlib.AbstractContextManager[None]
batch(fn: Callable[[], T] | None = None) -> Any

Batch multiple signal updates into a single execution frame.

When you update multiple signals inside a batch, effects depending on those signals will only run once after the batch completes, preventing unnecessary intermediate renders.

Can be used as a context manager with batch(): or as a higher-order function batch(fn).

Parameters:

Name Type Description Default
fn Callable[[], T] | None

Optional function to run inside the batch.

None
Example
first = signal("John")
last = signal("Doe")

effect(lambda: print(f"Name: {first()} {last()}"))

with batch():
    first.set("Jane")
    last.set("Smith")
# Effect only prints "Name: Jane Smith" once
Source code in pymiro/core/signals.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def batch[T](fn: Callable[[], T] | None = None) -> Any:
    """
    Batch multiple signal updates into a single execution frame.

    When you update multiple signals inside a batch, effects depending on those
    signals will only run once after the batch completes, preventing unnecessary
    intermediate renders.

    Can be used as a context manager `with batch():` or as a higher-order
    function `batch(fn)`.

    Args:
        fn: Optional function to run inside the batch.

    Example:
        ```python
        first = signal("John")
        last = signal("Doe")

        effect(lambda: print(f"Name: {first()} {last()}"))

        with batch():
            first.set("Jane")
            last.set("Smith")
        # Effect only prints "Name: Jane Smith" once
        ```
    """
    if fn is not None:
        with _batch_cm():
            return fn()
    return _batch_cm()

component

component(fn: F) -> F

Wrap a function into a pymiro component.

Components are the building blocks of a pymiro UI. They are regular Python functions that return a VNode (like Text, Button, or VStack) and are wrapped in the @component decorator. This decorator enables the use of hooks (like use_signal or use_effect) within the function.

Parameters:

Name Type Description Default
fn F

The function returning a VNode.

required

Returns:

Type Description
F

The wrapped component function.

Example
from pymiro import component
from pymiro.components import Button, Text, VStack
from pymiro.core.hooks import use_signal

@component
def Counter():
    count, set_count = use_signal(0)

    return VStack(
        Text(lambda: f"Count: {count()}"),
        Button("Increment", on_click=lambda: set_count(count() + 1))
    )
Source code in pymiro/core/component.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def component[F: Callable[..., Any]](fn: F) -> F:
    """
    Wrap a function into a pymiro component.

    Components are the building blocks of a pymiro UI. They are regular Python
    functions that return a `VNode` (like `Text`, `Button`, or `VStack`) and
    are wrapped in the `@component` decorator. This decorator enables the use
    of hooks (like `use_signal` or `use_effect`) within the function.

    Args:
        fn: The function returning a VNode.

    Returns:
        The wrapped component function.

    Example:
        ```python
        from pymiro import component
        from pymiro.components import Button, Text, VStack
        from pymiro.core.hooks import use_signal

        @component
        def Counter():
            count, set_count = use_signal(0)

            return VStack(
                Text(lambda: f"Count: {count()}"),
                Button("Increment", on_click=lambda: set_count(count() + 1))
            )
        ```
    """

    @functools.wraps(fn)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        prev_is_in = _is_in_component.get()
        prev_comp = _current_component.get()
        prev_idx = _hook_index.get()

        try:
            states = _component_states.get()
        except LookupError:
            states = {}
            _component_states.set(states)

        _is_in_component.set(True)
        _current_component.set(fn)
        _hook_index.set(0)

        if fn not in states:
            states[fn] = []

        disposers: list[Callable[[], None]] = []
        token_disp = _current_disposers.set(disposers)

        try:
            result = fn(*args, **kwargs)
        finally:
            _is_in_component.set(prev_is_in)
            _current_component.set(prev_comp)
            _hook_index.set(prev_idx)
            _current_disposers.reset(token_disp)

        if not isinstance(result, VNode):
            raise ComponentError(f"Component {fn.__name__} must return a VNode")

        # Add cleanup to VNode or store it somewhere? Wait!
        # The prompt says: "Effect cleanup: are use_effect disposers actually called on component unmount, or are they registered but never invoked?"
        # Where can we store the disposers?
        # We can store them in a special field on VNode or in a global registry keyed by the node_id.
        # But wait! We don't have node_id in the component!
        # What if we store the disposers in a closure in the VNode's props?
        # A hidden prop `__disposers__`!
        if disposers:
            result.props["__disposers__"] = disposers

        return result

    setattr(wrapper, "__pymiro_component__", True)
    return cast(F, wrapper)

computed

computed(fn: Callable[[], T]) -> Computed[T]

Create a reactive computed value derived from other signals.

A computed value automatically tracks which signals it reads. When those signals change, the computed value marks itself as dirty and lazily re-evaluates only when its value is read again.

Parameters:

Name Type Description Default
fn Callable[[], T]

A function that computes a value based on signals.

required

Returns:

Name Type Description
Computed Computed[T]

A computed object. Call it to read the derived value.

Example
first = signal("Hello")
last = signal("World")

full_name = computed(lambda: f"{first()} {last()}")
print(full_name())  # "Hello World"

first.set("Hi")
print(full_name())  # "Hi World"
Source code in pymiro/core/signals.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def computed[T](fn: Callable[[], T]) -> Computed[T]:
    """
    Create a reactive computed value derived from other signals.

    A computed value automatically tracks which signals it reads. When those
    signals change, the computed value marks itself as dirty and lazily
    re-evaluates only when its value is read again.

    Args:
        fn: A function that computes a value based on signals.

    Returns:
        Computed: A computed object. Call it to read the derived value.

    Example:
        ```python
        first = signal("Hello")
        last = signal("World")

        full_name = computed(lambda: f"{first()} {last()}")
        print(full_name())  # "Hello World"

        first.set("Hi")
        print(full_name())  # "Hi World"
        ```
    """
    return _Computed(fn)

effect

effect(fn: Callable[[], None]) -> Callable[[], None]

Create a side effect that automatically re-runs when dependencies change.

The function is run immediately to determine its dependencies. Whenever any of the signals read during execution change, the function runs again.

Parameters:

Name Type Description Default
fn Callable[[], None]

A function containing side effects (e.g., printing, DOM updates).

required

Returns:

Name Type Description
Callable Callable[[], None]

A disposer function. Call it to stop the effect and clean up its subscriptions.

Example
count = signal(0)

# Runs immediately and prints "Count is 0"
dispose = effect(lambda: print(f"Count is {count()}"))

count.set(1)  # Automatically prints "Count is 1"

dispose()  # Stop listening
count.set(2)  # Nothing prints
Source code in pymiro/core/signals.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def effect(fn: Callable[[], None]) -> Callable[[], None]:
    """
    Create a side effect that automatically re-runs when dependencies change.

    The function is run immediately to determine its dependencies. Whenever any
    of the signals read during execution change, the function runs again.

    Args:
        fn: A function containing side effects (e.g., printing, DOM updates).

    Returns:
        Callable: A disposer function. Call it to stop the effect and clean up
            its subscriptions.

    Example:
        ```python
        count = signal(0)

        # Runs immediately and prints "Count is 0"
        dispose = effect(lambda: print(f"Count is {count()}"))

        count.set(1)  # Automatically prints "Count is 1"

        dispose()  # Stop listening
        count.set(2)  # Nothing prints
        ```
    """
    e = _Effect(fn)
    return e.dispose

signal

signal(value: T) -> Signal[T]

Create a reactive signal holding a value.

A signal is the core reactive primitive. It holds a value and notifies subscribers (like effects or computed values) whenever the value changes.

Parameters:

Name Type Description Default
value T

The initial value of the signal.

required

Returns:

Name Type Description
Signal Signal[T]

A signal object. Call it my_signal() to read the value, or my_signal.set(new_value) to update it.

Example
count = signal(0)
print(count())  # 0

count.set(1)
print(count())  # 1
Source code in pymiro/core/signals.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def signal[T](value: T) -> Signal[T]:
    """
    Create a reactive signal holding a value.

    A signal is the core reactive primitive. It holds a value and notifies
    subscribers (like effects or computed values) whenever the value changes.

    Args:
        value: The initial value of the signal.

    Returns:
        Signal: A signal object. Call it `my_signal()` to read the value,
            or `my_signal.set(new_value)` to update it.

    Example:
        ```python
        count = signal(0)
        print(count())  # 0

        count.set(1)
        print(count())  # 1
        ```
    """
    return _Signal(value)