2025, Nov 21 19:00

Fix Missing Reactive Values in Modular Shiny for Python Apps with expressify by Passing the Session Input

Learn why reactive inputs disappear in modular Shiny for Python apps using expressify, and how to fix it by passing the session input into module functions

Modularizing a Shiny for Python app is a good idea once the code grows beyond a single file. A common pitfall appears when using expressify and trying to reference reactive inputs from a separate module. The UI renders, static text shows up, but reactive values mysteriously don’t. The root cause is subtle: the input object you use inside the module is not the same input object bound to the running session.

Problem setup

The app is split into two files. The main file defines the navset and a basic control, while the secondary file contributes another tab with a numeric input and attempts to display its value. The display remains blank if you compute or reference values inside the same function.

Here is a minimal version that reproduces the issue.

main_app.py

import section_view
from shiny import ui
from shiny.express import input, ui

with ui.navset_tab(id="active_tab"):
    with ui.nav_panel("Welcome", value="home_view"):
        with ui.card():
            ui.input_selectize(
                "dummy", "Filler",
                ["list", "of", "items", "here"],
                multiple=False
            )
    section_view.build_other()

section_view.py

from shiny import render
from shiny.express import input, ui, expressify

@expressify
def build_other():
    with ui.nav_panel("Detached page", value="aux_view"):
        ui.input_numeric("num_val", "I need this inpt value!", 1, min=1, max=1000)
        @render.express
        def show_text():
            val = input.num_val()
            val

What’s actually wrong

The input used inside the module is a separate object from the one in the main app. The main app already has an input, and that session-bound input must be the source of truth everywhere. Importing input again inside the module yields a distinct object that the session doesn’t know about, so expressions that rely on it don’t produce output. You can confirm it by replacing the body of the render function with a static string; the string renders fine, but referencing the numeric value doesn’t. Depending on how the code is executed, this can even surface as an attribute access error on the wrong input object.

Fix

Stop importing input in the module. Instead, accept the main input as a parameter to the module function and use that value inside the render block. Then pass the session’s input from the main file into the module.

section_view.py

from shiny import render
from shiny.express import ui, expressify

@expressify
def build_other(inlet):
    with ui.nav_panel("Detached page", value="aux_view"):
        ui.input_numeric("num_val", "I need this input value!", 1, min=1, max=1000)
        @render.express
        def show_text():
            current = inlet.num_val()
            f"input value: {current}"

main_app.py

import section_view
from shiny import ui
from shiny.express import input, ui

with ui.navset_tab(id="active_tab"):
    with ui.nav_panel("Welcome", value="home_view"):
        with ui.card():
            ui.input_selectize(
                "dummy", "Filler",
                ["list", "of", "items", "here"],
                multiple=False
            )
    section_view.build_other(inlet=input)

Why this matters

When you split a Shiny app across files, it’s essential to keep the session-scoped reactive objects consistent. If a module silently constructs its own input, it becomes detached from the running session. The UI might render, static text might appear, but reactive display will not, because you’re reading from an object that the session doesn’t update. Passing the session’s input into the module keeps everything in sync while preserving the benefits of a clean, multi-file layout.

Takeaways

Keep your app modular, but route the correct input into each module that needs it. Avoid importing a fresh input inside secondary files. Instead, accept the input from the main app and use it in render.express to display user-driven values. This small wiring step prevents elusive no-output states and keeps your Shiny for Python app reactive and maintainable as it grows.