clear_panes() docstring says "optionally awaitable" but skipping the await causes DuplicateIds
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:
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 subsequentadd_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) |
Source: Textualize/textual