Repository: Textualize/textual
Pull request: Data table cell padding
**Please review the following checklist.**

- [x] Docstrings on all new or modified functions / classes 
- [x] Updated documentation
- [x] Updated CHANGELOG.md (where appropriate)

This fixes #3435.


Problem statement:
Add ability to remove cell padding
I haven't been able to modify the style of a DataTable so that there is zero space between the columns. This in contrast to the rows, which are stacked with zero space between the cells. 

I've tried setting `margin`, `grid-gutter` and `padding` to zero in the TCSS file, with no success: 

```
DataTable {
	grid-gutter: 0;
	padding: 0;
	margin: 0;
} 
```

Hints from issue discussion:
We found the following entries in the [FAQ](https://textual.textualize.io/FAQ/) which you may find helpful:


- [How do I center a widget in a screen?](https://textual.textualize.io/FAQ/#how-do-i-center-a-widget-in-a-screen)
- [Why doesn't Textual look good on macOS?](https://textual.textualize.io/FAQ/#why-doesn't-textual-look-good-on-macos)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
I assume you mean the cell padding that's baked in https://github.com/Textualize/textual/discussions/2503#discussioncomment-5822737. 

Textual CSS works at the widget level, but datatable cells aren't widgets internally. Your CSS above is targetting the margin/padding of the table widget itself, not the cells.
@rmurray2 There's no way to remove the cell padding at the moment. Without cell padding the cells would be right next to each other, and it would be difficult to read.

Perhaps you could explain why you want this? We might be able to offer alternative suggestions.
@willmcgugan thanks for your reply. 

I'd ultimately like to generate a heatmap, like the image below, in which the backgrounds correspond to an intensity value for that row/column combination

![heatmap](https://github.com/Textualize/textual/assets/5521035/0c9a6411-49b0-4b27-bfbb-fa467bc807fd)

That's a novel use for the datatable! I doubt it will be difficult for use to add an option to remove cell padding. We will look in to it for the next version.
Great, thanks, just one suggestion: The column header row will become difficult to read if the padding is removed, so I would suggest, if it's possible, tilting the right side of the labels at about a 45 or more degree angle. Or any arbitrary, user-defined angle. That way they won't run into each other.
I may not have followed what you are suggesting there, but its impossible to rotate text in the terminal I'm afraid.

Commit message: Remove magical constant from tests.

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b63f1ca8a7..785164b4ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Added
 
 - `OutOfBounds` exception to be raised by `Pilot` https://github.com/Textualize/textual/pull/3360
+- Reactive `cell_padding` (and respective parameter) to define horizontal cell padding in data table columns https://github.com/Textualize/textual/issues/3435
 - Added `Input.clear` method https://github.com/Textualize/textual/pull/3430
 
 ### Changed
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index ee4fdae3ec..b6cf4e7651 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -41,7 +41,8 @@
 """The valid types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type]."""
 CellType = TypeVar("CellType")
 
-CELL_X_PADDING = 2
+_DEFAULT_CELL_X_PADDING = 1
+"""Default padding to use on each side of a column in the data table."""
 
 
 class CellDoesNotExist(Exception):
@@ -170,14 +171,18 @@ class Column:
     content_width: int = 0
     auto_width: bool = False
 
-    @property
-    def render_width(self) -> int:
-        """Width in cells, required to render a column."""
-        # +2 is to account for space padding either side of the cell
-        if self.auto_width:
-            return self.content_width + CELL_X_PADDING
-        else:
-            return self.width + CELL_X_PADDING
+    def render_width(self, data_table: DataTable[Any]) -> int:
+        """Width, in cells, required to render the column with padding included.
+
+        Args:
+            data_table: The data table where the column will be rendered.
+
+        Returns:
+            The width, in cells, required to render the column with padding included.
+        """
+        return 2 * data_table.cell_padding + (
+            self.content_width if self.auto_width else self.width
+        )
 
 
 @dataclass
@@ -309,6 +314,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
     show_cursor = Reactive(True)
     cursor_type: Reactive[CursorType] = Reactive[CursorType]("cell")
     """The type of the cursor of the `DataTable`."""
+    cell_padding = Reactive(_DEFAULT_CELL_X_PADDING)
+    """Horizontal padding between cells, applied on each side of each cell."""
 
     cursor_coordinate: Reactive[Coordinate] = Reactive(
         Coordinate(0, 0), repaint=False, always_update=True
@@ -584,11 +591,43 @@ def __init__(
         cursor_foreground_priority: Literal["renderable", "css"] = "css",
         cursor_background_priority: Literal["renderable", "css"] = "renderable",
         cursor_type: CursorType = "cell",
+        cell_padding: int = _DEFAULT_CELL_X_PADDING,
         name: str | None = None,
         id: str | None = None,
         classes: str | None = None,
         disabled: bool = False,
     ) -> None:
+        """Initialises a widget to display tabular data.
+
+        Args:
+            show_header: Whether the table header should be visible or not.
+            show_row_labels: Whether the row labels should be shown or not.
+            fixed_rows: The number of rows, counting from the top, that should be fixed
+                and still visible when the user scrolls down.
+            fixed_columns: The number of columns, counting from the left, that should be
+                fixed and still visible when the user scrolls right.
+            zebra_stripes: Enables or disables a zebra effect applied to the background
+                color of the rows of the table, where alternate colors are styled
+                differently to improve the readability of the table.
+            header_height: The height, in number of cells, of the data table header.
+            show_cursor: Whether the cursor should be visible when navigating the data
+                table or not.
+            cursor_foreground_priority: If the data associated with a cell is an
+                arbitrary renderable with a set foreground color, this determines whether
+                that color is prioritised over the cursor component class or not.
+            cursor_background_priority: If the data associated with a cell is an
+                arbitrary renderable with a set background color, this determines whether
+                that color is prioritesed over the cursor component class or not.
+            cursor_type: The type of cursor to be used when navigating the data table
+                with the keyboard.
+            cell_padding: The number of cells added on each side of each column. Setting
+                this value to zero will likely make your table very heard to read.
+            name: The name of the widget.
+            id: The ID of the widget in the DOM.
+            classes: The CSS classes for the widget.
+            disabled: Whether the widget is disabled or not.
+        """
+
         super().__init__(name=name, id=id, classes=classes, disabled=disabled)
         self._data: dict[RowKey, dict[ColumnKey, CellType]] = {}
         """Contains the cells of the table, indexed by row key and column key.
@@ -673,6 +712,8 @@ def __init__(
          in the event where a cell contains a renderable with a background color."""
         self.cursor_type = cursor_type
         """The type of cursor of the `DataTable`."""
+        self.cell_padding = cell_padding
+        """Horizontal padding between cells, applied on each side of each cell."""
 
     @property
     def hover_row(self) -> int:
@@ -996,7 +1037,7 @@ def watch_show_header(self, show: bool) -> None:
 
     def watch_show_row_labels(self, show: bool) -> None:
         width, height = self.virtual_size
-        column_width = self._label_column.render_width
+        column_width = self._label_column.render_width(self)
         width_change = column_width if show else -column_width
         self.virtual_size = Size(width + width_change, height)
         self._scroll_cursor_into_view()
@@ -1164,7 +1205,11 @@ def _highlight_cursor(self) -> None:
     @property
     def _row_label_column_width(self) -> int:
         """The render width of the column containing row labels"""
-        return self._label_column.render_width if self._should_render_row_labels else 0
+        return (
+            self._label_column.render_width(self)
+            if self._should_render_row_labels
+            else 0
+        )
 
     def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
         """Update the widths of the columns based on the newly updated cell widths."""
@@ -1259,7 +1304,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
                         row_index,
                         column_index,
                         style,
-                        column.render_width,
+                        column.render_width(self),
                         cursor=should_highlight(
                             cursor_location, cell_location, cursor_type
                         ),
@@ -1269,7 +1314,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
                     )
                     cell_height = len(rendered_cell)
                     rendered_cells.append(
-                        (rendered_cell, cell_height, column.render_width)
+                        (rendered_cell, cell_height, column.render_width(self))
                     )
                     height = max(height, cell_height)
 
@@ -1286,7 +1331,9 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
                             ]
                         )
 
-        data_cells_width = sum(column.render_width for column in self.columns.values())
+        data_cells_width = sum(
+            column.render_width(self) for column in self.columns.values()
+        )
         total_width = data_cells_width + self._row_label_column_width
         header_height = self.header_height if self.show_header else 0
         self.virtual_size = Size(
@@ -1306,11 +1353,14 @@ def _get_cell_region(self, coordinate: Coordinate) -> Region:
         # The x-coordinate of a cell is the sum of widths of the data cells to the left
         # plus the width of the render width of the longest row label.
         x = (
-            sum(column.render_width for column in self.ordered_columns[:column_index])
+            sum(
+                column.render_width(self)
+                for column in self.ordered_columns[:column_index]
+            )
             + self._row_label_column_width
         )
         column_key = self._column_locations.get_key(column_index)
-        width = self.columns[column_key].render_width
+        width = self.columns[column_key].render_width(self)
         height = row.height
         y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index])
         if self.show_header:
