posit-dev / posit-dev/py-shiny

[Bug]: `shiny.module` re-exports are stripped from generated type stubs, so Pyright rejects them in Shinylive

Open
#2,425 0 comments 0 reactions 1 assignee View on GitHub

@jat255 is already working on this.

Since Aug 7, 2026.

bug needs-triage Priority: Low
Dominant language
Python
Stars
1.8k
Forks
135
Avg merge
2d 18h
Merged PRs (30d)
21

Description

Summary

In the Shinylive editor, these imports are flagged as errors even though the code runs correctly:

from shiny.module import resolve_id        # "resolve_id" is unknown import symbol
from shiny.module import current_namespace # "current_namespace" is unknown import symbol
from shiny.module import ResolvedId        # "ResolvedId" is unknown import symbol

This came up while working on some doc changes in https://github.com/posit-dev/py-shiny-site/pull/436. For example:

Image

shiny/module.py re-exports these names from shiny._namespaces but never references them in
its own body. pyright --createstub drops imports that aren't referenced, so the generated
shiny/module.pyi still advertises the names in __all__ while never importing them. Shinylive
bundles those stubs for its in-browser Pyright (via scripts/create_typeshed.py in the shinylive
repo), so the documented import surfaces a spurious error.

This is editor-only. Runtime behavior is correct.

Evidence

The stub actually shipped in the shinylive 0.10.14 assets
(shinylive/pyright/typeshed.en.json/src/typings/shiny/module.pyi):

from typing import Callable, TYPE_CHECKING, TypeVar
from ._docstring import add_example
from ._namespaces import Id                     # ← only Id survived
from ._typing_extensions import Concatenate, ParamSpec
from .session import Inputs, Outputs, Session

__all__ = ("current_namespace", "resolve_id", "ui", "server", "ResolvedId")

shiny/_namespaces.pyi in the same bundle defines resolve_id, current_namespace, and
ResolvedId correctly — only the re-export hop through shiny/module.py is lost.

Status of every name in shiny.module.__all__:

Name Stub status Why
current_namespace broken re-export only
resolve_id broken re-export only
ResolvedId broken re-export only
ui OK defined in module.py
server OK defined in module.py

Id survives only because of the existing workaround at shiny/module.py:23-24:

# Ensure that Id type is not stripped out from .pyi file when generating type stubs
_: Id  # type: ignore

which suggests this was hit once already and patched for a single symbol.

Reproduction

Any Shinylive example using the documented import reproduces it. Minimal standalone repro of the
stub generation, with a package mirroring module.py's re-export pattern:

# mypkg/module.py
__all__ = ("current_namespace", "resolve_id", "ui", "server", "ResolvedId")
from ._namespaces import Id, ResolvedId, current_namespace, namespace_context, resolve_id
_: Id  # type: ignore

def ui(fn): ...
pyright --createstub mypkg && cat typings/mypkg/module.pyi

Output keeps only from ._namespaces import Id; the other three names are dropped while __all__
still lists them.

Options tested (pyright 1.1.411)

Approach Result
Current form (plain import + __all__) Reproduces the bug; only Id retained
PEP 484 redundant alias — import resolve_id as resolve_id Regresses — entire import line dropped, Id lost too
Annotation probe — _r: ResolvedId Retains type symbols
Module-level assignment — _keep = (resolve_id, ...) Does not retain functions (RHS elided to ...)
Defining thin wrappers in module.py Emits real def resolve_id(...) / def current_namespace(...)

Worth calling out: the redundant-alias form is the idiomatic PEP 484 re-export fix and is the
natural first thing to reach for, but it makes this worse under --createstub.

Suggested fix

Annotations retain type symbols; they can't retain functions, and assignments don't either. So the
two categories need different handling in shiny/module.py:

  1. ResolvedId — extend the existing annotation workaround:
    _r: ResolvedId  # type: ignore
    
  2. resolve_id / current_namespace — define thin delegating wrappers so createstub emits
    real function signatures:
    def resolve_id(id: Id) -> ResolvedId:
        """Resolve an ID, possibly with a module namespace."""
        return _namespaces.resolve_id(id)
    

Both were verified to produce a correct stub. A wrapper adds one call of indirection; if that's
unwanted, the alternative is fixing it on the shinylive side (post-process the generated stubs, or
don't use --createstub for shiny) — happy to move this issue there if that's the better venue.

Impact

from shiny.module import resolve_id is the documented public API for making custom components and
hand-written HTML ids module-aware (see the "Custom JavaScript component" docs page and the modules
article). Anyone following those docs in Shinylive gets a red squiggle on correct code.

Environment

  • shiny 1.7.1.dev6 (py-shiny f85a444); the pattern is unchanged in current main
  • shinylive JS assets 0.10.14 (via shinylive PyPI 0.8.11)
  • pyright 1.1.411
  • Runtime unaffected — apps using these imports run correctly

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.