Skip to content

fix(renderable): KeyError in @gr.render when inner gr.Examples pops a fake-event fn#13344

Open
GopalGB wants to merge 1 commit intogradio-app:mainfrom
GopalGB:gb-fix/render-fns-keyerror-12081
Open

fix(renderable): KeyError in @gr.render when inner gr.Examples pops a fake-event fn#13344
GopalGB wants to merge 1 commit intogradio-app:mainfrom
GopalGB:gb-fix/render-fns-keyerror-12081

Conversation

@GopalGB
Copy link
Copy Markdown

@GopalGB GopalGB commented Apr 30, 2026

Repro

Closes #12081.

```python
import gradio as gr

with gr.Blocks() as demo:
with gr.Tab(key="tab", label="Tab"):
dropdown = gr.Dropdown(["a", "b", "c"])

    @gr.render(inputs=[dropdown])
    def render(value):
        text = gr.Textbox(key="text", label="Text")
        gr.Examples(
            examples=["a1", "a2", "a3"],
            example_labels=["First", "Second", "Third"],
            inputs=[text],
        )

demo.launch()
```

Selecting any value in the dropdown raised:

```
File ".../gradio/renderable.py", line 89, in apply
if blocks_config.fns[fn._id].render_iteration != self.render_iteration:
KeyError: 3
```

(Removing either the `gr.Tab` wrapper OR the `example_labels` argument made the crash go away — both signals point to the same code path.)

Root cause

`Renderable.apply` (`gradio/renderable.py`) snapshots `fns_from_last_render` before running the user render function (lines 68–71), then walks the snapshot to clean up stale fns:

```python
for fn in fns_from_last_render:
if blocks_config.fns[fn._id].render_iteration != self.render_iteration:
del blocks_config.fns[fn._id]
```

When the inner render contains a `gr.Examples`, `Examples` transiently appends and then pops a "fake_event" function (`gradio/helpers.py:580`) for cache-on-launch processing. After this, `blocks_config.fns` no longer has an entry for that id — but the snapshot still does, and `blocks_config.fns[fn._id]` raises `KeyError`.

Fix

The cleanup intent is "drop stale fns from the previous iteration" — an already-absent entry is in the desired state. Switching the lookup to `.get(fn._id)` and `continue`-ing on `None` removes the crash without changing the cleanup outcome:

```python
for fn in fns_from_last_render:
current_fn = blocks_config.fns.get(fn._id)
if current_fn is None:
continue
if current_fn.render_iteration != self.render_iteration:
del blocks_config.fns[fn._id]
```

Test

New file `test/test_renderable.py` adds `test_render_apply_does_not_raise_keyerror_when_fns_are_popped` which deterministically reproduces the bug without spinning up an HTTP queue:

  1. Build the block layout from the issue (`Tab > render > Examples`).
  2. Capture the `Renderable` that `@gr.render` registered.
  3. Inject a stub `BlockFunction` whose `rendered_in` matches the renderable, model `gr.Examples` popping it mid-render.
  4. Call `renderable.apply("a")`.

Without the fix this test fails at `gradio/renderable.py:89` with `KeyError`; with the fix it passes.

```
$ pytest test/test_renderable.py -v
test/test_renderable.py::test_render_apply_does_not_raise_keyerror_when_fns_are_popped PASSED
```

A changeset entry is included.

🤖 Generated with Claude Code


Note

Low Risk
Low risk: a small defensive change in Renderable.apply’s cleanup loop plus a targeted regression test; behavior only differs when a previously-rendered function id is already missing.

Overview
Prevents @gr.render from crashing when an inner render (notably gr.Examples) transiently registers and then removes a block function.

Renderable.apply now treats missing entries in blocks_config.fns as already-cleaned rather than indexing and raising KeyError, and a new regression test reproduces the “popped fake_event” scenario to ensure apply() completes without error. A patch changeset is included.

Reviewed by Cursor Bugbot for commit 3f37875. Bugbot is set up for automated code reviews on this repo. Configure here.

`Renderable.apply` snapshots `fns_from_last_render` before running the user
render function and then walks that snapshot to drop fns whose
`render_iteration` is stale. If the user render function (or anything it
calls -- e.g. `gr.Examples`, which transiently appends and then pops a
"fake_event" function in `gradio/helpers.py:580`) leaves
`blocks_config.fns` without an entry for one of the captured ids, the
cleanup loop crashed with `KeyError`.

Reproduction (issue gradio-app#12081):

  with gr.Blocks() as demo:
      with gr.Tab(key="tab", label="Tab"):
          dropdown = gr.Dropdown(["a", "b", "c"])
          @gr.render(inputs=[dropdown])
          def render(value):
              text = gr.Textbox(key="text", label="Text")
              gr.Examples(
                  examples=["a1", "a2", "a3"],
                  example_labels=["First", "Second", "Third"],
                  inputs=[text],
              )

Triggering the dropdown raised:

  File ".../gradio/renderable.py", line 89, in apply
    if blocks_config.fns[fn._id].render_iteration != self.render_iteration:
  KeyError: 3

The cleanup intent is "drop stale fns from the previous iteration", so an
already-absent entry is in the desired state. Switching the lookup to
`.get(fn._id)` and continuing on `None` removes the crash without
changing the cleanup outcome.

Test: `test/test_renderable.py::test_render_apply_does_not_raise_keyerror_when_fns_are_popped`
deterministically reproduces the issue without spinning up an HTTP queue:
it captures the renderable registered by `@gr.render`, registers a stub
block-function whose `rendered_in` matches, and pops it during the inner
render to model `gr.Examples`' fake-event cleanup. Without this fix the
test fails with `KeyError`; with the fix it passes.

Closes gradio-app#12081

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

render callback inside tab breaks examples with labels

2 participants