Repository: marimo-team/marimo
Pull request: fix: raise valueError for pageSize > 200
## 📝 Summary

<!--
Provide a concise summary of what this pull request is addressing.

If this PR fixes any issues, list them here by number (e.g., Fixes #123).
-->
Fixes #3407 . This will raise an error for `data_editor`, `dataframe` and `table` when the `page_size` configured is > 200.
<img width="450" alt="image" src="https://github.com/user-attachments/assets/8ec47f57-492b-45df-bff5-b5c2d481ebf1" />

## 🔍 Description of Changes

<!--
Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily.
-->

## 📋 Checklist

- [X] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md).
- [ ] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable).
- [X] I have added tests for the changes made.
- [X] I have run the code and verified that it works as expected.

## 📜 Reviewers

<!--
Tag potential reviewers from the community or maintainers who might be interested in reviewing this pull request.

Your PR will be reviewed more quickly if you can figure out the right person to tag with @ -->

@akshayka OR @mscolnick


Problem statement:
webpage crashes with large mo.ui.table page_size
### Describe the bug

When I use `mo.ui.table` with a large page_size config, the browser will crash. The only solution is to restart marimo. At least some fallback / error message / hardcode the limit is preferable

### Environment

<details>

```
{
  "marimo": "0.10.12",
  "OS": "Darwin",
  "OS Version": "23.6.0",
  "Processor": "arm",
  "Python Version": "3.13.1",
  "Binaries": {
    "Browser": "131.0.6778.265",
    "Node": "v23.2.0"
  },
  "Dependencies": {
    "click": "8.1.8",
    "docutils": "0.21.2",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.7",
    "narwhals": "1.21.1",
    "packaging": "24.2",
    "psutil": "6.1.1",
    "pygments": "2.19.1",
    "pymdown-extensions": "10.14",
    "pyyaml": "6.0.2",
    "ruff": "0.9.1",
    "starlette": "0.45.2",
    "tomlkit": "0.13.2",
    "typing-extensions": "4.12.2",
    "uvicorn": "0.34.0",
    "websockets": "14.1"
  },
  "Optional Dependencies": {
    "anywidget": "0.9.13",
    "duckdb": "1.1.3",
    "polars": "1.18.0",
    "pyarrow": "18.1.0"
  }
}
```

</details>


### Code to reproduce

```.py
@app.cell
def _(mo):
    random = mo.sql(
        f"""
        SELECT 
            1, 2, 3, 4, 5
        FROM generate_series(1,10000)
        """
    )
    return (random,)


@app.cell
def _(mo, random):
    mo.ui.table(random, page_size=10000)
    return
```

Hints from issue discussion:
My guess is from rendering react. We could add row virtualization ([example](https://tanstack.com/table/v8/docs/framework/react/examples/virtualized-rows)) when rows are e.g. > 500
In the meantime I am totally okay with raising a ValueError in Python when the page size is too big (eg, greater than 200)
Hey, sorry have been quite occupied recently. I tested Myles's solution w Marimo and yes it seems to be really smooth! This would add @tanstack/react-virtual lib.

Are we opting for this solution? Instead of hardcoding a page size limit. I imagine either solution is fine for most users 🤔
@Light2Dark - might be nice to avoid the complexity of virtualization and whatever other requests come up (not sure how complex your solution was, the additional dependency is ok).

I think a ValueError with "Table limited to 200 rows. If you'd like to this increased, please file an issue <github-url>". And then park this branch for later?

Open to thoughts, especially if the solution does carry a maintenance cost (still works with column pinning and other features).


yeap agreed, there is a decent amount of refactoring needed with virtualization. Add tableContainer, grid display. And the table component is used in many places.
@Light2Dark have you started the `ValueError`? (no worries if so) if not, @Haleshot was going to try to take a stab at it.
ah 😅, I finished it but wanted to test in the morning. But feel free to just take it up next time @Haleshot :)

Commit message: page size shouldn't accept None

Patch to review:
diff --git a/marimo/_plugins/ui/_impl/data_editor.py b/marimo/_plugins/ui/_impl/data_editor.py
index e2ffe629451..eca1ed2b437 100644
--- a/marimo/_plugins/ui/_impl/data_editor.py
+++ b/marimo/_plugins/ui/_impl/data_editor.py
@@ -25,6 +25,7 @@
 from marimo._output.rich_help import mddoc
 from marimo._plugins.ui._core.ui_element import UIElement
 from marimo._plugins.ui._impl.tables.utils import get_table_manager
+from marimo._plugins.validators import validate_page_size
 
 LOGGER = _loggers.marimo_logger()
 
@@ -142,6 +143,7 @@ def __init__(
             ]
         ] = None,
     ) -> None:
