Textualize/textual · error · DuplicateKey

The row key {row_key!r} already exists.

Error message

The row key {row_key!r} already exists.

What it means

Raised by DataTable.add_row when row_key already exists in _row_locations. Row keys must be unique; this commonly fires when a caller supplies explicit keys and re-adds a row that already exists (e.g. during data refresh), because add_row does not upsert.

Source

Thrown at src/textual/widgets/_data_table.py:1693

    ) -> RowKey:
        """Add a row at the bottom of the DataTable.

        Args:
            *cells: Positional arguments should contain cell data.
            height: The height of a row (in lines). Use `None` to auto-detect the optimal
                height.
            key: A key which uniquely identifies this row. If None, it will be generated
                for you and returned.
            label: The label for the row. Will be displayed to the left if supplied.

        Returns:
            Unique identifier for this row. Can be used to retrieve this row regardless
                of its current location in the DataTable (it could have moved after
                being added due to sorting or insertion/deletion of other rows).
        """
        row_key = RowKey(key)
        if row_key in self._row_locations:
            raise DuplicateKey(f"The row key {row_key!r} already exists.")

        # TODO: If there are no columns: do we generate them here?
        #  If we don't do this, users will be required to call add_column(s)
        #  Before they call add_row.

        if len(cells) > len(self.ordered_columns):
            raise ValueError("More values provided than there are columns.")

        row_index = self.row_count
        # Map the key of this row to its current index
        self._row_locations[row_key] = row_index
        self._data[row_key] = {
            column.key: cell
            for column, cell in zip_longest(self.ordered_columns, cells)
        }

        label = Text.from_markup(label, end="") if isinstance(label, str) else label

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Deduplicate before adding: if key not in table.row_keys: table.add_row(..., key=key).
  2. For refreshes, remove existing rows (remove_row) or clear() before re-adding.
  3. Let Textual auto-generate keys (omit key=) when uniqueness tracking is not needed.
  4. Catch DuplicateKey to make refresh logic idempotent.

Example fix

# before
table.add_row(*values, key=item_id)

# after
if item_id not in table.row_keys:
    table.add_row(*values, key=item_id)
Defensive patterns

Strategy: validation

Validate before calling

if item_id not in table.row_keys:
    table.add_row(*values, key=item_id)

Try / catch

from textual.widgets._data_table import DuplicateKey
try:
    table.add_row(*values, key=item_id)
except DuplicateKey:
    pass  # row already present, idempotent refresh

Prevention

When it happens

Trigger: Calling add_row(..., key=k) twice with the same key; periodic refresh jobs that append rows with stable IDs without deduplicating; re-adding rows after clear() of columns only.

Common situations: Polling loops appending log/feed rows keyed by message id; reloading a dataset without clearing; retries of failed add_row calls that actually succeeded.

Related errors


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