Skip to content

DataTable

A widget to display text in a table. This includes the ability to update data, use a cursor to navigate data, respond to mouse clicks, delete rows or columns, and individually render each cell as a Rich Text renderable. DataTable provides an efficiently displayed and updated table capable for most applications.

Applications may have custom rules for formatting, numbers, repopulating tables after searching or filtering, and responding to selections. The widget emits events to interface with custom logic.

  • Focusable
  • Container

Guide

Adding data

The following example shows how to fill a table with data. First, we use add_columns to include the lane, swimmer, country, and time columns in the table. After that, we use the add_rows method to insert the rows into the table.

TableApp  lane  swimmer               country        time    4     Joseph Schooling      Singapore      50.39   2     Michael Phelps        United States  51.14   5     Chad le Clos          South Africa   51.14   6     László Cseh           Hungary        51.14   3     Li Zhuhao             China          51.26   8     Mehdy Metella         France         51.58   7     Tom Shields           United States  51.73   1     Aleksandr Sadovnikov  Russia         51.84   10    Darren Burns          Scotland       51.84 

from textual.app import App, ComposeResult
from textual.widgets import DataTable

ROWS = [
    ("lane", "swimmer", "country", "time"),
    (4, "Joseph Schooling", "Singapore", 50.39),
    (2, "Michael Phelps", "United States", 51.14),
    (5, "Chad le Clos", "South Africa", 51.14),
    (6, "László Cseh", "Hungary", 51.14),
    (3, "Li Zhuhao", "China", 51.26),
    (8, "Mehdy Metella", "France", 51.58),
    (7, "Tom Shields", "United States", 51.73),
    (1, "Aleksandr Sadovnikov", "Russia", 51.84),
    (10, "Darren Burns", "Scotland", 51.84),
]


class TableApp(App):
    def compose(self) -> ComposeResult:
        yield DataTable()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns(*ROWS[0])
        table.add_rows(ROWS[1:])


app = TableApp()
if __name__ == "__main__":
    app.run()

To add a single row or column use add_row and add_column, respectively.

Styling and justifying cells

Cells can contain more than just plain strings - Rich renderables such as Text are also supported. Text objects provide an easy way to style and justify cell content:

TableApp  lane  swimmer               country        time      4    Joseph Schooling    Singapore50.39    2      Michael PhelpsUnited States51.14    5        Chad le Clos South Africa51.14    6         László Cseh      Hungary51.14    3           Li Zhuhao        China51.26    8       Mehdy Metella       France51.58    7         Tom ShieldsUnited States51.73    1Aleksandr Sadovnikov       Russia51.84   10        Darren Burns     Scotland51.84

from rich.text import Text

from textual.app import App, ComposeResult
from textual.widgets import DataTable

ROWS = [
    ("lane", "swimmer", "country", "time"),
    (4, "Joseph Schooling", "Singapore", 50.39),
    (2, "Michael Phelps", "United States", 51.14),
    (5, "Chad le Clos", "South Africa", 51.14),
    (6, "László Cseh", "Hungary", 51.14),
    (3, "Li Zhuhao", "China", 51.26),
    (8, "Mehdy Metella", "France", 51.58),
    (7, "Tom Shields", "United States", 51.73),
    (1, "Aleksandr Sadovnikov", "Russia", 51.84),
    (10, "Darren Burns", "Scotland", 51.84),
]


class TableApp(App):
    def compose(self) -> ComposeResult:
        yield DataTable()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns(*ROWS[0])
        for row in ROWS[1:]:
            # Adding styled and justified `Text` objects instead of plain strings.
            styled_row = [
                Text(str(cell), style="italic #03AC13", justify="right") for cell in row
            ]
            table.add_row(*styled_row)


app = TableApp()
if __name__ == "__main__":
    app.run()

Keys

When adding a row to the table, you can supply a key to add_row. A key is a unique identifier for that row. If you don't supply a key, Textual will generate one for you and return it from add_row. This key can later be used to reference the row, regardless of its current position in the table.

When working with data from a database, for example, you may wish to set the row key to the primary key of the data to ensure uniqueness. The method add_column also accepts a key argument and works similarly.

Keys are important because cells in a data table can change location due to factors like row deletion and sorting. Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table.

If you want to change the table based solely on coordinates, you may need to convert that coordinate to a cell key first using the coordinate_to_cell_key method.

Cursors

A cursor allows navigating within a table with the keyboard or mouse. There are four cursor types: "cell" (the default), "row", "column", and "none".