@@ -1327,7 +1377,7 @@ def _get_row_region(self, row_index: int) -> Region:
         row_key = self._row_locations.get_key(row_index)
         row = rows[row_key]
         row_width = (
-            sum(column.render_width for column in self.columns.values())
+            sum(column.render_width(self) for column in self.columns.values())
             + self._row_label_column_width
         )
         y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index])
@@ -1343,11 +1393,14 @@ def _get_column_region(self, column_index: int) -> Region:
 
         columns = self.columns
         x = (
-            sum(column.render_width for column in self.ordered_columns[:column_index])
+            sum(
+                column.render_width(self)
+                for column in self.ordered_columns[:column_index]
+            )
             + self._row_label_column_width
         )
         column_key = self._column_locations.get_key(column_index)
-        width = columns[column_key].render_width
+        width = columns[column_key].render_width(self)
         header_height = self.header_height if self.show_header else 0
         height = self._total_row_height + header_height
         full_column_region = Region(x, 0, width, height)
@@ -1881,7 +1934,7 @@ def _render_cell(
                     )
             lines = self.app.console.render_lines(
                 Styled(
-                    Padding(cell, (0, 1)),
+                    Padding(cell, (0, self.cell_padding)),
                     pre_style=base_style + component_style,
                     post_style=post_style,
                 ),
@@ -2030,7 +2083,7 @@ def _render_line_in_row(
                     row_index,
                     column_index,
                     fixed_style,
-                    column.render_width,
+                    column.render_width(self),
                     cursor=should_highlight(
                         cursor_location, cell_location, cursor_type
                     ),
@@ -2047,7 +2100,7 @@ def _render_line_in_row(
                 row_index,
                 column_index,
                 row_style,
-                column.render_width,
+                column.render_width(self),
                 cursor=should_highlight(cursor_location, cell_location, cursor_type),
                 hover=should_highlight(hover_location, cell_location, cursor_type),
             )[line_no]
@@ -2057,7 +2110,7 @@ def _render_line_in_row(
         widget_width = self.size.width
         table_width = (
             sum(
-                column.render_width
+                column.render_width(self)
                 for column in self.ordered_columns[self.fixed_columns :]
             )
             + self._row_label_column_width
@@ -2141,7 +2194,8 @@ def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip:
             hover_location=self.hover_coordinate,
         )
         fixed_width = sum(
-            column.render_width for column in self.ordered_columns[: self.fixed_columns]
+            column.render_width(self)
+            for column in self.ordered_columns[: self.fixed_columns]
         )
 
         fixed_line: list[Segment] = list(chain.from_iterable(fixed)) if fixed else []
@@ -2260,7 +2314,7 @@ def _get_fixed_offset(self) -> Spacing:
         top += sum(row.height for row in self.ordered_rows[: self.fixed_rows])
         left = (
             sum(
-                column.render_width
+                column.render_width(self)
                 for column in self.ordered_columns[: self.fixed_columns]
             )
             + self._row_label_column_width
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index a1b1206dbe..ac7d6c80d6 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -14001,6 +14001,164 @@
   
   '''
 # ---
+# name: test_datatable_cell_padding
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-1699433504-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-1699433504-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-1699433504-r1 { fill: #e1e1e1 }
+  .terminal-1699433504-r2 { fill: #c5c8c6 }
+  .terminal-1699433504-r3 { fill: #dde6ed;font-weight: bold }
+  .terminal-1699433504-r4 { fill: #dde6ed }
+  .terminal-1699433504-r5 { fill: #211505 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-1699433504-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-1699433504-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-1699433504-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1699433504-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TableApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-1699433504-clip-terminal)">
+      <rect fill="#1e1e1e" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="12.2" y="25.9" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="73.2" y="25.9" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="134.2" y="25.9" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="195.2" y="25.9" width="768.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="12.2" y="50.3" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="73.2" y="50.3" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="134.2" y="50.3" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="195.2" y="50.3" width="768.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="12.2" y="99.1" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="97.6" y="99.1" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="183" y="99.1" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="268.4" y="99.1" width="695.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="12.2" y="123.5" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="97.6" y="123.5" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="123.5" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="268.4" y="123.5" width="695.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="12.2" y="172.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="122" y="172.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="231.8" y="172.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="341.6" y="172.3" width="622.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="12.2" y="196.7" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="122" y="196.7" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="231.8" y="196.7" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="341.6" y="196.7" width="622.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="12.2" y="245.5" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="146.4" y="245.5" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="280.6" y="245.5" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="414.8" y="245.5" width="549" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="12.2" y="269.9" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="146.4" y="269.9" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="280.6" y="269.9" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="414.8" y="269.9" width="549" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="12.2" y="318.7" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="170.8" y="318.7" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#004578" x="329.4" y="318.7" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#073b61" x="488" y="318.7" width="475.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="12.2" y="343.1" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="343.1" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="329.4" y="343.1" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="475.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="963.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-1699433504-matrix">
+      <text class="terminal-1699433504-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1699433504-line-0)">
+  </text><text class="terminal-1699433504-r3" x="12.2" y="44.4" textLength="61" clip-path="url(#terminal-1699433504-line-1)">one&#160;&#160;</text><text class="terminal-1699433504-r3" x="73.2" y="44.4" textLength="61" clip-path="url(#terminal-1699433504-line-1)">two&#160;&#160;</text><text class="terminal-1699433504-r3" x="134.2" y="44.4" textLength="61" clip-path="url(#terminal-1699433504-line-1)">three</text><text class="terminal-1699433504-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1699433504-line-1)">
+  </text><text class="terminal-1699433504-r5" x="12.2" y="68.8" textLength="61" clip-path="url(#terminal-1699433504-line-2)">value</text><text class="terminal-1699433504-r1" x="73.2" y="68.8" textLength="61" clip-path="url(#terminal-1699433504-line-2)">value</text><text class="terminal-1699433504-r1" x="134.2" y="68.8" textLength="61" clip-path="url(#terminal-1699433504-line-2)">val&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1699433504-line-2)">
+  </text><text class="terminal-1699433504-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1699433504-line-3)">
+  </text><text class="terminal-1699433504-r3" x="12.2" y="117.6" textLength="85.4" clip-path="url(#terminal-1699433504-line-4)">&#160;one&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="97.6" y="117.6" textLength="85.4" clip-path="url(#terminal-1699433504-line-4)">&#160;two&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="183" y="117.6" textLength="85.4" clip-path="url(#terminal-1699433504-line-4)">&#160;three&#160;</text><text class="terminal-1699433504-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1699433504-line-4)">
+  </text><text class="terminal-1699433504-r5" x="12.2" y="142" textLength="85.4" clip-path="url(#terminal-1699433504-line-5)">&#160;value&#160;</text><text class="terminal-1699433504-r1" x="97.6" y="142" textLength="85.4" clip-path="url(#terminal-1699433504-line-5)">&#160;value&#160;</text><text class="terminal-1699433504-r1" x="183" y="142" textLength="85.4" clip-path="url(#terminal-1699433504-line-5)">&#160;val&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1699433504-line-5)">
+  </text><text class="terminal-1699433504-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1699433504-line-6)">
+  </text><text class="terminal-1699433504-r3" x="12.2" y="190.8" textLength="109.8" clip-path="url(#terminal-1699433504-line-7)">&#160;&#160;one&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="122" y="190.8" textLength="109.8" clip-path="url(#terminal-1699433504-line-7)">&#160;&#160;two&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="231.8" y="190.8" textLength="109.8" clip-path="url(#terminal-1699433504-line-7)">&#160;&#160;three&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1699433504-line-7)">
+  </text><text class="terminal-1699433504-r5" x="12.2" y="215.2" textLength="109.8" clip-path="url(#terminal-1699433504-line-8)">&#160;&#160;value&#160;&#160;</text><text class="terminal-1699433504-r1" x="122" y="215.2" textLength="109.8" clip-path="url(#terminal-1699433504-line-8)">&#160;&#160;value&#160;&#160;</text><text class="terminal-1699433504-r1" x="231.8" y="215.2" textLength="109.8" clip-path="url(#terminal-1699433504-line-8)">&#160;&#160;val&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1699433504-line-8)">
+  </text><text class="terminal-1699433504-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1699433504-line-9)">
+  </text><text class="terminal-1699433504-r3" x="12.2" y="264" textLength="134.2" clip-path="url(#terminal-1699433504-line-10)">&#160;&#160;&#160;one&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="146.4" y="264" textLength="134.2" clip-path="url(#terminal-1699433504-line-10)">&#160;&#160;&#160;two&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="280.6" y="264" textLength="134.2" clip-path="url(#terminal-1699433504-line-10)">&#160;&#160;&#160;three&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1699433504-line-10)">
+  </text><text class="terminal-1699433504-r5" x="12.2" y="288.4" textLength="134.2" clip-path="url(#terminal-1699433504-line-11)">&#160;&#160;&#160;value&#160;&#160;&#160;</text><text class="terminal-1699433504-r1" x="146.4" y="288.4" textLength="134.2" clip-path="url(#terminal-1699433504-line-11)">&#160;&#160;&#160;value&#160;&#160;&#160;</text><text class="terminal-1699433504-r1" x="280.6" y="288.4" textLength="134.2" clip-path="url(#terminal-1699433504-line-11)">&#160;&#160;&#160;val&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1699433504-line-11)">
+  </text><text class="terminal-1699433504-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1699433504-line-12)">
+  </text><text class="terminal-1699433504-r3" x="12.2" y="337.2" textLength="158.6" clip-path="url(#terminal-1699433504-line-13)">&#160;&#160;&#160;&#160;one&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="170.8" y="337.2" textLength="158.6" clip-path="url(#terminal-1699433504-line-13)">&#160;&#160;&#160;&#160;two&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r3" x="329.4" y="337.2" textLength="158.6" clip-path="url(#terminal-1699433504-line-13)">&#160;&#160;&#160;&#160;three&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1699433504-line-13)">
+  </text><text class="terminal-1699433504-r5" x="12.2" y="361.6" textLength="158.6" clip-path="url(#terminal-1699433504-line-14)">&#160;&#160;&#160;&#160;value&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r1" x="170.8" y="361.6" textLength="158.6" clip-path="url(#terminal-1699433504-line-14)">&#160;&#160;&#160;&#160;value&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r1" x="329.4" y="361.6" textLength="158.6" clip-path="url(#terminal-1699433504-line-14)">&#160;&#160;&#160;&#160;val&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1699433504-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1699433504-line-14)">
+  </text><text class="terminal-1699433504-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1699433504-line-15)">
+  </text><text class="terminal-1699433504-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1699433504-line-16)">
+  </text><text class="terminal-1699433504-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1699433504-line-17)">
+  </text><text class="terminal-1699433504-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1699433504-line-18)">
+  </text><text class="terminal-1699433504-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1699433504-line-19)">
+  </text><text class="terminal-1699433504-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1699433504-line-20)">
+  </text><text class="terminal-1699433504-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1699433504-line-21)">
+  </text><text class="terminal-1699433504-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1699433504-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
 # name: test_datatable_column_cursor_render
   '''
   <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
diff --git a/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py b/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py
new file mode 100644
index 0000000000..ec1d8a6991
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py
@@ -0,0 +1,22 @@
+from textual.app import App, ComposeResult
+from textual.widgets import DataTable
+
+
+class TableApp(App):
+    CSS = """
+    DataTable {
+        margin: 1;
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        for cell_padding in range(5):
+            dt = DataTable(cell_padding=cell_padding)
+            dt.add_columns("one", "two", "three")
+            dt.add_row("value", "value", "val")
+            yield dt
+
+
+app = TableApp()
+if __name__ == "__main__":
+    app.run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 75211245ab..4e5f8d53df 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -167,6 +167,11 @@ def test_datatable_add_row_auto_height_sorted(snap_compare):
     )
 
 
+def test_datatable_cell_padding(snap_compare):
+    # Check that horizontal cell padding is respected.
+    assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_cell_padding.py")
+
+
 def test_footer_render(snap_compare):
     assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")
 
@@ -708,7 +713,9 @@ def test_nested_fr(snap_compare) -> None:
     assert snap_compare(SNAPSHOT_APPS_DIR / "nested_fr.py")
 
 
-@pytest.mark.skipif(sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher")
+@pytest.mark.skipif(
+    sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher"
+)
 @pytest.mark.parametrize("language", BUILTIN_LANGUAGES)
 def test_text_area_language_rendering(language, snap_compare):
     # This test will fail if we're missing a snapshot test for a valid
@@ -760,9 +767,12 @@ def setup_selection(pilot):
     )
 
 
-@pytest.mark.skipif(sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher")
-@pytest.mark.parametrize("theme_name",
-                         [theme.name for theme in TextAreaTheme.builtin_themes()])
+@pytest.mark.skipif(
+    sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher"
+)
+@pytest.mark.parametrize(
+    "theme_name", [theme.name for theme in TextAreaTheme.builtin_themes()]
+)
 def test_text_area_themes(snap_compare, theme_name):
     """Each theme should have its own snapshot with at least some Python
     to check that the rendering is sensible. This also ensures that theme
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
index ebb5ab3674..5e424d6610 100644
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -11,6 +11,7 @@
 from textual.geometry import Offset
 from textual.message import Message
 from textual.widgets import DataTable
+from textual.widgets._data_table import _DEFAULT_CELL_X_PADDING
 from textual.widgets.data_table import (
     CellDoesNotExist,
     CellKey,
@@ -271,7 +272,10 @@ async def test_add_column_with_width():
         row = table.add_row("123")
         assert table.get_cell(row, column) == "123"
         assert table.columns[column].width == 10
-        assert table.columns[column].render_width == 12  # 10 + (2 padding)
+        assert (
+            table.columns[column].render_width(table)
+            == 10 + 2 * _DEFAULT_CELL_X_PADDING
+        )
 
 
 async def test_add_columns():
@@ -689,7 +693,10 @@ async def test_update_cell_at_column_width(label, new_value, new_content_width):
         table.update_cell_at(Coordinate(0, 0), new_value, update_width=True)
         await wait_for_idle()
         assert first_column.content_width == new_content_width
-        assert first_column.render_width == new_content_width + 2
+        assert (
+            first_column.render_width(table)
+            == new_content_width + 2 * _DEFAULT_CELL_X_PADDING
+        )
 
 
 async def test_coordinate_to_cell_key():
@@ -1188,17 +1195,28 @@ async def test_add_row_auto_height(cell: RenderableType, height: int):
 async def test_add_row_expands_column_widths():
     """Regression test for https://github.com/Textualize/textual/issues/1026."""
     app = DataTableApp()
-    from textual.widgets._data_table import CELL_X_PADDING
 
     async with app.run_test() as pilot:
         table = app.query_one(DataTable)
         table.add_column("First")
         table.add_column("Second", width=10)
         await pilot.pause()
-        assert table.ordered_columns[0].render_width == 5 + CELL_X_PADDING
-        assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING
+        assert (
+            table.ordered_columns[0].render_width(table)
+            == 5 + 2 * _DEFAULT_CELL_X_PADDING
+        )
+        assert (
+            table.ordered_columns[1].render_width(table)
+            == 10 + 2 * _DEFAULT_CELL_X_PADDING
+        )
 
         table.add_row("a" * 20, "a" * 20)
         await pilot.pause()
-        assert table.ordered_columns[0].render_width == 20 + CELL_X_PADDING
-        assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING
+        assert (
+            table.ordered_columns[0].render_width(table)
+            == 20 + 2 * _DEFAULT_CELL_X_PADDING
+        )
+        assert (
+            table.ordered_columns[1].render_width(table)
+            == 10 + 2 * _DEFAULT_CELL_X_PADDING
+        )


Code review comments:
diff_hunk: @@ -170,14 +171,18 @@ class Column:
     content_width: int = 0
     auto_width: bool = False
 
-    @property
-    def render_width(self) -> int:
-        """Width in cells, required to render a column."""
-        # +2 is to account for space padding either side of the cell
-        if self.auto_width:
-            return self.content_width + CELL_X_PADDING
-        else:
-            return self.width + CELL_X_PADDING
+    def render_width(self, data_table: DataTable[Any]) -> int:
path: src/textual/widgets/_data_table.py
text: @user1:
Since this is no longer a property, it should be named `"<VERB>_<NOUN>"`.  Suggest "get_render_width".

@author:
That's reasonable. See b84334c.