+        validate_page_size(page_size)
         table_manager = get_table_manager(data)
 
         size = table_manager.get_num_rows()
diff --git a/marimo/_plugins/ui/_impl/dataframes/dataframe.py b/marimo/_plugins/ui/_impl/dataframes/dataframe.py
index c346ec67046..7c2ca6e3c42 100644
--- a/marimo/_plugins/ui/_impl/dataframes/dataframe.py
+++ b/marimo/_plugins/ui/_impl/dataframes/dataframe.py
@@ -36,7 +36,10 @@
 from marimo._plugins.ui._impl.tables.utils import (
     get_table_manager,
 )
-from marimo._plugins.validators import validate_no_integer_columns
+from marimo._plugins.validators import (
+    validate_no_integer_columns,
+    validate_page_size,
+)
 from marimo._runtime.functions import EmptyArgs, Function
 from marimo._utils.memoize import memoize_last_value
 from marimo._utils.parse_dataclass import parse_raw
@@ -93,8 +96,7 @@ class dataframe(UIElement[Dict[str, Any], DataFrameType]):
 
     Args:
         df (DataFrameType): The DataFrame or series to transform.
-        page_size (Optional[int], optional): The number of rows to show in the table.
-            Defaults to 5.
+        page_size (int): The number of rows to show in the table. Defaults to 5.
         limit (Optional[int], optional): The number of items to load into memory, in case
             the data is remote and lazily fetched. This is likely true for SQL-backed
             dataframes via Ibis.
@@ -108,10 +110,11 @@ def __init__(
         self,
         df: DataFrameType,
         on_change: Optional[Callable[[DataFrameType], None]] = None,
-        page_size: Optional[int] = 5,
+        page_size: int = 5,
         limit: Optional[int] = None,
     ) -> None:
         validate_no_integer_columns(df)
+        validate_page_size(page_size)
         # This will raise an error if the dataframe type is not supported.
         handler = get_handler_for_dataframe(df)
 
diff --git a/marimo/_plugins/ui/_impl/table.py b/marimo/_plugins/ui/_impl/table.py
index 0df9236ab9c..d62837ff925 100644
--- a/marimo/_plugins/ui/_impl/table.py
+++ b/marimo/_plugins/ui/_impl/table.py
@@ -38,7 +38,10 @@
 )
 from marimo._plugins.ui._impl.tables.utils import get_table_manager
 from marimo._plugins.ui._impl.utils.dataframe import ListOrTuple, TableData
-from marimo._plugins.validators import validate_no_integer_columns
+from marimo._plugins.validators import (
+    validate_no_integer_columns,
+    validate_page_size,
+)
 from marimo._runtime.functions import EmptyArgs, Function
 from marimo._utils.narwhals_utils import unwrap_narwhals_dataframe
 
@@ -254,6 +257,7 @@ def __init__(
         _internal_total_rows: Optional[Union[int, Literal["too_many"]]] = None,
     ) -> None:
         validate_no_integer_columns(data)
+        validate_page_size(page_size)
 
         # The original data passed in
         self._data = data
