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

  1. Give each sibling a unique id, e.g. derive it from a unique key: f"row-{item['pk']}"
  2. If the same widget is re-mounted, call await widget.remove() first, or use replace_children/mount with `before`/`after` on a fresh instance
  3. Use query_one('#id') on the right parent scope to confirm which container already holds the id shown in the error
  4. 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

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


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/d9c6e5e335bb5915. Report an issue: GitHub.