Writing robust callbacks#
Notebook hooks are powerful, but they can also be the fastest way to create “weird notebook states” if callbacks are slow, crash, or mutate data unexpectedly.
This page gives battle-tested patterns for writing callbacks that behave well for:
wet-lab users (copy/paste; predictable behavior),
computational users (large data; heavy analysis),
developers (threading, debugging, and failure isolation).
Key rule (read this once)#
Hook callbacks can run on the server’s request-handling thread.
Practical meaning:
keep callbacks fast and defensive,
avoid doing heavy analysis directly inside the callback,
treat hook payloads as untrusted and validate keys and types.
Fast path: safe callback template (copy/paste)#
import logging
logger = logging.getLogger("cellucid.hooks")
@viewer.on_selection
def on_selection(event):
cells = event["cells"]
if type(cells) is not list or any(type(index) is not int for index in cells):
raise TypeError("selection cells must be a list of native integers")
logger.info("Selection: %d cells", len(cells))
Note
Callback exceptions propagate through event delivery. The server logs the exception and answers that event request with HTTP 500. Catch only an error that the callback can handle completely; otherwise let it surface.
Practical patterns#
Pattern A: “Do the minimum in the callback, do the work elsewhere”#
Use a queue:
from queue import Queue
selection_queue: Queue[list[int]] = Queue()
@viewer.on_selection
def enqueue_selection(event):
cells = event["cells"]
if cells:
selection_queue.put(cells)
Then in another cell (or later in the notebook), consume:
cells = selection_queue.get() # blocks
subset = adata[cells].copy()
subset
Why this is robust:
callbacks stay fast,
heavy work happens in normal notebook execution order,
you control debouncing and cancellation.
Pattern B: debouncing selection events#
Some UI workflows may emit multiple selection events during an interaction. You can debounce in Python:
import time
last_at = 0.0
@viewer.on_selection
def on_selection_debounced(event):
global last_at
now = time.time()
if now - last_at < 0.25: # 250ms
return
last_at = now
print("Selection:", len(event["cells"]))
Pattern C: treat hover as a debounced state stream#
Hover is throttled and can drop events under load. Avoid heavy work in hover hooks:
@viewer.on_hover
def on_hover(event):
cell = event["cell"]
if cell is None:
return
# Good: lightweight UI feedback / logging
# Bad: expensive plotting, model inference, large AnnData slicing
Pattern D: prefer wait_for_event for deterministic workflows#
If you want a predictable “do X after the next selection” flow, it can be clearer to block:
viewer.wait_for_ready(timeout=60)
event = viewer.wait_for_event("selection", timeout=None)
cells = event["cells"]
This avoids callback threading concerns entirely.
Deep path: threading and AnnData pitfalls#
Backed .h5ad and thread safety#
When you run
show_anndata("data.h5ad", dataset_name="My dataset", dataset_id="my-dataset"),
AnnData is opened read-only-backed.
Depending on your stack (h5py/hdf5), concurrent access from multiple threads can be unsafe.
Recommendations:
avoid slicing backed AnnData inside hook callbacks,
if you must, serialize access with a lock,
or copy the needed arrays eagerly.
Don’t block the server thread#
If your callback blocks for seconds:
events can queue up behind it,
the UI may feel “laggy” or “stuck” from the user’s perspective.
Move long work out of the callback (queue pattern above).
Logging: make failures visible#
To see Cellucid hook/log output:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("cellucid.jupyter").setLevel(logging.INFO)
logging.getLogger("cellucid.server").setLevel(logging.INFO)
If you are debugging deeply, use DEBUG.
Edge cases checklist#
event["cells"]can be empty (user cleared selection).event["cell"]can beNone(not hovering).Use the documented fields for the exact event type and validate their native JSON-derived types before analysis.
Very large selections can hit the
/_cellucid/eventssize limit (see Frontend → Python events).
Troubleshooting#
Symptoms and fixes:
“My callback never runs” → check connectivity; run
viewer.debug_connection(); see Troubleshooting (hooks).“My callback request returns HTTP 500” → read the logged callback exception and fix it; the failure is not swallowed.
“Notebook becomes sluggish” → your callback is doing too much; move work out of it.
Next steps#
Viewer state +
wait_for_event: Viewer state and wait_for_eventFull troubleshooting: Troubleshooting (hooks)