diff --git a/marimo/_plugins/validators.py b/marimo/_plugins/validators.py
index 935d24b63eb..1682184f1ab 100644
--- a/marimo/_plugins/validators.py
+++ b/marimo/_plugins/validators.py
@@ -77,3 +77,11 @@ def validate_no_integer_columns(df: Any) -> None:
             "Please use strings for column names.",
             stacklevel=3,
         )
+
+
+def validate_page_size(page_size: int) -> None:
+    # We will need to support frontend row virtualization beyond this limit
+    if page_size > 200:
+        raise ValueError(
+            "Page size limited to 200 rows. If you'd like this increased, please file an issue"
+        )
diff --git a/tests/_plugins/ui/_impl/dataframes/test_dataframe.py b/tests/_plugins/ui/_impl/dataframes/test_dataframe.py
index 353120dea87..6f746b4ef0a 100644
--- a/tests/_plugins/ui/_impl/dataframes/test_dataframe.py
+++ b/tests/_plugins/ui/_impl/dataframes/test_dataframe.py
@@ -217,6 +217,13 @@ def test_dataframe_with_custom_page_size(df: Any) -> None:
         assert search_result.total_rows == 100
         assert search_result.data == result.url
 
+    @staticmethod
+    def test_dataframe_too_large_page_size() -> None:
+        df = pd.DataFrame({"A": range(300)})
+        with pytest.raises(ValueError) as e:
+            _ = ui.dataframe(df, page_size=201)
+        assert "limited to 200 rows" in str(e.value)
+
     @staticmethod
     def test_dataframe_with_non_string_column_names() -> None:
         df = pd.DataFrame(
diff --git a/tests/_plugins/ui/_impl/test_data_editor.py b/tests/_plugins/ui/_impl/test_data_editor.py
index c5028914041..f5416ae586f 100644
--- a/tests/_plugins/ui/_impl/test_data_editor.py
+++ b/tests/_plugins/ui/_impl/test_data_editor.py
@@ -394,6 +394,15 @@ def test_data_editor_with_custom_pagination():
     assert editor._component_args["page-size"] == 25
 
 
+@pytest.mark.skipif(
+    not DependencyManager.polars.has(), reason="Polars not installed"
+)
+def test_data_editor_with_too_large_pagesize():
+    data = [{"A": i} for i in range(300)]
+    with pytest.raises(ValueError):
+        _ = data_editor(data=data, page_size=201)
+
+
 @pytest.mark.skipif(
     not DependencyManager.polars.has(), reason="Polars not installed"
 )
diff --git a/tests/_plugins/ui/_impl/test_table.py b/tests/_plugins/ui/_impl/test_table.py
index e39673c4540..ae564304971 100644
--- a/tests/_plugins/ui/_impl/test_table.py
+++ b/tests/_plugins/ui/_impl/test_table.py
@@ -396,6 +396,13 @@ def test_table_with_too_many_rows_gets_clamped() -> None:
     assert len(table._component_args["data"]) == 10
 
 
+def test_table_too_large_pagesize_throws_error() -> None:
+    data = {"a": list(range(20_002))}
+    with pytest.raises(ValueError) as e:
+        _ = ui.table(data, page_size=201)
+    assert "limited to 200 rows" in str(e.value)
+
+
 def test_can_get_second_page() -> None:
     data = {"a": list(range(40))}
     table = ui.table(data)


Code review comments:
diff_hunk: @@ -108,10 +110,11 @@ def __init__(
         self,
         df: DataFrameType,
         on_change: Optional[Callable[[DataFrameType], None]] = None,
-        page_size: Optional[int] = 5,
+        page_size: int = 5,
path: marimo/_plugins/ui/_impl/dataframes/dataframe.py
text: @author:
page_size shouldn't accept None

@user1:
this might be a breaking change. if it is `None`, could we just use the df size (and still validate/error)

@author:
yeah, I should be careful. thanks!