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
- Deduplicate before adding: if key not in table.row_keys: table.add_row(..., key=key).
- For refreshes, remove existing rows (remove_row) or clear() before re-adding.
- Let Textual auto-generate keys (omit key=) when uniqueness tracking is not needed.
- 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
- Deduplicate by key before add_row in refresh loops
- Remove or clear rows before re-adding
- Omit key= to let Textual generate unique keys when tracking isn't needed
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
- Row index {row_index!r} is not valid.
- The column key {key!r} already exists.
- Duplicated mode name {mode!r}.
- Row key {row_key!r} is not valid.
- No row exists for row_key={row_key!r}
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/4c5e21cec85abb94.
Report an issue: GitHub.