#6715·textual

clear_panes() docstring says "optionally awaitable" but skipping the await causes DuplicateIds

Author: jmleclercqCreated Sep 12, 2026Updated Sep 12, 2026

Hey, first off — thanks for Textual, it's been genuinely fun to build with.

I hit a small gotcha with TabbedContent.clear_panes() that took me a while to track down, so figured I'd flag it in case it's an easy doc fix (or worth changing behavior-wise).

The docstring says it returns "an optionally awaitable object", which reads as "safe to fire-and-forget". In practice, if you skip the await and immediately re-add panes with the same explicit ids right after (a pretty natural "refresh" pattern), the removal is only scheduled — so the new panes race the old ones being torn down, and you get a DuplicateIds crash. Not obvious from the docstring that this matters!

(This is a different flavor of bug from #5215, by the way — that one was about auto-generated ids reusing a stale counter. Here the ids are explicit and stable; the crash only happens because clear_panes() wasn't awaited.)

Reproduction

This crashes on the first press of r:

python
from textual.app import App, ComposeResult
from textual.widgets import Footer, Label, TabbedContent, TabPane


class ExampleApp(App):
    BINDINGS = [("r", "refresh", "Refresh (clear + re-add same ids)")]

    def compose(self) -> ComposeResult:
        yield TabbedContent(id="tabs")
        yield Footer()

    def on_mount(self) -> None:
        self._render()

    def _render(self) -> None:
        tabs = self.query_one("#tabs", TabbedContent)
        tabs.clear_panes()  # not awaited
        for i in range(2):
            tabs.add_pane(TabPane(f"Tab {i}", Label(f"content {i}"), id=f"pane-{i}"))

    def action_refresh(self) -> None:
        self._render()


if __name__ == "__main__":
    ExampleApp().run()
DuplicateIds: Tried to insert a widget with ID '--content-tab-pane-0', but a widget already exists with that ID (ContentTab(id='--content-tab-pane-0')); ensure all child widgets have a unique ID.

Making _render/on_mount/action_refresh async and doing await tabs.clear_panes() instead fixes it — tested 5 refreshes in a row with no crash.

Maybe worth doing

No strong opinion on which, but one of:

  • Tweak the docstring so it's clearer that "optionally awaitable" doesn't mean "safe to ignore" if you're about to reuse the same ids, or
  • Have clear_panes() settle the teardown before any subsequent add_pane() call can race it, so this can't bite people regardless of whether they await it.

Ran into this building hastty, a little TUI for Home Assistant — refreshing its TabbedContent on a keybinding is exactly the pattern above. Happy to help test a fix if useful!

Textual Diagnostics

Name Value
Textual 8.2.8
Rich 15.0.0

Python

Name Value
Version 3.12.3 (GCC 13.3.0)

Operating System

Name Value
System Linux
Release 6.8.0-... (Ubuntu 24.04)