Change the cursor type by assigning to the cursor_type reactive attribute.
The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute.

Using the keyboard, arrow keys, Page Up, Page Down, Home and End move the cursor highlight, emitting a CellHighlighted message, then enter selects the cell, emitting a CellSelected message. If the cursor_type is row, then RowHighlighted and RowSelected are emitted, similarly for ColumnHighlighted and ColumnSelected.

When moving the mouse over the table, a MouseMove event is emitted, the cell hovered over is styled, and the hover_coordinate reactive attribute is updated. Clicking the mouse then emits the CellHighlighted and CellSelected events.

TableApp  lane  swimmer               country        time    4     Joseph Schooling      Singapore      50.39   2     Michael Phelps        United States  51.14   5     Chad le Clos          South Africa   51.14   6     László Cseh           Hungary        51.14   3     Li Zhuhao             China          51.26   8     Mehdy Metella         France         51.58   7     Tom Shields           United States  51.73   1     Aleksandr Sadovnikov  Russia         51.84   10    Darren Burns          Scotland       51.84 

TableApp  lane  swimmer               country        time    4     Joseph Schooling      Singapore      50.39   2     Michael Phelps        United States  51.14   5     Chad le Clos          South Africa   51.14   6     László Cseh           Hungary        51.14   3     Li Zhuhao             China          51.26   8     Mehdy Metella         France         51.58   7     Tom Shields           United States  51.73   1     Aleksandr Sadovnikov  Russia         51.84   10    Darren Burns          Scotland       51.84 

TableApp  lane  swimmer               country        time    4     Joseph Schooling      Singapore      50.39   2     Michael Phelps        United States  51.14   5     Chad le Clos          South Africa   51.14   6     László Cseh           Hungary        51.14   3     Li Zhuhao             China          51.26   8     Mehdy Metella         France         51.58   7     Tom Shields           United States  51.73   1     Aleksandr Sadovnikov  Russia         51.84   10    Darren Burns          Scotland       51.84 

TableApp  lane  swimmer               country        time    4     Joseph Schooling      Singapore      50.39   2     Michael Phelps        United States  51.14   5     Chad le Clos          South Africa   51.14   6     László Cseh           Hungary        51.14   3     Li Zhuhao             China          51.26   8     Mehdy Metella         France         51.58   7     Tom Shields           United States  51.73   1     Aleksandr Sadovnikov  Russia         51.84   10    Darren Burns          Scotland       51.84 

from itertools import cycle

from textual.app import App, ComposeResult
from textual.widgets import DataTable

ROWS = [
    ("lane", "swimmer", "country", "time"),
    (4, "Joseph Schooling", "Singapore", 50.39),
    (2, "Michael Phelps", "United States", 51.14),
    (5, "Chad le Clos", "South Africa", 51.14),
    (6, "László Cseh", "Hungary", 51.14),
    (3, "Li Zhuhao", "China", 51.26),
    (8, "Mehdy Metella", "France", 51.58),
    (7, "Tom Shields", "United States", 51.73),
    (1, "Aleksandr Sadovnikov", "Russia", 51.84),
    (10, "Darren Burns", "Scotland", 51.84),
]

cursors = cycle(["column", "row", "cell", "none"])


class TableApp(App):
    def compose(self) -> ComposeResult:
        yield DataTable()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.cursor_type = next(cursors)
        table.zebra_stripes = True
        table.add_columns(*ROWS[0])
        table.add_rows(ROWS[1:])

    def key_c(self):
        table = self.query_one(DataTable)
        table.cursor_type = next(cursors)


app = TableApp()
if __name__ == "__main__":
    app.run()

Updating data

Cells can be updated using the update_cell and update_cell_at methods.

Removing data

To remove all data in the table, use the clear method. To remove individual rows, use remove_row. The remove_row method accepts a key argument, which identifies the row to be removed.

If you wish to remove the row below the cursor in the DataTable, use coordinate_to_cell_key to get the row key of the row under the current cursor_coordinate, then supply this key to remove_row:

# Get the keys for the row and column under the cursor.
row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
# Supply the row key to `remove_row` to delete the row.
table.remove_row(row_key)

Removing columns

To remove individual columns, use remove_column. The remove_column method accepts a key argument, which identifies the column to be removed.

You can remove the column below the cursor using the same coordinate_to_cell_key method described above:

# Get the keys for the row and column under the cursor.
_, column_key = table.coordinate_to_cell_key(table.cursor_coordinate)
# Supply the column key to `column_row` to delete the column.
table.remove_column(column_key)

Fixed data

