Skip to content

Accept a covariant Mapping of elements in ui.dictionary and .batch - #10653

Merged
kirangadhave merged 2 commits into
marimo-team:mainfrom
winklemad:fix/dictionary-batch-mapping-elements
Aug 31, 2026
Merged

kirangadhave merged 2 commits into
marimo-team:mainfrom
winklemad:fix/dictionary-batch-mapping-elements

Conversation

@winklemad

Copy link
Copy Markdown
Contributor

This pull request was authored by a coding agent.

📝 Summary

Closes #10631

dict is invariant, so typing the elements parameter as dict[str, UIElement[Any, Any]] makes static type checkers (mypy, Pyright/Pylance) reject valid subclass dictionaries — e.g. dict[str, mo.ui.checkbox] — even though they are perfectly fine at runtime. Users have to work around it with cast(dict[str, Any], elements).

Reproduction under mypy (marimo's CI type checker) on main:

import marimo as mo

checks: dict[str, mo.ui.checkbox] = {"a": mo.ui.checkbox()}
mo.ui.dictionary(checks)
# error: Argument 1 to "dictionary" has incompatible type
#        "dict[str, checkbox]"; expected "dict[str, UIElement[Any, Any]]"  [arg-type]
# note: "Dict" is invariant

Fix

The affected constructors only read elements — they immediately clone it into a fresh dict via validate_and_clone — so the parameter can be a read-only, covariant Mapping. Widened in:

  • ui.dictionary.__init__
  • the internal batch element __init__
  • validate_and_clone (the shared helper, per the maintainer's note on the issue)

validate_and_clone still returns a dict, and the internal _batch_base (which stores and mutates the mapping) keeps its dict type since it always receives the cloned dict. ui.array already uses the covariant Sequence, so it needs no change. Runtime behaviour is unchanged.

Verification

  • The mypy reproduction above goes from erroring to clean after the change.
  • mypy marimo reports no new errors in the changed files.
  • New regression tests build a ui.dictionary and call validate_and_clone from a read-only MappingProxyType (a non-dict Mapping), which fails if the parameter is ever narrowed back to dict.
  • test_dictionary.py + test_batch.py pass (11 tests); ruff format/ruff check clean.

📋 Pre-Review Checklist

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

@winklemad

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@vercel

vercel Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview Aug 28, 2026 3:56am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a static typing limitation in marimo’s UI container APIs by widening elements inputs from invariant dict[...] to covariant Mapping[...], allowing type checkers to accept dictionaries of UIElement subclasses (e.g. dict[str, ui.checkbox]) without casts, while preserving existing runtime behavior by cloning into a fresh dict.

Changes:

  • Widened elements parameter types to Mapping[...] in ui.dictionary, ui.batch, and the shared validate_and_clone helper (still returning a concrete dict).
  • Added regression tests ensuring read-only mappings (e.g. MappingProxyType) are accepted and cloned correctly.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/_plugins/ui/_impl/test_dictionary.py Adds coverage that ui.dictionary accepts and clones a read-only Mapping.
tests/_plugins/ui/_impl/test_batch.py Adds coverage that validate_and_clone accepts and clones a read-only Mapping.
marimo/_plugins/ui/_impl/dictionary.py Widens dictionary.__init__ elements typing to Mapping[...].
marimo/_plugins/ui/_impl/batch.py Widens validate_and_clone and batch.__init__ elements typing to Mapping[...].

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 102 to 106
self,
elements: dict[str, UIElement[Any, Any]],
elements: Mapping[str, UIElement[Any, Any]],
*,
label: str = "",
on_change: Callable[[dict[str, object]], None] | None = None,
Comment on lines 217 to 221
def __init__(
self,
html: Html,
elements: dict[str, UIElement[Any, Any]],
elements: Mapping[str, UIElement[Any, Any]],
on_change: Callable[[dict[str, object]], None] | None = None,

@kirangadhave kirangadhave left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @winklemad, I have a few change requests below. Also please rebase the branch from origin/main

Comment thread tests/_plugins/ui/_impl/test_batch.py Outdated
md("Example {thing}").batch(thing={"key": "value"}) # type: ignore


def test_validate_and_clone_accepts_readonly_mapping() -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test will run without any changes to batch.py because Python doesn't enforce annotations on runtime.

Typing only change requires a type-level test. See tests/_ast/test_app_typing.py for reference.

assert dictionary.value == {"a": 2, "b": "goodbye", "c": 1}


def test_dictionary_accepts_readonly_mapping() -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread marimo/_plugins/ui/_impl/batch.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring still refers to dict

Suggested change
elements (Mapping): A dict of the batched elements (clones of the originals).

Comment thread marimo/_plugins/ui/_impl/dictionary.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring still refers to dict

Suggested change
elements (Mapping): A dict of the wrapped elements (clones of the originals).

def test_dictionary_accepts_readonly_mapping() -> None:
# `elements` is typed as a covariant Mapping, so any read-only mapping of
# UI elements (not just a plain dict) must be accepted and cloned.
from types import MappingProxyType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import at module level similar to other imports

Comment thread tests/_plugins/ui/_impl/test_batch.py Outdated
Comment on lines +31 to +33
from types import MappingProxyType

from marimo._plugins.ui._impl.batch import validate_and_clone

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import at module level similar to other imports

Comment thread tests/_plugins/ui/_impl/test_batch.py Outdated
md("Example {thing}").batch(thing={"key": "value"}) # type: ignore


def test_validate_and_clone_accepts_readonly_mapping() -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move the test to bottom. it separates two similar tests.

@kirangadhave
kirangadhave marked this pull request as ready for review August 27, 2026 18:25
`dict` is invariant, so annotating the `elements` parameter as
`dict[str, UIElement[Any, Any]]` made type checkers reject valid subclass
dictionaries such as `dict[str, mo.ui.checkbox]`, forcing users to add
casts. The constructors only read `elements` (they clone it into a fresh
dict via `validate_and_clone`), so widen the parameter to a read-only
`Mapping` in `ui.dictionary`, the internal `batch` element, and
`validate_and_clone`. Runtime behaviour is unchanged.

Add regression tests that build from a read-only `MappingProxyType`.
- Add basedpyright type-level tests (TestDictionaryTyping) that fail on the old
  invariant dict[...] param and pass on the covariant Mapping[...] one, so the
  typing change is actually guarded (runtime tests can't enforce annotations)
- Update the batch/dictionary docstrings that still referred to dict
- Hoist the MappingProxyType / validate_and_clone imports to module scope
- Move the validate_and_clone test next to the other batch tests
@winklemad
winklemad force-pushed the fix/dictionary-batch-mapping-elements branch from 1d71f14 to ca31563 Compare August 28, 2026 03:55
@winklemad

Copy link
Copy Markdown
Contributor Author

Thanks @kirangadhave — addressed all of these in ca31563:

  • Type-level test (the load-bearing one): added TestDictionaryTyping in tests/_ast/test_app_typing.py, following the existing _check_pyright pattern. The two snippets error on the old invariant dict[...] param and pass on the covariant Mapping[...], so the typing change is now genuinely guarded — basedpyright even suggests "switching from dict to Mapping which is covariant" on the old signature. (You're right that the runtime tests pass either way.)
  • Docstrings: updated the elements entries in batch.py / dictionary.py that still said dict.
  • Imports: hoisted MappingProxyType and validate_and_clone to module scope in both test files.
  • Nit: moved the validate_and_clone test down next to the other batch tests.
  • Rebased on origin/main.

Let me know if you'd like anything else.

@kirangadhave kirangadhave added the bug Something isn't working label Aug 31, 2026
@kirangadhave
kirangadhave merged commit 07921bd into marimo-team:main Aug 31, 2026
40 of 41 checks passed

This branch was successfully deployed

1 active deployment
Preview — ca31563d Deployed Aug 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typing: ui.dictionary.__init__ uses invariant dict[str, UIElement] and rejects dict[str, ui.checkbox]

3 participants