Textualize/textual · error · DuplicateIds
Tried to insert a widget with ID {widget_id!r}, but a widget
Error message
Tried to insert a widget with ID {widget_id!r}, but a widget already exists with that ID ({self._nodes_by_id[widget_id]!r}); ensure all child widgets have a unique ID. What it means
Textual's NodeList._ensure_unique_id runs every time a widget is appended or inserted into a container, and raises DuplicateIds if the incoming widget's id already exists among that container's direct children. IDs in Textual are only required to be unique among siblings of the same parent widget, not globally. The message includes both the requested id and a repr of the existing widget that already claims it, which makes locating the collision straightforward.
Source
Thrown at src/textual/_node_list.py:158
self._nodes.insert(index, widget)
self._nodes_set.add(widget)
widget_id = widget.id
if widget_id is not None:
self._ensure_unique_id(widget_id)
self._nodes_by_id[widget_id] = widget
self.updated()
def _ensure_unique_id(self, widget_id: str) -> None:
"""Ensure a new widget ID would be unique.
Args:
widget_id: New widget ID.
Raises:
DuplicateIds: If the given ID is not unique.
"""
if widget_id in self._nodes_by_id:
raise DuplicateIds(
f"Tried to insert a widget with ID {widget_id!r}, but a widget already exists with that ID ({self._nodes_by_id[widget_id]!r}); "
"ensure all child widgets have a unique ID."
)
def _remove(self, widget: Widget) -> None:
"""Remove a widget from the list.
Removing a widget not in the list is a null-op.
Args:
widget: A Widget in the list.
"""
if widget in self._nodes_set:
del self._nodes[self._nodes.index(widget)]
self._nodes_set.remove(widget)
widget_id = widget.id
if widget_id in self._nodes_by_id:
del self._nodes_by_id[widget_id]View on GitHub (pinned to 06dbeef4bb)
Solutions
- Give each sibling a unique id, e.g. derive it from a unique key: f"row-{item['pk']}"
- If the same widget is re-mounted, call await widget.remove() first, or use replace_children/mount with `before`/`after` on a fresh instance
- Use query_one('#id') on the right parent scope to confirm which container already holds the id shown in the error
- Omit ids entirely and use CSS classes when per-widget identity is not needed
Example fix
# before
for name in ["a", "a"]:
yield Static(name, id=name) # DuplicateIds
# after
for i, name in enumerate(["a", "a"]):
yield Static(name, id=f"item-{i}-{name}") Defensive patterns
Strategy: validation
Validate before calling
def ids_are_unique(parent) -> bool:
ids = [c.id for c in parent.children if c.id is not None]
return len(ids) == len(set(ids))
# also check the incoming widget before mounting:
def can_mount(parent, widget) -> bool:
return widget.id is None or widget.id not in {c.id for c in parent.children} Try / catch
from textual._node_list import DuplicateIds
try:
await self.mount(widget)
except DuplicateIds as e:
widget.id = f"{widget.id}-{next(counter)}" # or skip/log Prevention
- Derive ids from guaranteed-unique keys (primary keys, uuids)
- Scope queries with parent.query_one('#id') to reason about sibling uniqueness
- Omit ids when uniqueness is hard to guarantee; use classes instead
When it happens
Trigger: Mounting two widgets with the same id into the same parent: `mount(Static("a", id="x"), Static("b", id="x"))`; mounting a widget that was constructed with an id equal to one used by a sibling; loops that generate ids like f"row{i}" with colliding counters; compositing the same widget instance twice without removing it first.
Common situations: Data-driven UIs that build rows keyed by a non-unique field (e.g. two rows with the same category id); copy-pasted widget definitions retaining the same id; widgets re-mounted after remove() in refresh loops where removal hasn't completed; two different components each defaulting to id="root" or id="container" mounted into the same screen.
Related errors
- New options contain duplicated IDs; Ensure that the IDs are
- Unable to add {option!r} due to duplicate ID
- No {name!r} key in COMPONENT_CLASSES
- node has no screen
- A widget can't be its own parent
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/d9c6e5e335bb5915.
Report an issue: GitHub.