You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. To do this, assign an integer to the fixed_rows or fixed_columns reactive attributes of the DataTable.

TableApp  A   B    C     1   2    3     2   4    6     3   6    9     4   8    12    5   10   15  ▁▁  6   12   18    7   14   21    8   16   24    9   18   27    10  20   30    11  22   33    12  24   36    13  26   39    14  28   42    15  30   45    16  32   48    17  34   51    18  36   54    19  38   57    20  40   60    21  42   63    22  44   66    23  46   69  

from textual.app import App, ComposeResult
from textual.widgets import DataTable


class TableApp(App):
    CSS = "DataTable {height: 1fr}"

    def compose(self) -> ComposeResult:
        yield DataTable()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.focus()
        table.add_columns("A", "B", "C")
        for number in range(1, 100):
            table.add_row(str(number), str(number * 2), str(number * 3))
        table.fixed_rows = 2
        table.fixed_columns = 1
        table.cursor_type = "row"
        table.zebra_stripes = True


app = TableApp()
if __name__ == "__main__":
    app.run()

In the example above, we set fixed_rows to 2, and fixed_columns to 1, meaning the first two rows and the leftmost column do not scroll - they always remain visible as you scroll through the data table.

Sorting

The DataTable rows can be sorted using the sort method.

There are three methods of using sort:

  • By Column. Pass columns in as parameters to sort by the natural order of one or more columns. Specify a column using either a ColumnKey instance or the key you supplied to add_column. For example, sort("country", "region") would sort by country, and, when the country values are equal, by region.
  • By Key function. Pass a function as the key parameter to sort, similar to the key function parameter of Python's sorted built-in. The function will be called once per row with a tuple of all row values.
  • By both Column and Key function. You can specify which columns to include as parameters to your key function. For example, sort("hours", "rate", key=lambda h, r: h*r) passes two values to the key function for each row.

The reverse argument reverses the order of your sort. Note that correct sorting may require your key function to undo your formatting.

TableApp  lane  swimmer               country        time 1  time 2   4     Joseph Schooling     Singapore 50.39   51.84    2     Michael Phelps       United States 50.39   51.84    5     Chad le Clos         South Africa 51.14   51.73    6     László Cseh          Hungary 51.14   51.58    3     Li Zhuhao            China 51.26   51.26    8     Mehdy Metella        France 51.58   52.15    7     Tom Shields          United States 51.73   51.12    1     Aleksandr Sadovnikov Russia 51.84   50.85    10    Darren Burns         Scotland 51.84   51.55    a Sort By Average Time  n Sort By Last Name  c Sort By Country  d S^p palette

from rich.text import Text

from textual.app import App, ComposeResult
from textual.widgets import DataTable, Footer

ROWS = [
    ("lane", "swimmer", "country", "time 1", "time 2"),
    (4, "Joseph Schooling", Text("Singapore", style="italic"), 50.39, 51.84),
    (2, "Michael Phelps", Text("United States", style="italic"), 50.39, 51.84),
    (5, "Chad le Clos", Text("South Africa", style="italic"), 51.14, 51.73),
    (6, "László Cseh", Text("Hungary", style="italic"), 51.14, 51.58),
    (3, "Li Zhuhao", Text("China", style="italic"), 51.26, 51.26),
    (8, "Mehdy Metella", Text("France", style="italic"), 51.58, 52.15),
    (7, "Tom Shields", Text("United States", style="italic"), 51.73, 51.12),
    (1, "Aleksandr Sadovnikov", Text("Russia", style="italic"), 51.84, 50.85),
    (10, "Darren Burns", Text("Scotland", style="italic"), 51.84, 51.55),
]


class TableApp(App):
    BINDINGS = [
        ("a", "sort_by_average_time", "Sort By Average Time"),
        ("n", "sort_by_last_name", "Sort By Last Name"),
        ("c", "sort_by_country", "Sort By Country"),
        ("d", "sort_by_columns", "Sort By Columns (Only)"),
    ]

    current_sorts: set = set()

    def compose(self) -> ComposeResult:
        yield DataTable()
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        for col in ROWS[0]:
            table.add_column(col, key=col)
        table.add_rows(ROWS[1:])

    def sort_reverse(self, sort_type: str):
        """Determine if `sort_type` is ascending or descending."""
        reverse = sort_type in self.current_sorts
        if reverse:
            self.current_sorts.remove(sort_type)
        else:
            self.current_sorts.add(sort_type)
        return reverse

    def action_sort_by_average_time(self) -> None:
        """Sort DataTable by average of times (via a function) and