You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

marimo

Package Overview
Dependencies
Maintainers
2
Versions
376
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

marimo - pypi Package Compare versions

Comparing version
0.20.1
to
0.20.2
+62
marimo/_runtime/cell_output_list.py
# Copyright 2026 Marimo. All rights reserved.
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from marimo._output.hypertext import Html
class CellOutputList:
"""Thread-safe container for a cell's imperatively constructed output."""
def __init__(self) -> None:
self._items: list[Html] = []
self._lock = threading.RLock()
def append(self, item: Html) -> None:
with self._lock:
self._items.append(item)
def clear(self) -> None:
with self._lock:
self._items.clear()
def replace_at_index(self, item: Html, idx: int) -> None:
with self._lock:
if idx > len(self._items):
raise IndexError(
f"idx is {idx}, must be <= {len(self._items)}"
)
if idx == len(self._items):
self._items.append(item)
else:
self._items[idx] = item
def remove(self, value: object) -> None:
with self._lock:
self._items[:] = [
item for item in self._items if item is not value
]
def stack(self) -> Html | None:
"""Return `vstack` of the items, or `None` if empty."""
with self._lock:
if self._items:
from marimo._plugins.stateless.flex import vstack
return vstack(self._items)
return None
def __bool__(self) -> bool:
with self._lock:
return bool(self._items)
def __len__(self) -> int:
with self._lock:
return len(self._items)
def __repr__(self) -> str:
with self._lock:
return f"CellOutputList({self._items!r})"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# ]
# ///
# Copyright 2026 Marimo. All rights reserved.
import marimo
__generated_with = "0.15.5"
app = marimo.App()
@app.cell
def _():
import marimo as mo
import time
return mo, time
@app.cell
def _(mo):
sleep_time_radio = mo.ui.radio(
[".01", ".1", "1"], label="Sleep time", value=".01"
)
sleep_time_radio
return (sleep_time_radio,)
@app.cell
def _(sleep_time_radio):
sleep_time = float(sleep_time_radio.value)
return (sleep_time,)
@app.cell
def _(mo):
disabled_switch = mo.ui.switch(label="Disable progress bar")
disabled_switch
return (disabled_switch,)
@app.cell
def _(disabled_switch, mo, sleep_time, time):
for _ in mo.status.progress_bar(
range(10),
title="Loading",
subtitle="Please wait",
disabled=disabled_switch.value,
):
time.sleep(sleep_time)
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
for _ in mo.status.progress_bar(
range(10),
title="Loading",
subtitle="Please wait",
show_eta=True,
show_rate=True,
disabled=disabled_switch.value,
):
time.sleep(sleep_time)
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
with mo.status.progress_bar(
title="Loading",
subtitle="Please wait",
total=10,
disabled=disabled_switch.value,
) as bar:
for _ in range(10):
time.sleep(sleep_time)
bar.update()
return
@app.cell
def _(mo, sleep_time, time):
with mo.status.spinner(title="Loading...", remove_on_exit=True) as _spinner:
time.sleep(0.1)
_spinner.update("Almost done")
time.sleep(sleep_time)
return
@app.cell
def _(mo, sleep_time, time):
with mo.status.spinner(title="Loading...", remove_on_exit=True) as _spinner:
time.sleep(sleep_time)
_spinner.update("Almost done")
time.sleep(sleep_time)
mo.ui.table([1, 2, 3])
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
# Fast updates should be debounced for performance
for i in mo.status.progress_bar(
range(1000),
disabled=disabled_switch.value,
):
time.sleep(sleep_time / 10)
return
if __name__ == "__main__":
app.run()
import marimo
__generated_with = "0.20.1"
app = marimo.App()
@app.cell
def _():
import marimo as mo
import time
import threading
return mo, threading, time
@app.cell
def _(mo, threading, time):
def append():
for i in range(3):
mo.output.append(f"{i}: Hello from {threading.get_ident()}")
time.sleep(1)
return (append,)
@app.cell
def _(mo, threading, time):
def replace():
for i in range(3):
mo.output.replace(f"{i}: Hello from {threading.get_ident()}")
time.sleep(1)
return (replace,)
@app.cell
def _(mo):
def run_threads(fn):
_threads = [mo.Thread(target=fn) for _ in range(3)]
for _t in _threads:
_t.start()
for _t in _threads:
_t.join()
return (run_threads,)
@app.cell
def _(append, run_threads):
run_threads(append)
return
@app.cell
def _(replace, run_threads):
run_threads(replace)
return
if __name__ == "__main__":
app.run()
import marimo
__generated_with = "0.20.1"
app = marimo.App()
@app.cell
def _():
import marimo as mo
import random
import time
import threading
return mo, random, threading, time
@app.cell
def _(mo, random, threading, time):
def step(pbar: mo.status.progress_bar, work: int):
for _ in range(work):
# Sleep... or anything else that releases GIL
time.sleep(random.uniform(0.5, 1))
pbar.update(
subtitle=f"work completed by thread {threading.get_ident()}"
)
return (step,)
@app.cell
def _(mo, random, step, time):
total = 30
with mo.status.progress_bar(total=total) as pbar:
n_threads = 4
work = total // n_threads
remainder = total % n_threads
threads = [
mo.Thread(target=step, args=(pbar, work))
for _ in range(n_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
for _ in range(remainder):
time.sleep(random.uniform(0.5, 1))
pbar.update(subtitle="work completed by main thread")
return
if __name__ == "__main__":
app.run()
import marimo
__generated_with = "0.17.7"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import random
return mo, random
@app.cell
def _(mo):
g, s = mo.state(0)
g(), s(1)
return g, s
@app.cell
def _(g, random):
g(), (x := random.randint(0, 10))
return (x,)
@app.cell
def _(x):
x
return
@app.cell
def _(s):
s(6)
return
@app.cell
def _(mo):
def f(s):
import random
import time
thread = mo.current_thread()
while not thread.should_exit:
s(random.randint(0, 100000))
time.sleep(1)
return (f,)
@app.cell
def _(f, mo, s):
mo.Thread(target=f, args=(s,)).start()
return
@app.cell
def _(g):
g()
return
if __name__ == "__main__":
app.run()
import marimo
__generated_with = "0.15.5"
app = marimo.App()
@app.cell
def _():
import marimo as mo
return (mo,)
@app.function
def foo():
print("hi")
@app.cell
def _():
import threading
return (threading,)
@app.cell
def _(mo, threading):
with mo.redirect_stdout():
threading.Thread(target=foo).start()
return
@app.cell
def _(mo):
with mo.redirect_stdout():
mo.Thread(target=foo).start()
return
if __name__ == "__main__":
app.run()

Sorry, the diff of this file is too big to display

+15
-12
# Copyright 2026 Marimo. All rights reserved.
from __future__ import annotations
import threading
import time

@@ -58,2 +59,3 @@ from collections.abc import AsyncIterable, Iterable, Sized

self.start_time = time.time()
self._lock = threading.Lock()
super().__init__(self._get_text())

@@ -67,3 +69,3 @@

) -> None:
"""Update the progress indicator.
"""Update the progress indicator. Thread-safe.

@@ -88,14 +90,15 @@ Examples:

"""
if self.closed:
raise RuntimeError(
"Progress indicators cannot be updated after exiting "
"the context manager that created them. "
)
self.current += increment
if title is not None:
self.title = title
if subtitle is not None:
self.subtitle = subtitle
with self._lock:
if self.closed:
raise RuntimeError(
"Progress indicators cannot be updated after exiting "
"the context manager that created them. "
)
self.current += increment
if title is not None:
self.title = title
if subtitle is not None:
self.subtitle = subtitle
self._text = self._get_text()
self._text = self._get_text()
self.debounced_flush()

@@ -102,0 +105,0 @@

@@ -138,3 +138,3 @@ # Copyright 2026 Marimo. All rights reserved.

buf = io.BytesIO()
figure.savefig(buf, format="png")
figure.savefig(buf, format="png", dpi=figure.get_dpi(), bbox_inches=None)
buf.seek(0)

@@ -208,14 +208,3 @@ encoded = base64.b64encode(buf.read()).decode("utf-8")

fig_width_px, fig_height_px = _figure_pixel_size(figure)
# Axes pixel bounds: [left, top, right, bottom]
# relative to the full figure image
bbox = axes.get_position()
axes_pixel_bounds: list[float] = [
bbox.x0 * fig_width_px, # left
(1 - bbox.y1) * fig_height_px, # top
bbox.x1 * fig_width_px, # right
(1 - bbox.y0) * fig_height_px, # bottom
]
# Validate scales first (fail fast, no side effects)
_SUPPORTED_SCALES = ("linear", "log")

@@ -235,2 +224,16 @@ x_scale = axes.get_xscale()

# Render the figure first — savefig triggers the draw which
# finalizes layout (tight_layout, constrained_layout, etc.)
chart_base64 = _figure_to_base64(figure)
# Now capture axes position — reflects post-layout bounds
fig_width_px, fig_height_px = _figure_pixel_size(figure)
bbox = axes.get_position()
axes_pixel_bounds: list[float] = [
bbox.x0 * fig_width_px, # left
(1 - bbox.y1) * fig_height_px, # top
bbox.x1 * fig_width_px, # right
(1 - bbox.y0) * fig_height_px, # bottom
]
super().__init__(

@@ -241,3 +244,3 @@ component_name=matplotlib.name,

args={
"chart-base64": _figure_to_base64(figure),
"chart-base64": chart_base64,
"x-bounds": list(axes.get_xlim()),

@@ -244,0 +247,0 @@ "y-bounds": list(axes.get_ylim()),

@@ -93,11 +93,6 @@ # Copyright 2026 Marimo. All rights reserved.

"""Update the app's cached outputs."""
from marimo._plugins.stateless.flex import vstack
del ctx
if (
run_result.output is None
and run_result.accumulated_output is not None
):
self.outputs[cell.cell_id] = vstack(
run_result.accumulated_output
if run_result.output is None and run_result.accumulated_output:
self.outputs[cell.cell_id] = (
run_result.accumulated_output.stack()
)

@@ -104,0 +99,0 @@ else:

@@ -12,3 +12,3 @@ # Copyright 2026 Marimo. All rights reserved.

from contextlib import contextmanager
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Optional

@@ -18,2 +18,3 @@

from marimo._messaging.context import HTTP_REQUEST_CTX
from marimo._runtime.cell_output_list import CellOutputList

@@ -31,3 +32,2 @@ if TYPE_CHECKING:

from marimo._messaging.types import Stderr, Stdout, Stream
from marimo._output.hypertext import Html
from marimo._plugins.ui._core.registry import UIElementRegistry

@@ -73,4 +73,4 @@ from marimo._runtime import dataflow

local_cell_id: Optional[CellId_t] = None
# output object set imperatively
output: Optional[list[Html]] = None
# outputs set imperatively via mo.output.append and associated functions
output: CellOutputList = field(default_factory=CellOutputList)
duckdb_connection: duckdb.DuckDBPyConnection | None = None

@@ -77,0 +77,0 @@

@@ -9,3 +9,2 @@ # Copyright 2026 Marimo. All rights reserved.

from marimo._output.rich_help import mddoc
from marimo._plugins.stateless.flex import vstack
from marimo._runtime.context import get_context

@@ -46,6 +45,7 @@ from marimo._runtime.context.types import ContextNotInitializedError

return
elif value is None:
ctx.execution_context.output = None
else:
ctx.execution_context.output = [formatting.as_html(value)]
output = ctx.execution_context.output
output.clear()
if value is not None:
output.append(formatting.as_html(value))
write_internal(cell_id=ctx.execution_context.cell_id, value=value)

@@ -71,15 +71,10 @@

if ctx.execution_context is None or ctx.execution_context.output is None:
if ctx.execution_context is None:
return
elif idx > len(ctx.execution_context.output):
raise IndexError(
f"idx is {idx}, must be <= {len(ctx.execution_context.output)}"
)
elif idx == len(ctx.execution_context.output):
ctx.execution_context.output.append(formatting.as_html(value))
else:
ctx.execution_context.output[idx] = formatting.as_html(value)
output = ctx.execution_context.output
output.replace_at_index(formatting.as_html(value), idx)
write_internal(
cell_id=ctx.execution_context.cell_id,
value=vstack(ctx.execution_context.output),
value=output.stack(),
)

@@ -106,9 +101,7 @@

if ctx.execution_context.output is None:
ctx.execution_context.output = [formatting.as_html(value)]
else:
ctx.execution_context.output.append(formatting.as_html(value))
output = ctx.execution_context.output
output.append(formatting.as_html(value))
write_internal(
cell_id=ctx.execution_context.cell_id,
value=vstack(ctx.execution_context.output),
value=output.stack(),
)

@@ -133,7 +126,4 @@

if ctx.execution_context.output is not None:
value = vstack(ctx.execution_context.output)
else:
value = None
write_internal(cell_id=ctx.execution_context.cell_id, value=value)
output = ctx.execution_context.output
write_internal(cell_id=ctx.execution_context.cell_id, value=output.stack())

@@ -148,8 +138,6 @@

if ctx.execution_context is None or ctx.execution_context.output is None:
if ctx.execution_context is None or not ctx.execution_context.output:
return
output = [
item for item in ctx.execution_context.output if item is not value
]
ctx.execution_context.output = output if output else None
output = ctx.execution_context.output
output.remove(value)
flush()

@@ -331,6 +331,6 @@ # Copyright 2026 Marimo. All rights reserved.

# 1. if run_result.output is not None, need to send it
# 2. otherwise if exc_ctx.output is None, then need to send
# 2. otherwise if accumulated_output is empty, then need to send
# the (empty) output (to clear it)
should_send_output = (
run_result.output is not None or run_result.accumulated_output is None
run_result.output is not None or not run_result.accumulated_output
)

@@ -337,0 +337,0 @@ if (

@@ -10,2 +10,3 @@ # Copyright 2026 Marimo. All rights reserved.

from marimo._runtime.cell_lifecycle_item import CellLifecycleItem
from marimo._runtime.cell_output_list import CellOutputList
from marimo._runtime.context.kernel_context import KernelRuntimeContext

@@ -151,2 +152,10 @@ from marimo._runtime.context.script_context import ScriptRuntimeContext

pass
output = CellOutputList()
if self._marimo_ctx is not None:
if (exec_ctx := self._marimo_ctx.execution_context) is not None:
# Share the parent's CellOutputList so appends from threads
# are visible to the main execution context.
output = exec_ctx.output
if isinstance(self._marimo_ctx, KernelRuntimeContext):

@@ -156,2 +165,3 @@ self._marimo_ctx.execution_context = ExecutionContext(

setting_element_value=False,
output=output,
)

@@ -158,0 +168,0 @@ thread_id = threading.get_ident()

@@ -5,3 +5,3 @@ # Copyright 2026 Marimo. All rights reserved.

__generated_with = "0.15.5"
__generated_with = "0.20.1"
app = marimo.App()

@@ -13,2 +13,3 @@

import marimo as mo
return (mo,)

@@ -20,2 +21,3 @@

import time
return (time,)

@@ -35,2 +37,3 @@

time.sleep(.01)
return loop_append, loop_replace

@@ -41,3 +44,5 @@

def _(mo):
mo.md("""### Replace""")
mo.md("""
### Replace
""")
return

@@ -62,3 +67,5 @@

def _(mo):
mo.md("""### Append""")
mo.md("""
### Append
""")
return

@@ -83,3 +90,5 @@

def _(mo):
mo.md("""### Clear""")
mo.md("""
### Clear
""")
return

@@ -106,3 +115,5 @@

def _(mo):
mo.md("""### Sleep (stale)""")
mo.md("""
### Sleep (stale)
""")
return

@@ -109,0 +120,0 @@

import marimo
__generated_with = "0.19.11"
__generated_with = "0.20.1"
app = marimo.App()

@@ -42,12 +42,8 @@

@app.cell
def _(embedding, mnist, plt):
def _(embedding, mnist, plt, pymde):
x = embedding[:, 0]
y = embedding[:, 1]
plt.scatter(x=x, y=y, s=0.05, cmap="Spectral", c=mnist.attributes["digits"])
plt.yticks([-2, 0, 2])
plt.xticks([-2, 0, 2])
plt.ylim(-2.5, 2.5)
plt.xlim(-2.5, 2.5)
ax = plt.gca()
ax = pymde.plot(X=embedding, color_by=mnist.attributes["digits"])
plt.tight_layout()
return ax, x, y

@@ -78,2 +74,67 @@

mo.md("""
## Edge-data test
Points are clustered right at the axes edges. Clicking on tick labels,
axis labels, or the title should **not** start a selection. Previously
the click was clamped to the nearest edge, which would select these
edge points via `get_mask()`.
""")
return
@app.cell
def _(np, plt):
rng = np.random.default_rng(99)
# Points hugging the four edges of [0, 10] x [0, 10]
edge_n = 30
edge_x = np.concatenate([
rng.uniform(0, 0.3, edge_n), # left edge
rng.uniform(9.7, 10, edge_n), # right edge
rng.uniform(0, 10, edge_n), # bottom edge
rng.uniform(0, 10, edge_n), # top edge
rng.uniform(3, 7, edge_n), # centre (control)
])
edge_y = np.concatenate([
rng.uniform(0, 10, edge_n), # left edge
rng.uniform(0, 10, edge_n), # right edge
rng.uniform(0, 0.3, edge_n), # bottom edge
rng.uniform(9.7, 10, edge_n), # top edge
rng.uniform(3, 7, edge_n), # centre (control)
])
plt.figure()
plt.scatter(edge_x, edge_y, s=20, c=edge_y, cmap="viridis")
plt.colorbar(label="y value")
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.xlabel("X axis (click here should NOT select)")
plt.ylabel("Y axis (click here should NOT select)")
plt.title("Title area (click here should NOT select)")
edge_ax = plt.gca()
return edge_ax, edge_x, edge_y
@app.cell
def _(edge_ax, mo):
edge_fig = mo.ui.matplotlib(edge_ax)
edge_fig
return (edge_fig,)
@app.cell
def _(edge_fig, edge_x, edge_y):
_m = edge_fig.value.get_mask(edge_x, edge_y)
f"Selected {_m.sum()} / {len(edge_x)} points"
return
@app.cell
def _(edge_fig):
edge_fig.value if edge_fig.value else "No selection!"
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## Log-scale axes test

@@ -80,0 +141,0 @@ """)

@@ -69,3 +69,3 @@ <!DOCTYPE html>

<title>{{ title }}</title>
<script type="module" crossorigin src="./assets/index-DEPjuBkG.js"></script>
<script type="module" crossorigin src="./assets/index-xckvhXGM.js"></script>
<link rel="modulepreload" crossorigin href="./assets/preload-helper-reX6CfMN.js">

@@ -72,0 +72,0 @@ <link rel="modulepreload" crossorigin href="./assets/clsx-nlQpVU_5.js">

@@ -142,6 +142,6 @@ # Copyright 2026 Marimo. All rights reserved.

mo.md(r"""
/// Tip | "Data sources panel"
/// Tip | "Variables panel"
Click the database "barrel" icon in the left toolbar to see all dataframes and in-
memory tables that your notebook has access to.
Open the variables panel in the left toolbar to see all dataframes
and in-memory tables that your notebook has access to.
///

@@ -148,0 +148,0 @@ """)

Metadata-Version: 2.3
Name: marimo
Version: 0.20.1
Version: 0.20.2
Summary: A library for making reactive notebooks and apps

@@ -5,0 +5,0 @@ License:

@@ -7,3 +7,3 @@ [build-system]

name = "marimo"
version = "0.20.1"
version = "0.20.2"
description = "A library for making reactive notebooks and apps"

@@ -10,0 +10,0 @@ # We try to keep dependencies to a minimum, to avoid conflicts with

import marimo
__generated_with = "0.17.7"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import random
return mo, random
@app.cell
def _(mo):
g, s = mo.state(0)
g(), s(1)
return g, s
@app.cell
def _(g, random):
g(), (x := random.randint(0, 10))
return (x,)
@app.cell
def _(x):
x
return
@app.cell
def _(s):
s(6)
return
@app.cell
def _(mo):
def f(s):
import random
import time
thread = mo.current_thread()
while not thread.should_exit:
s(random.randint(0, 100000))
time.sleep(1)
return (f,)
@app.cell
def _(f, mo, s):
mo.Thread(target=f, args=(s,)).start()
return
@app.cell
def _(g):
g()
return
if __name__ == "__main__":
app.run()
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# ]
# ///
# Copyright 2026 Marimo. All rights reserved.
import marimo
__generated_with = "0.15.5"
app = marimo.App()
@app.cell
def _():
import marimo as mo
import time
return mo, time
@app.cell
def _(mo):
sleep_time_radio = mo.ui.radio(
[".01", ".1", "1"], label="Sleep time", value=".01"
)
sleep_time_radio
return (sleep_time_radio,)
@app.cell
def _(sleep_time_radio):
sleep_time = float(sleep_time_radio.value)
return (sleep_time,)
@app.cell
def _(mo):
disabled_switch = mo.ui.switch(label="Disable progress bar")
disabled_switch
return (disabled_switch,)
@app.cell
def _(disabled_switch, mo, sleep_time, time):
for _ in mo.status.progress_bar(
range(10),
title="Loading",
subtitle="Please wait",
disabled=disabled_switch.value,
):
time.sleep(sleep_time)
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
for _ in mo.status.progress_bar(
range(10),
title="Loading",
subtitle="Please wait",
show_eta=True,
show_rate=True,
disabled=disabled_switch.value,
):
time.sleep(sleep_time)
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
with mo.status.progress_bar(
title="Loading",
subtitle="Please wait",
total=10,
disabled=disabled_switch.value,
) as bar:
for _ in range(10):
time.sleep(sleep_time)
bar.update()
return
@app.cell
def _(mo, sleep_time, time):
with mo.status.spinner(title="Loading...", remove_on_exit=True) as _spinner:
time.sleep(0.1)
_spinner.update("Almost done")
time.sleep(sleep_time)
return
@app.cell
def _(mo, sleep_time, time):
with mo.status.spinner(title="Loading...", remove_on_exit=True) as _spinner:
time.sleep(sleep_time)
_spinner.update("Almost done")
time.sleep(sleep_time)
mo.ui.table([1, 2, 3])
return
@app.cell
def _(disabled_switch, mo, sleep_time, time):
# Fast updates should be debounced for performance
for i in mo.status.progress_bar(
range(1000),
disabled=disabled_switch.value,
):
time.sleep(sleep_time / 10)
return
if __name__ == "__main__":
app.run()
import marimo
__generated_with = "0.15.5"
app = marimo.App()
@app.cell
def _():
import marimo as mo
return (mo,)
@app.function
def foo():
print("hi")
@app.cell
def _():
import threading
return (threading,)
@app.cell
def _(mo, threading):
with mo.redirect_stdout():
threading.Thread(target=foo).start()
return
@app.cell
def _(mo):
with mo.redirect_stdout():
mo.Thread(target=foo).start()
return
if __name__ == "__main__":
app.run()

Sorry, the diff of this file is too big to display