Repository: streamlit/streamlit
Pull request: Extend dataframe-handling support for collection-like types
## Describe your changes

Extends the dataframe-handling support for a variety of (Python-native) collection-like types:

- dict.keys()
- dict.values()
- dict.items()
- collections.OrderedDict
- collections.defaultdict
- collections.Counter
- collections.deque
- collections.ChainMap
- Frozenset
- NamedTupel
- dataclasses
- frozenset
- range
- TypedDict
- (String) Enum
- Generator functions
- array.array
- MappingProxyType instances
- UserDict instances
- Pyarrow Array
- Pandas Array

This PR also contains a big refactoring of all the dataframe-handling-related unit tests by centralizing all tests to a single list of test cases that contain many data formats. In addition, this PR also contains a couple of smaller refactorings to better support all these types across all commands.

## GitHub Issue Link (if applicable)

- Closes https://github.com/streamlit/streamlit/issues/2737

## Testing Plan

- Updated unit tests

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
Support defaultdict as data source for built-in charts
### Problem
Charts that accept regular dicts, such as st.bar_chart, throw an error when given a defaultdict, which I would expect to work and just be treated as a normal one.

### Solution

**MVP:** 
Allow the use of a defaultdict by treating it as a dict, ignoring any unset keys.

**Possible additions:**
Could potentially try to do something to show missing keys as having the default value, if the present keys form a dense, mostly contiguous set.


### Additional context
Discovered this problem during my interview

---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**

Hints from issue discussion:

Commit message: Add test for custom dict check

Patch to review:
diff --git a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[chromium].png b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[chromium].png
index f137b417b521..b6228ac197bf 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[chromium].png and b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[firefox].png b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[firefox].png
index ea86118ec2ac..6a2b0a28e583 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[firefox].png and b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[webkit].png b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[webkit].png
index 5fb213317126..6e77d431d062 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[webkit].png and b/e2e_playwright/__snapshots__/linux/st_data_editor_input_data_test/st_data_editor-input_data_21[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[chromium].png b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[chromium].png
index aeef84c1d718..67c2ff535aa4 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[chromium].png and b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[firefox].png b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[firefox].png
index ddd9c642343a..62083d2941d6 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[firefox].png and b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[webkit].png b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[webkit].png
index 7156102c8994..7b1e3426b2c4 100644
Binary files a/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[webkit].png and b/e2e_playwright/__snapshots__/linux/st_dataframe_input_data_test/st_dataframe-input_data_21[webkit].png differ
diff --git a/lib/streamlit/dataframe_util.py b/lib/streamlit/dataframe_util.py
index ffcf6a286352..dc638e7f8faf 100644
--- a/lib/streamlit/dataframe_util.py
+++ b/lib/streamlit/dataframe_util.py
@@ -17,8 +17,14 @@
 from __future__ import annotations
 
 import contextlib
+import dataclasses
+import inspect
 import math
+import re
+from collections import ChainMap, UserDict, deque
+from collections.abc import ItemsView, KeysView, ValuesView
 from enum import Enum, EnumMeta, auto
+from types import MappingProxyType
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -34,9 +40,14 @@
 
 from typing_extensions import TypeAlias, TypeGuard
 
-import streamlit as st
 from streamlit import config, errors, logger, string_util
-from streamlit.type_util import is_type
+from streamlit.type_util import (
+    has_callable_attr,
+    is_custom_dict,
+    is_dataclass_instance,
+    is_namedtuple,
+    is_type,
+)
 
 if TYPE_CHECKING:
     import numpy as np
@@ -51,6 +62,7 @@
 # Maximum number of rows to request from an unevaluated (out-of-core) dataframe
 _MAX_UNEVALUATED_DF_ROWS = 10000
 
+_PANDAS_DATA_OBJECT_TYPE_RE: Final = re.compile(r"^pandas.*$")
 _PANDAS_STYLER_TYPE_STR: Final = "pandas.io.formats.style.Styler"
 _SNOWPARK_DF_TYPE_STR: Final = "snowflake.snowpark.dataframe.DataFrame"
 _SNOWPARK_DF_ROW_TYPE_STR: Final = "snowflake.snowpark.row.Row"
@@ -61,7 +73,6 @@
 _SNOWPANDAS_DF_TYPE_STR: Final = "snowflake.snowpark.modin.pandas.dataframe.DataFrame"
 _SNOWPANDAS_SERIES_TYPE_STR: Final = "snowflake.snowpark.modin.pandas.series.Series"
 
-
 V_co = TypeVar(
     "V_co",
     covariant=True,  # https://peps.python.org/pep-0484/#covariance-and-contravariance
@@ -111,9 +122,11 @@ class DataFormat(Enum):
     PANDAS_DATAFRAME = auto()  # pd.DataFrame
     PANDAS_SERIES = auto()  # pd.Series
     PANDAS_INDEX = auto()  # pd.Index
+    PANDAS_ARRAY = auto()  # pd.array
     NUMPY_LIST = auto()  # np.array[Scalar]
     NUMPY_MATRIX = auto()  # np.array[List[Scalar]]
     PYARROW_TABLE = auto()  # pyarrow.Table
+    PYARROW_ARRAY = auto()  # pyarrow.Array
     SNOWPARK_OBJECT = auto()  # Snowpark DataFrame, Table, List[Row]
     PYSPARK_OBJECT = auto()  # pyspark.DataFrame
     MODIN_OBJECT = auto()  # Modin DataFrame, Series
@@ -136,9 +149,9 @@ def is_dataframe_like(obj: object) -> bool:
     This does not include basic collection types like list, dict, tuple, etc.
     """
 
-    if obj is None or isinstance(
-        obj, (list, tuple, set, dict, str, bytes, int, float, bool)
-    ):
+    # We exclude list and dict here since there are some cases where a list or dict is
+    # considered a dataframe-like object.
+    if obj is None or isinstance(obj, (tuple, set, str, bytes, int, float, bool)):
         # Basic types are not considered dataframe-like, so we can
         # return False early to avoid unnecessary checks.
         return False
@@ -148,13 +161,16 @@ def is_dataframe_like(obj: object) -> bool:
         DataFormat.PANDAS_SERIES,
         DataFormat.PANDAS_INDEX,
         DataFormat.PANDAS_STYLER,
+        DataFormat.PANDAS_ARRAY,
         DataFormat.NUMPY_LIST,
         DataFormat.NUMPY_MATRIX,
         DataFormat.PYARROW_TABLE,
+        DataFormat.PYARROW_ARRAY,
         DataFormat.SNOWPARK_OBJECT,
         DataFormat.PYSPARK_OBJECT,
         DataFormat.MODIN_OBJECT,
         DataFormat.SNOWPANDAS_OBJECT,
+        DataFormat.COLUMN_SERIES_MAPPING,
     ]
 
 
@@ -166,6 +182,7 @@ def is_unevaluated_data_object(obj: object) -> bool:
     - PySpark DataFrame
     - Modin DataFrame / Series
     - Snowpandas DataFrame / Series
+    - Generator functions
 
     Unevaluated means that the data is not yet in the local memory.
     Unevaluated data objects are treated differently from other data objects by only
@@ -176,9 +193,15 @@ def is_unevaluated_data_object(obj: object) -> bool:
         or is_pyspark_data_object(obj)
         or is_snowpandas_data_object(obj)
         or is_modin_data_object(obj)
+        or inspect.isgeneratorfunction(obj)
     )
 
 
+def is_pandas_data_object(obj: object) -> bool:
+    """True if obj is a Pandas object (e.g. DataFrame, Series, Index, Styler, ...)."""
+    return is_type(obj, _PANDAS_DATA_OBJECT_TYPE_RE)
+
+
 def is_snowpark_data_object(obj: object) -> bool:
     """True if obj is a Snowpark DataFrame or Table."""
     return is_type(obj, _SNOWPARK_TABLE_TYPE_STR) or is_type(obj, _SNOWPARK_DF_TYPE_STR)
@@ -186,13 +209,12 @@ def is_snowpark_data_object(obj: object) -> bool:
 
 def is_snowpark_row_list(obj: object) -> bool:
     """True if obj is a list of snowflake.snowpark.row.Row."""
-    if not isinstance(obj, list):
-        return False
-    if len(obj) < 1:
-        return False
-    if not hasattr(obj[0], "__class__"):
-        return False
-    return is_type(obj[0], _SNOWPARK_DF_ROW_TYPE_STR)
+    return (
+        isinstance(obj, list)
+        and len(obj) > 0
+        and is_type(obj[0], _SNOWPARK_DF_ROW_TYPE_STR)
+        and has_callable_attr(obj[0], "as_dict")
+    )
 
 
 def is_pyspark_data_object(obj: object) -> bool:
@@ -221,6 +243,78 @@ def is_pandas_styler(obj: object) -> TypeGuard[Styler]:
     return is_type(obj, _PANDAS_STYLER_TYPE_STR)
 
 
+def _is_list_of_scalars(data: Iterable[Any]) -> bool:
+    """Check if the list only contains scalar values."""
+    from pandas.api.types import infer_dtype
+
+    # Overview on all value that are interpreted as scalar:
+    # https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_scalar.html
+    return infer_dtype(data, skipna=True) not in ["mixed", "unknown-array"]
+
+
+def _iterable_to_list(
+    iterable: Iterable[Any], max_iterations: int | None = None
+) -> list[Any]:
+    """Convert an iterable to a list.
+
+    Parameters
+    ----------
+    iterable : Iterable
+        The iterable to convert to a list.
+
+    max_iterations : int or None
+        The maximum number of iterations to perform. If None, all iterations are performed.
+
+    Returns
+    -------
+    list
+        The converted list.
+    """
+    if max_iterations is None:
+        return list(iterable)
+
+    result = []
+    for i, item in enumerate(iterable):
+        if i >= max_iterations:
+            break
+        result.append(item)
+    return result
+
+
+def _fix_column_naming(data_df: DataFrame) -> DataFrame:
+    """Rename the first column to "value" if it is not named
+    and if there is only one column in the dataframe.
+
+    The default name of the first column is 0 if it is not named
+    which is not very descriptive.
+    """
+
+    if len(data_df.columns) == 1 and data_df.columns[0] == 0:
+        # Pandas automatically names the first column with 0 if it is not named.
+        # We rename it to "value" to make it more descriptive if there is only
+        # one column in the dataframe.
+        data_df.rename(columns={0: "value"}, inplace=True)
+    return data_df
+
+
+def _dict_to_pandas_df(data: dict[Any, Any]) -> DataFrame:
+    """Convert a key-value dict to a Pandas DataFrame.
+
+    Parameters
+    ----------
+    data : dict
+        The dict to convert to a Pandas DataFrame.
+
+    Returns
+    -------
+    pandas.DataFrame
+        The converted Pandas DataFrame.
+    """
+    import pandas as pd
+
+    return _fix_column_naming(pd.DataFrame.from_dict(data, orient="index"))
+
+
 def convert_anything_to_pandas_df(
     data: Any,
     max_unevaluated_rows: int = _MAX_UNEVALUATED_DF_ROWS,
@@ -246,81 +340,123 @@ def convert_anything_to_pandas_df(
     pandas.DataFrame
 
     """
+    import array
+
     import numpy as np
     import pandas as pd
 
     if isinstance(data, pd.DataFrame):
         return data.copy() if ensure_copy else cast(pd.DataFrame, data)
 
-    if isinstance(data, (pd.Series, pd.Index)):
+    if isinstance(data, (pd.Series, pd.Index, pd.api.extensions.ExtensionArray)):
         return pd.DataFrame(data)
 
     if is_pandas_styler(data):
         return cast(pd.DataFrame, data.data.copy() if ensure_copy else data.data)
 
     if isinstance(data, np.ndarray):
-        return pd.DataFrame([]) if len(data.shape) == 0 else pd.DataFrame(data)
+        return (
+            pd.DataFrame([])
+            if len(data.shape) == 0
+            else _fix_column_naming(pd.DataFrame(data))
+        )
 
     if is_modin_data_object(data):
         data = data.head(max_unevaluated_rows)._to_pandas()
 
-        if isinstance(data, pd.Series):
+        if isinstance(data, (pd.Series, pd.Index)):
             data = data.to_frame()
 
         if data.shape[0] == max_unevaluated_rows:
-            st.caption(
+            _show_data_information(
                 f"⚠️ Showing only {string_util.simplify_number(max_unevaluated_rows)} "
-                "rows. Call `_to_pandas()` on the dataframe to show more."
+                "rows. Call `_to_pandas()` on the data object to show more."
             )
         return cast(pd.DataFrame, data)
 
     if is_pyspark_data_object(data):
         data = data.limit(max_unevaluated_rows).toPandas()
         if data.shape[0] == max_unevaluated_rows:
-            st.caption(
+            _show_data_information(
+                f"⚠️ Showing only {string_util.simplify_number(max_unevaluated_rows)} "
+                "rows. Call `toPandas()` on the data object to show more."
+            )
+        return cast(pd.DataFrame, data)
+
+    if is_snowpandas_data_object(data):
+        data = data[:max_unevaluated_rows].to_pandas()
+
+        if isinstance(data, (pd.Series, pd.Index)):
+            data = data.to_frame()
+
+        if data.shape[0] == max_unevaluated_rows:
+            _show_data_information(
                 f"⚠️ Showing only {string_util.simplify_number(max_unevaluated_rows)} "
-                "rows. Call `toPandas()` on the dataframe to show more."
+                "rows. Call `to_pandas()` on the data object to show more."
             )
         return cast(pd.DataFrame, data)
 
     if is_snowpark_data_object(data):
         data = data.limit(max_unevaluated_rows).to_pandas()
         if data.shape[0] == max_unevaluated_rows:
-            st.caption(
+            _show_data_information(
                 f"⚠️ Showing only {string_util.simplify_number(max_unevaluated_rows)} "
-                "rows. Call `to_pandas()` on the dataframe to show more."
+                "rows. Call `to_pandas()` on the data object to show more."
             )
         return cast(pd.DataFrame, data)
 
-    if is_snowpandas_data_object(data):
-        data = data.head(max_unevaluated_rows).to_pandas()
+    if is_snowpark_row_list(data):
+        return pd.DataFrame([row.as_dict() for row in data])
 
-        if isinstance(data, pd.Series):
-            data = data.to_frame()
+    if has_callable_attr(data, "to_pandas"):
+        return pd.DataFrame(data.to_pandas())
+
+    # Support for generator functions
+    if inspect.isgeneratorfunction(data):
+        data = _fix_column_naming(
+            pd.DataFrame(_iterable_to_list(data(), max_iterations=max_unevaluated_rows))
+        )
 
         if data.shape[0] == max_unevaluated_rows:
-            st.caption(
+            _show_data_information(
                 f"⚠️ Showing only {string_util.simplify_number(max_unevaluated_rows)} "
-                "rows. Call `to_pandas()` on the dataframe to show more."
+                "rows. Convert the data to a list to show more."
             )
-        return cast(pd.DataFrame, data)
+        return data
 
-    # This is inefficient when data is a pyarrow.Table as it will be converted
-    # back to Arrow when marshalled to protobuf, but area/bar/line charts need
-    # DataFrame magic to generate the correct output.
-    if hasattr(data, "to_pandas"):
-        return pd.DataFrame(data.to_pandas())
+    if isinstance(data, EnumMeta):
+        # Support for enum classes
+        return _fix_column_naming(pd.DataFrame([c.value for c in data]))  # type: ignore
+
+    # Support for some list like objects
+    if isinstance(data, (deque, map, array.ArrayType)):
+        return _fix_column_naming(pd.DataFrame(list(data)))
+
+    # Support for Streamlit's custom dict-like objects
+    if is_custom_dict(data):
+        return _dict_to_pandas_df(data.to_dict())
+
+    # Support for named tuples
+    if is_namedtuple(data):
+        return _dict_to_pandas_df(data._asdict())
+
+    # Support for dataclass instances
+    if is_dataclass_instance(data):
+        return _dict_to_pandas_df(dataclasses.asdict(data))
+
+    # Support for dict-like objects
+    if isinstance(data, (ChainMap, MappingProxyType, UserDict)):
+        return _dict_to_pandas_df(dict(data))
 
     # Try to convert to pandas.DataFrame. This will raise an error is df is not
     # compatible with the pandas.DataFrame constructor.
     try:
-        return pd.DataFrame(data)
-
+        return _fix_column_naming(pd.DataFrame(data))
     except ValueError as ex:
         if isinstance(data, dict):
             with contextlib.suppress(ValueError):
                 # Try to use index orient as back-up to support key-value dicts
-                return pd.DataFrame.from_dict(data, orient="index")
+                return _dict_to_pandas_df(data)
         raise errors.StreamlitAPIException(
             f"""
 Unable to convert object of type `{type(data)}` to `pandas.DataFrame`.
@@ -419,6 +555,14 @@ def convert_arrow_bytes_to_pandas_df(source: bytes) -> DataFrame:
     return reader.read_pandas()
 
 
+def _show_data_information(msg: str) -> None:
+    """Show a message to the user with important information
+    about the processed dataset."""
+    from streamlit.delta_generator import main_dg
+
+    main_dg.caption(msg)
+
+
 def convert_anything_to_arrow_bytes(
     data: Any,
     max_unevaluated_rows: int = _MAX_UNEVALUATED_DF_ROWS,
@@ -449,8 +593,16 @@ def convert_anything_to_arrow_bytes(
     if isinstance(data, pa.Table):
         return convert_arrow_table_to_arrow_bytes(data)
 
+    if is_pandas_data_object(data):
+        # All pandas data objects should be handled via our pandas
+        # conversion logic. We are already calling it here
+        # to ensure that its not handled via the interchange
+        # protocol support below.
+        df = convert_anything_to_pandas_df(data, max_unevaluated_rows)
+        return convert_pandas_df_to_arrow_bytes(df)
+
     # Fallback: try to convert to pandas DataFrame
-    # and then to Arrow bytes
+    # and then to Arrow bytes.
     df = convert_anything_to_pandas_df(data, max_unevaluated_rows)
     return convert_pandas_df_to_arrow_bytes(df)
 
@@ -475,7 +627,9 @@ def convert_anything_to_sequence(obj: OptionSequence[V_co]) -> Sequence[V_co]:
     if obj is None:
         return []  # type: ignore
 
-    if isinstance(obj, (str, list, tuple, set, range, EnumMeta)):
+    if isinstance(
+        obj, (str, list, tuple, set, range, EnumMeta, deque, map)
+    ) and not is_snowpark_row_list(obj):
         # This also ensures that the sequence is copied to prevent
         # potential mutations to the original object.
         return list(obj)
@@ -569,8 +723,7 @@ def _maybe_truncate_table(
                 # we just display the exact numbers.
                 displayed_rows = str(table.num_rows)
                 total_rows = str(table.num_rows + truncated_rows)
-
-            st.caption(
+            _show_data_information(
                 f"⚠️ Showing {displayed_rows} out of {total_rows} "
                 "rows due to data size limitations."
             )
@@ -579,7 +732,8 @@ def _maybe_truncate_table(
 
 
 def is_colum_type_arrow_incompatible(column: Series[Any] | Index) -> bool:
-    """Return True if the column type is known to cause issues during Arrow conversion."""
+    """Return True if the column type is known to cause issues during
+    Arrow conversion."""
     from pandas.api.types import infer_dtype, is_dict_like, is_list_like
 
     if column.dtype.kind in [
@@ -610,7 +764,8 @@ def is_colum_type_arrow_incompatible(column: Series[Any] | Index) -> bool:
         ]:
             return True
         elif inferred_type == "mixed":
-            # This includes most of the more complex/custom types (objects, dicts, lists, ...)
+            # This includes most of the more complex/custom types (objects, dicts,
+            # lists, ...)
             if len(column) == 0 or not hasattr(column, "iloc"):
                 # The column seems to be invalid, so we assume it is incompatible.
                 # But this would most likely never happen since empty columns
@@ -622,7 +777,8 @@ def is_colum_type_arrow_incompatible(column: Series[Any] | Index) -> bool:
 
             if (
                 not is_list_like(first_value)
-                # dicts are list-like, but have issues in Arrow JS (see comments in Quiver.ts)
+                # dicts are list-like, but have issues in Arrow JS (see comments in
+                # Quiver.ts)
                 or is_dict_like(first_value)
                 # Frozensets are list-like, but are not compatible with pyarrow.
                 or isinstance(first_value, frozenset)
@@ -684,15 +840,6 @@ def fix_arrow_incompatible_column_types(
     return df_copy if df_copy is not None else df
 
 
-def _is_list_of_scalars(data: Iterable[Any]) -> bool:
-    """Check if the list only contains scalar values."""
-    from pandas.api.types import infer_dtype
-
-    # Overview on all value that are interpreted as scalar:
-    # https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_scalar.html
-    return infer_dtype(data, skipna=True) not in ["mixed", "unknown-array"]
-
-
 def determine_data_format(input_data: Any) -> DataFormat:
     """Determine the data format of the input data.
 
@@ -706,6 +853,8 @@ def determine_data_format(input_data: Any) -> DataFormat:
     DataFormat
         The data format of the input data.
     """
+    import array
+
     import numpy as np
     import pandas as pd
     import pyarrow as pa
@@ -722,26 +871,43 @@ def determine_data_format(input_data: Any) -> DataFormat:
         return DataFormat.NUMPY_MATRIX
     elif isinstance(input_data, pa.Table):
         return DataFormat.PYARROW_TABLE
+    elif isinstance(input_data, pa.Array):
+        return DataFormat.PYARROW_ARRAY
     elif isinstance(input_data, pd.Series):
         return DataFormat.PANDAS_SERIES
     elif isinstance(input_data, pd.Index):
         return DataFormat.PANDAS_INDEX
     elif is_pandas_styler(input_data):
         return DataFormat.PANDAS_STYLER
-    elif is_snowpark_data_object(input_data):
-        return DataFormat.SNOWPARK_OBJECT
+    elif isinstance(input_data, pd.api.extensions.ExtensionArray):
+        return DataFormat.PANDAS_ARRAY
     elif is_modin_data_object(input_data):
         return DataFormat.MODIN_OBJECT
     elif is_snowpandas_data_object(input_data):
         return DataFormat.SNOWPANDAS_OBJECT
     elif is_pyspark_data_object(input_data):
         return DataFormat.PYSPARK_OBJECT
-    elif isinstance(input_data, (list, tuple, set)):
+    elif is_snowpark_data_object(input_data) or is_snowpark_row_list(input_data):
+        return DataFormat.SNOWPARK_OBJECT
+    elif isinstance(
+        input_data, (range, EnumMeta, KeysView, ValuesView, deque, map, array.ArrayType)
+    ):
+        return DataFormat.LIST_OF_VALUES
+    elif (
+        isinstance(input_data, (ChainMap, MappingProxyType, UserDict))
+        or is_dataclass_instance(input_data)
+        or is_namedtuple(input_data)
+        or is_custom_dict(input_data)
+    ):
+        return DataFormat.KEY_VALUE_DICT
+    elif isinstance(input_data, (ItemsView, enumerate)):
+        return DataFormat.LIST_OF_ROWS
+    elif isinstance(input_data, (list, tuple, set, frozenset)):
         if _is_list_of_scalars(input_data):
             # -> one-dimensional data structure
             if isinstance(input_data, tuple):
                 return DataFormat.TUPLE_OF_VALUES
-            if isinstance(input_data, set):
+            if isinstance(input_data, (set, frozenset)):
                 return DataFormat.SET_OF_VALUES
             return DataFormat.LIST_OF_VALUES
         else:
@@ -751,23 +917,23 @@ def determine_data_format(input_data: Any) -> DataFormat:
             first_element = next(iter(input_data))
             if isinstance(first_element, dict):
                 return DataFormat.LIST_OF_RECORDS
-            if isinstance(first_element, (list, tuple, set)):
+            if isinstance(first_element, (list, tuple, set, frozenset)):
                 return DataFormat.LIST_OF_ROWS
     elif isinstance(input_data, dict):
         if not input_data:
             return DataFormat.KEY_VALUE_DICT
         if len(input_data) > 0:
             first_value = next(iter(input_data.values()))
+            # In the future, we could potentially also support tight & split formats
             if isinstance(first_value, dict):
                 return DataFormat.COLUMN_INDEX_MAPPING
             if isinstance(first_value, (list, tuple)):
                 return DataFormat.COLUMN_VALUE_MAPPING
             if isinstance(first_value, pd.Series):
                 return DataFormat.COLUMN_SERIES_MAPPING
-            # In the future, we could potentially also support the tight & split formats here
-            if _is_list_of_scalars(input_data.values()):
-                # Only use the key-value dict format if the values are only scalar values
-                return DataFormat.KEY_VALUE_DICT
+            # Use key-value dict as fallback. However, if the values of the dict
+            # contains mixed types, it will become non-editable in the frontend.
+            return DataFormat.KEY_VALUE_DICT
     return DataFormat.UNKNOWN
 
 
@@ -783,12 +949,30 @@ def _unify_missing_values(df: DataFrame) -> DataFrame:
     return df.fillna(np.nan).replace([np.nan], [None])
 
 
+def _pandas_df_to_series(df: DataFrame) -> Series[Any]:
+    """Convert a Pandas DataFrame to a Pandas Series by selecting the first column.
+
+    Raises
+    ------
+    ValueError
+        If the DataFrame has more than one column.
+    """
+    # Select first column in dataframe and create a new series based on the values
+    if len(df.columns) != 1:
+        raise ValueError(
+            "DataFrame is expected to have a single column but "
+            f"has {len(df.columns)}."
+        )
+    return df[df.columns[0]]
+
+
 def convert_pandas_df_to_data_format(
     df: DataFrame, data_format: DataFormat
 ) -> (
     DataFrame
     | Series[Any]
     | pa.Table
+    | pa.Array
     | np.ndarray[Any, np.dtype[Any]]
     | tuple[Any]
     | list[Any]
@@ -818,6 +1002,7 @@ def convert_pandas_df_to_data_format(
         DataFormat.PYSPARK_OBJECT,
         DataFormat.PANDAS_INDEX,
         DataFormat.PANDAS_STYLER,
+        DataFormat.PANDAS_ARRAY,
         DataFormat.MODIN_OBJECT,
         DataFormat.SNOWPANDAS_OBJECT,
     ]:
@@ -838,13 +1023,12 @@ def convert_pandas_df_to_data_format(
         import pyarrow as pa
 
         return pa.Table.from_pandas(df)
+    elif data_format == DataFormat.PYARROW_ARRAY:
+        import pyarrow as pa
+
+        return pa.Array.from_pandas(_pandas_df_to_series(df))
     elif data_format == DataFormat.PANDAS_SERIES:
-        # Select first column in dataframe and create a new series based on the values
-        if len(df.columns) != 1:
-            raise ValueError(
-                f"DataFrame is expected to have a single column but has {len(df.columns)}."
-            )
-        return df[df.columns[0]]
+        return _pandas_df_to_series(df)
     elif data_format == DataFormat.LIST_OF_RECORDS:
         return _unify_missing_values(df).to_dict(orient="records")
     elif data_format == DataFormat.LIST_OF_ROWS:
@@ -868,7 +1052,8 @@ def convert_pandas_df_to_data_format(
             return_list = df[df.columns[0]].tolist()
         elif len(df.columns) >= 1:
             raise ValueError(
-                f"DataFrame is expected to have a single column but has {len(df.columns)}."
+                "DataFrame is expected to have a single column but "
+                f"has {len(df.columns)}."
             )
         if data_format == DataFormat.TUPLE_OF_VALUES:
             return tuple(return_list)
diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py
index 063b5c385997..e05fa0a0c0e6 100644
--- a/lib/streamlit/delta_generator.py
+++ b/lib/streamlit/delta_generator.py
@@ -708,7 +708,8 @@ def _prep_data_for_add_rows(
 ) -> tuple[Data, AddRowsMetadata | None]:
     if not add_rows_metadata:
         if dataframe_util.is_pandas_styler(data):
-            # When calling add_rows on st.table or st.dataframe we want styles to pass through.
+            # When calling add_rows on st.table or st.dataframe we want styles to
+            # pass through.
             return data, None
         return dataframe_util.convert_anything_to_pandas_df(data), None
 
diff --git a/lib/streamlit/elements/arrow.py b/lib/streamlit/elements/arrow.py
index ef978847530a..d5f5b8a96a42 100644
--- a/lib/streamlit/elements/arrow.py
+++ b/lib/streamlit/elements/arrow.py
@@ -513,12 +513,7 @@ def dataframe(
             data_df = dataframe_util.convert_anything_to_pandas_df(
                 data, ensure_copy=False
             )
-            apply_data_specific_configs(
-                column_config_mapping,
-                data_df,
-                data_format,
-                check_arrow_compatibility=False,
-            )
+            apply_data_specific_configs(column_config_mapping, data_format)
             # Serialize the data to bytes:
             proto.data = dataframe_util.convert_pandas_df_to_arrow_bytes(data_df)
 
@@ -599,8 +594,10 @@ def table(self, data: Data = None) -> DeltaGenerator:
 
         """
 
-        # Check if data is uncollected, and collect it but with 100 rows max, instead of 10k rows, which is done in all other cases.
-        # Avoid this and use 100 rows in st.table, because large tables render slowly, take too much screen space, and can crush the app.
+        # Check if data is uncollected, and collect it but with 100 rows max, instead of
+        # 10k rows, which is done in all other cases.
+        # We usse 100 rows in st.table, because large tables render slowly,
+        # take too much screen space, and can crush the app.
         if dataframe_util.is_unevaluated_data_object(data):
             data = dataframe_util.convert_anything_to_pandas_df(
                 data, max_unevaluated_rows=100
diff --git a/lib/streamlit/elements/json.py b/lib/streamlit/elements/json.py
index 79b2aec908d9..cbcd9bf33053 100644
--- a/lib/streamlit/elements/json.py
+++ b/lib/streamlit/elements/json.py
@@ -15,13 +15,13 @@
 from __future__ import annotations
 
 import json
+import types
+from collections import ChainMap, UserDict
 from typing import TYPE_CHECKING, Any, cast
 
 from streamlit.proto.Json_pb2 import Json as JsonProto
-from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
 from streamlit.runtime.metrics_util import gather_metrics
-from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
-from streamlit.user_info import UserInfoProxy
+from streamlit.type_util import is_custom_dict, is_namedtuple
 
 if TYPE_CHECKING:
     from streamlit.delta_generator import DeltaGenerator
@@ -79,18 +79,18 @@ def json(
         """
         import streamlit as st
 
-        if isinstance(
-            body,
-            (
-                SessionStateProxy,
-                UserInfoProxy,
-                QueryParamsProxy,
-                StreamlitHeaders,
-                StreamlitCookies,
-            ),
-        ):
+        if is_custom_dict(body):
             body = body.to_dict()
 
+        if is_namedtuple(body):
+            body = body._asdict()
+
+        if isinstance(body, (map, enumerate)):
+            body = list(body)
+
+        if isinstance(body, (ChainMap, types.MappingProxyType, UserDict)):
+            body = dict(body)
+
         if not isinstance(body, str):
             try:
                 # Serialize body to string and try to interpret sets as lists
diff --git a/lib/streamlit/elements/lib/column_config_utils.py b/lib/streamlit/elements/lib/column_config_utils.py
index 8e39d3f0f4a8..4c27c47241c7 100644
--- a/lib/streamlit/elements/lib/column_config_utils.py
+++ b/lib/streamlit/elements/lib/column_config_utils.py
@@ -21,7 +21,7 @@
 
 from typing_extensions import TypeAlias
 
-from streamlit.dataframe_util import DataFormat, is_colum_type_arrow_incompatible
+from streamlit.dataframe_util import DataFormat
 from streamlit.elements.lib.column_types import ColumnConfig, ColumnType
 from streamlit.elements.lib.dicttools import remove_none_values
 from streamlit.errors import StreamlitAPIException
@@ -466,9 +466,7 @@ def update_column_config(
 
 def apply_data_specific_configs(
     columns_config: ColumnConfigMapping,
-    data_df: DataFrame,
     data_format: DataFormat,
-    check_arrow_compatibility: bool = False,
 ) -> None:
     """Apply data specific configurations to the provided dataframe.
 
@@ -480,24 +478,9 @@ def apply_data_specific_configs(
     columns_config : ColumnConfigMapping
         A mapping of column names/ids to column configurations.
 
-    data_df : pd.DataFrame
-        The dataframe to apply the configurations to.
-
     data_format : DataFormat
         The format of the data.
-
-    check_arrow_compatibility : bool
-        Whether to check if the data is compatible with arrow.
     """
-    import pandas as pd
-
-    # Deactivate editing for columns that are not compatible with arrow
-    if check_arrow_compatibility:
-        for column_name, column_data in data_df.items():
-            if is_colum_type_arrow_incompatible(column_data):
-                update_column_config(columns_config, column_name, {"disabled": True})
-                # Convert incompatible type to string
-                data_df[column_name] = column_data.astype("string")
 
     # Pandas adds a range index as default to all datastructures
     # but for most of the non-pandas data objects it is unnecessary
@@ -514,23 +497,6 @@ def apply_data_specific_configs(
     ]:
         update_column_config(columns_config, INDEX_IDENTIFIER, {"hidden": True})
 
-    # Rename the first column to "value" for some of the data formats
-    if data_format in [
-        DataFormat.SET_OF_VALUES,
-        DataFormat.TUPLE_OF_VALUES,
-        DataFormat.LIST_OF_VALUES,
-        DataFormat.NUMPY_LIST,
-        DataFormat.KEY_VALUE_DICT,
-    ]:
-        # Pandas automatically names the first column "0"
-        # We rename it to "value" in selected cases to make it more descriptive
-        data_df.rename(columns={0: "value"}, inplace=True)
-
-    if not isinstance(data_df.index, pd.RangeIndex):
-        # If the index is not a range index, we will configure it as required
-        # since the user is required to provide a (unique) value for editing.
-        update_column_config(columns_config, INDEX_IDENTIFIER, {"required": True})
-
 
 def _convert_column_config_to_json(column_config_mapping: ColumnConfigMapping) -> str:
     try:
diff --git a/lib/streamlit/elements/widgets/data_editor.py b/lib/streamlit/elements/widgets/data_editor.py
index d9241790b80c..e81996158b54 100644
--- a/lib/streamlit/elements/widgets/data_editor.py
+++ b/lib/streamlit/elements/widgets/data_editor.py
@@ -817,15 +817,31 @@ def data_editor(
 
         # Convert the user provided column config into the frontend compatible format:
         column_config_mapping = process_config_mapping(column_config)
-        apply_data_specific_configs(
-            column_config_mapping, data_df, data_format, check_arrow_compatibility=True
-        )
+
+        # Deactivate editing for columns that are not compatible with arrow
+        for column_name, column_data in data_df.items():
+            if dataframe_util.is_colum_type_arrow_incompatible(column_data):
+                update_column_config(
+                    column_config_mapping, column_name, {"disabled": True}
+                )
+                # Convert incompatible type to string
+                data_df[column_name] = column_data.astype("string")
+
+        apply_data_specific_configs(column_config_mapping, data_format)
 
         # Fix the column headers to work correctly for data editing:
         _fix_column_headers(data_df)
-        # Temporary workaround: We hide range indices if num_rows is dynamic.
-        # since the current way of handling this index during editing is a bit confusing.
-        if isinstance(data_df.index, pd.RangeIndex) and num_rows == "dynamic":
+
+        if not isinstance(data_df.index, pd.RangeIndex):
+            # If the index is not a range index, we will configure it as required
+            # since the user is required to provide a (unique) value for editing.
+            update_column_config(
+                column_config_mapping, INDEX_IDENTIFIER, {"required": True}
+            )
+        elif num_rows == "dynamic":
+            # Temporary workaround: We hide range indices if num_rows is dynamic.
+            # since the current way of handling this index during editing is a bit
+            # confusing.
             update_column_config(
                 column_config_mapping, INDEX_IDENTIFIER, {"hidden": True}
             )
diff --git a/lib/streamlit/elements/write.py b/lib/streamlit/elements/write.py
index 5046d8fb0394..d06dd9ef4a28 100644
--- a/lib/streamlit/elements/write.py
+++ b/lib/streamlit/elements/write.py
@@ -16,23 +16,20 @@
 
 import dataclasses
 import inspect
-import json
 import types
+from collections import ChainMap, UserDict
 from io import StringIO
 from typing import TYPE_CHECKING, Any, Callable, Final, Generator, Iterable, List, cast
 
 from streamlit import dataframe_util, type_util
 from streamlit.errors import StreamlitAPIException
 from streamlit.logger import get_logger
-from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
 from streamlit.runtime.metrics_util import gather_metrics
-from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
 from streamlit.string_util import (
     is_mem_address_str,
     max_char_sequence,
     probably_contains_html_tags,
 )
-from streamlit.user_info import UserInfoProxy
 
 if TYPE_CHECKING:
     from streamlit.delta_generator import DeltaGenerator
@@ -252,7 +249,8 @@ def write(self, *args: Any, unsafe_allow_html: bool = False, **kwargs) -> None:
             - write(string)         : Prints the formatted Markdown string, with
                 support for LaTeX expression, emoji shortcodes, and colored text.
                 See docs for st.markdown for more.
-            - write(data_frame)     : Displays the DataFrame as a table.
+            - write(data_frame)     : Displays any dataframe-compatible value
+                as read-only table.
             - write(error)          : Prints an exception specially.
             - write(func)           : Displays information about a function.
             - write(module)         : Displays information about the module.
@@ -408,9 +406,7 @@ def flush_buffer():
             elif isinstance(arg, Exception):
                 flush_buffer()
                 self.dg.exception(arg)
-            elif dataframe_util.is_dataframe_like(
-                arg
-            ) or dataframe_util.is_snowpark_row_list(arg):
+            elif dataframe_util.is_dataframe_like(arg):
                 flush_buffer()
                 self.dg.dataframe(arg)
             elif type_util.is_altair_chart(arg):
@@ -440,23 +436,24 @@ def flush_buffer():
                 flush_buffer()
                 dot = vis_utils.model_to_dot(arg)
                 self.dg.graphviz_chart(dot.to_string())
-            elif isinstance(
-                arg,
-                (
-                    dict,
-                    list,
-                    SessionStateProxy,
-                    UserInfoProxy,
-                    QueryParamsProxy,
-                    StreamlitHeaders,
-                    StreamlitCookies,
-                ),
+            elif (
+                isinstance(
+                    arg,
+                    (
+                        dict,
+                        list,
+                        map,
+                        enumerate,
+                        types.MappingProxyType,
+                        UserDict,
+                        ChainMap,
+                    ),
+                )
+                or type_util.is_custom_dict(arg)
+                or type_util.is_namedtuple(arg)
             ):
                 flush_buffer()
                 self.dg.json(arg)
-            elif type_util.is_namedtuple(arg):
-                flush_buffer()
-                self.dg.json(json.dumps(arg._asdict()))
             elif type_util.is_pydeck(arg):
                 flush_buffer()
                 self.dg.pydeck_chart(arg)
@@ -482,16 +479,12 @@ def flush_buffer():
                 # https://github.com/python/mypy/issues/12933
                 self.dg.help(cast(type, arg))
             elif (
-                hasattr(arg, "_repr_html_")
-                and callable(arg._repr_html_)
+                type_util.has_callable_attr(arg, "_repr_html_")
                 and (repr_html := arg._repr_html_())
                 and (unsafe_allow_html or not probably_contains_html_tags(repr_html))
             ):
                 # We either explicitly allow HTML or infer it's not HTML
                 self.dg.markdown(repr_html, unsafe_allow_html=unsafe_allow_html)
-            elif type_util.is_streamlit_secrets_class(arg):
-                flush_buffer()
-                self.dg.json(arg.to_dict())
             else:
                 stringified_arg = str(arg)
 
diff --git a/lib/streamlit/runtime/caching/cache_utils.py b/lib/streamlit/runtime/caching/cache_utils.py
index 97a86e8654c1..51e2d8a38dcd 100644
--- a/lib/streamlit/runtime/caching/cache_utils.py
+++ b/lib/streamlit/runtime/caching/cache_utils.py
@@ -299,20 +299,23 @@ def _handle_cache_miss(
             try:
                 cache.write_result(value_key, computed_value, messages)
                 return computed_value
-            except (CacheError, RuntimeError):
-                # An exception was thrown while we tried to write to the cache. Report it to the user.
-                # (We catch `RuntimeError` here because it will be raised by Apache Spark if we do not
-                # collect dataframe before using `st.cache_data`.)
+            except (CacheError, RuntimeError) as ex:
+                # An exception was thrown while we tried to write to the cache. Report
+                # it to the user. (We catch `RuntimeError` here because it will be
+                # raised by Apache Spark if we do not collect dataframe before
+                # using `st.cache_data`.)
                 if is_unevaluated_data_object(computed_value):
                     # If the returned value is an unevaluated dataframe, raise an error.
                     # Unevaluated dataframes are not yet in the local memory, which also
                     # means they cannot be properly cached (serialized).
                     raise UnevaluatedDataFrameError(
-                        f"""
-                        The function {get_cached_func_name_md(self._info.func)} is decorated with `st.cache_data` but it returns an unevaluated dataframe
-                        of type `{type_util.get_fqn_type(computed_value)}`. Please call `collect()` or `to_pandas()` on the dataframe before returning it,
-                        so `st.cache_data` can serialize and cache it."""
-                    )
+                        f"The function {get_cached_func_name_md(self._info.func)} is "
+                        "decorated with `st.cache_data` but it returns an unevaluated "
+                        f"data object of type `{type_util.get_fqn_type(computed_value)}`. "
+                        "Please convert the object to a serializable format "
+                        "(e.g. Pandas DataFrame) before returning it, so "
+                        "`st.cache_data` can serialize and cache it."
+                    ) from ex
                 raise UnserializableReturnValueError(
                     return_value=computed_value, func=self._info.func
                 )
diff --git a/lib/streamlit/type_util.py b/lib/streamlit/type_util.py
index 8f3d169d4894..1985deacb107 100644
--- a/lib/streamlit/type_util.py
+++ b/lib/streamlit/type_util.py
@@ -16,6 +16,7 @@
 
 from __future__ import annotations
 
+import dataclasses
 import re
 import types
 from typing import (
@@ -42,7 +43,6 @@
     from plotly.graph_objs import Figure
     from pydeck import Deck
 
-    from streamlit.runtime.secrets import Secrets
 
 T = TypeVar("T")
 
@@ -51,6 +51,16 @@ class SupportsStr(Protocol):
     def __str__(self) -> str: ...
 
 
+class CustomDict(Protocol):
+    """Protocol for Streamlit native custom dictionaries (e.g. session state, secrets, query params).
+    that can be converted to a dict.
+
+    All these implementations should provide a to_dict method.
+    """
+
+    def to_dict(self) -> dict[str, Any]: ...
+
+
 @overload
 def is_type(
     obj: object, fqn_type_pattern: Literal["pydeck.bindings.deck.Deck"]
@@ -246,15 +256,22 @@ def is_function(x: object) -> TypeGuard[types.FunctionType]:
     return isinstance(x, types.FunctionType)
 
 
+def has_callable_attr(obj: object, name: str) -> bool:
+    """True if obj has the specified attribute that is callable."""
+    return hasattr(obj, name) and callable(getattr(obj, name))
+
+
 def is_namedtuple(x: object) -> TypeGuard[NamedTuple]:
-    t = type(x)
-    b = t.__bases__
-    if len(b) != 1 or b[0] is not tuple:
-        return False
-    f = getattr(t, "_fields", None)
-    if not isinstance(f, tuple):
-        return False
-    return all(type(n).__name__ == "str" for n in f)
+    """True if obj is an instance of a namedtuple."""
+    return isinstance(x, tuple) and has_callable_attr(x, "_asdict")
+
+
+def is_dataclass_instance(obj: object) -> bool:
+    """True if obj is an instance of a dataclass."""
+    # The not isinstance(obj, type) check is needed to make sure that this
+    # is an instance of a dataclass and not the class itself.
+    # dataclasses.is_dataclass returns True for either instance or class.
+    return dataclasses.is_dataclass(obj) and not isinstance(obj, type)
 
 
 def is_pydeck(obj: object) -> TypeGuard[Deck]:
@@ -262,6 +279,26 @@ def is_pydeck(obj: object) -> TypeGuard[Deck]:
     return is_type(obj, "pydeck.bindings.deck.Deck")
 
 
+def is_custom_dict(obj: object) -> TypeGuard[CustomDict]:
+    """True if input looks like one of the Streamlit custom dictionaries."""
+    from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
+    from streamlit.runtime.secrets import Secrets
+    from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
+    from streamlit.user_info import UserInfoProxy
+
+    return isinstance(
+        obj,
+        (
+            SessionStateProxy,
+            UserInfoProxy,
+            QueryParamsProxy,
+            StreamlitHeaders,
+            StreamlitCookies,
+            Secrets,
+        ),
+    ) and has_callable_attr(obj, "to_dict")
+
+
 def is_iterable(obj: object) -> TypeGuard[Iterable[Any]]:
     try:
         # The ignore statement here is intentional, as this is a
@@ -272,11 +309,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[Any]]:
     return True
 
 
-def is_streamlit_secrets_class(obj: object) -> TypeGuard[Secrets]:
-    """True if obj is a Streamlit Secrets object."""
-    return is_type(obj, "streamlit.runtime.secrets.Secrets")
-
-
 def is_sequence(seq: Any) -> bool:
     """True if input looks like a sequence."""
     if isinstance(seq, str):
diff --git a/lib/tests/streamlit/data_mocks.py b/lib/tests/streamlit/data_mocks.py
index 8c26caa0aca0..f1bc83860fcb 100644
--- a/lib/tests/streamlit/data_mocks.py
+++ b/lib/tests/streamlit/data_mocks.py
@@ -14,225 +14,885 @@
 
 from __future__ import annotations
 
+import array
 import enum
 import random
+from collections import ChainMap, Counter, OrderedDict, UserDict, defaultdict, deque
+from dataclasses import dataclass
 from datetime import date
-from typing import NamedTuple
+from types import MappingProxyType
+from typing import Any, Literal, NamedTuple, TypedDict
 
 import numpy as np
 import pandas as pd
 import pyarrow as pa
 
 from streamlit.dataframe_util import DataFormat
-from tests.streamlit.pyspark_mocks import DataFrame as PysparkDataFrame
+from tests.streamlit.modin_mocks import DataFrame as ModinDataFrame
+from tests.streamlit.modin_mocks import Series as ModinSeries
+from tests.streamlit.pyspark_mocks import DataFrame as PySparkDataFrame
 from tests.streamlit.snowpandas_mocks import DataFrame as SnowpandasDataFrame
 from tests.streamlit.snowpandas_mocks import Series as SnowpandasSeries
 from tests.streamlit.snowpark_mocks import DataFrame as SnowparkDataFrame
+from tests.streamlit.snowpark_mocks import Row as SnowparkRow
 from tests.streamlit.snowpark_mocks import Table as SnowparkTable
 
 np.random.seed(0)
 random.seed(0)
 
 
-class TestCaseMetadata(NamedTuple):
+class CaseMetadata(NamedTuple):
+    # The expected number of rows
     expected_rows: int
+    # The expected number of columns (doesn't include index columns)
     expected_cols: int
+    # The expected data format
     expected_data_format: DataFormat
+    # The expected sequence when the data is converted to a sequence
+    # If None, the sequence is not checked.
+    expected_sequence: list[Any] | None
+    # The expected command used when the data is written via `st.write`
+    expected_write_command: Literal[
+        "markdown", "dataframe", "json", "help", "write_stream"
+    ]
+    # Whether the data structure is unevaluated and will be truncated
+    # if it is too large.
+    is_unevaluated: bool
+    # The expected return type of the data when it is
+    # returned from the `st.data_editor` function.
+    expected_type: type | None = None
 
-    # Tell pytest this is not a TestClass despite having "Test" in the name.
-    __test__ = False
-
-
-SHARED_TEST_CASES = [
-    # None:
-    (None, TestCaseMetadata(0, 0, DataFormat.EMPTY)),
-    # Empty list:
-    ([], TestCaseMetadata(0, 0, DataFormat.LIST_OF_VALUES)),
-    # Empty tuple:
-    ((), TestCaseMetadata(0, 0, DataFormat.TUPLE_OF_VALUES)),
-    # Empty dict (not a an empty set!)
-    ({}, TestCaseMetadata(0, 0, DataFormat.KEY_VALUE_DICT)),
-    # Empty set:
-    (set(), TestCaseMetadata(0, 0, DataFormat.SET_OF_VALUES)),
-    # Empty numpy array:
-    # for unknown reasons, pd.DataFrame initializes empty numpy arrays with a single column
-    (np.ndarray(0), TestCaseMetadata(0, 1, DataFormat.NUMPY_LIST)),
-    # Empty column value mapping with columns:
-    ({"name": [], "type": []}, TestCaseMetadata(0, 2, DataFormat.COLUMN_VALUE_MAPPING)),
-    # Empty dataframe:
-    (pd.DataFrame(), TestCaseMetadata(0, 0, DataFormat.PANDAS_DATAFRAME)),
-    # Empty dataframe with columns:
+
+@dataclass
+class ElementDataClass:
+    name: str
+    is_widget: bool
+    usage: float
+
+
+class ElementNamedTuple(NamedTuple):
+    name: str
+    is_widget: bool
+    usage: float
+
+
+class ElementTypedDict(TypedDict):
+    name: str
+    is_widget: bool
+    usage: float
+
+
+class UserDictExample(UserDict):  # type: ignore
+    pass
+
+
+class TestObject:
+    def __str__(self):
+        return "TestObject"
+
+
+class StrTestEnum(str, enum.Enum):
+    NUMBER_INPUT = "st.number_input"
+    TEXT_AREA = "st.text_area"
+    TEXT_INPUT = "st.text_input"
+
+
+class TestEnum(enum.Enum):
+    NUMBER_INPUT = "st.number_input"
+    TEXT_AREA = "st.text_area"
+    TEXT_INPUT = "st.text_input"
+
+
+def data_generator():
+    yield "st.number_input"
+    yield "st.text_area"
+    yield "st.text_input"
+
+
+SHARED_TEST_CASES: list[tuple[str, Any, CaseMetadata]] = [
+    ###################################
+    ####### Native Python Types #######
+    ###################################
     (
-        pd.DataFrame(
-            columns=["name", "type"], index=pd.RangeIndex(start=0, step=1)
-        ),  # Explicitly set the range index to have the same behavior across versions
-        TestCaseMetadata(0, 2, DataFormat.PANDAS_DATAFRAME),
+        "None",
+        None,
+        CaseMetadata(0, 0, DataFormat.EMPTY, [], "markdown", False, pd.DataFrame),
     ),
-    # Pandas DataFrame:
     (
-        pd.DataFrame(["st.text_area", "st.markdown"]),
-        TestCaseMetadata(2, 1, DataFormat.PANDAS_DATAFRAME),
+        "Empty list",
+        [],
+        CaseMetadata(0, 0, DataFormat.LIST_OF_VALUES, [], "json", False),
     ),
-    # List of strings (List[str]):
     (
+        "Empty tuple",
+        (),
+        CaseMetadata(0, 0, DataFormat.TUPLE_OF_VALUES, [], "markdown", False),
+    ),
+    (
+        "Empty dict",
+        {},
+        CaseMetadata(0, 0, DataFormat.KEY_VALUE_DICT, [], "json", False),
+    ),
+    (
+        "Empty set",
+        set(),
+        CaseMetadata(0, 0, DataFormat.SET_OF_VALUES, [], "markdown", False),
+    ),
+    (
+        "List[str]",
         ["st.text_area", "st.number_input", "st.text_input"],
-        TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES),
-    ),
-    # List of integers (List[int]):
-    ([1, 2, 3], TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES)),
-    # List of floats (List[float]):
-    ([1.0, 2.0, 3.0], TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES)),
-    # List of booleans (List[bool]):
-    ([True, False, True], TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES)),
-    # List of Nones (List[None]):
-    ([None, None, None], TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES)),
-    # List of dates (List[date]):
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            ["st.text_area", "st.number_input", "st.text_input"],
+            "json",
+            False,
+        ),
+    ),
     (
+        "List[int]",
+        [1, 2, 3],
+        CaseMetadata(3, 1, DataFormat.LIST_OF_VALUES, [1, 2, 3], "json", False),
+    ),
+    (
+        "List[float]",
+        [1.1, 2.2, 3.3],
+        CaseMetadata(3, 1, DataFormat.LIST_OF_VALUES, [1.1, 2.2, 3.3], "json", False),
+    ),
+    (
+        "List[bool]",
+        [True, False, True],
+        CaseMetadata(
+            3, 1, DataFormat.LIST_OF_VALUES, [True, False, True], "json", False
+        ),
+    ),
+    (
+        "List[None]",
+        [None, None, None],
+        CaseMetadata(
+            3, 1, DataFormat.LIST_OF_VALUES, [None, None, None], "json", False
+        ),
+    ),
+    (
+        "List[date]",
         [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)],
-        TestCaseMetadata(3, 1, DataFormat.LIST_OF_VALUES),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)],
+            "json",
+            False,
+        ),
     ),
-    # Set of strings (Set[str]):
-    # Set does not have a stable order across different Python version.
-    # Therefore, we are only testing this with one item.
     (
+        "Set[str]",
+        # Set does not have a stable order across different Python version.
+        # Therefore, we are only testing this with one item.
         {"st.number_input", "st.number_input"},  # noqa: B033
-        TestCaseMetadata(1, 1, DataFormat.SET_OF_VALUES),
+        CaseMetadata(
+            1, 1, DataFormat.SET_OF_VALUES, ["st.number_input"], "markdown", False
+        ),
     ),
-    # Tuple of strings (Tuple[str]):
     (
+        "Tuple[str]",
         ("st.text_area", "st.number_input", "st.text_input"),
-        TestCaseMetadata(3, 1, DataFormat.TUPLE_OF_VALUES),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.TUPLE_OF_VALUES,
+            ["st.text_area", "st.number_input", "st.text_input"],
+            "markdown",
+            False,
+        ),
     ),
-    # Numpy list / 1D numpy array (np.array[str]):
     (
-        np.array(["st.text_area", "st.number_input", "st.text_input"]),
-        TestCaseMetadata(3, 1, DataFormat.NUMPY_LIST),
+        "Frozenset[str]",
+        # Set does not have a stable order across different Python version.
+        # Therefore, we are only testing this with one item.
+        frozenset({"st.number_input", "st.number_input"}),  # noqa: B033
+        CaseMetadata(
+            1,
+            1,
+            DataFormat.SET_OF_VALUES,
+            ["st.number_input"],
+            "markdown",
+            False,
+            set,
+        ),
     ),
-    # np.array[int]:
-    (np.array([1, 2, 3]), TestCaseMetadata(3, 1, DataFormat.NUMPY_LIST)),
-    # Multi-dimensional numpy array (np.array[List[Scalar]])
     (
-        np.array(
+        "Empty frozenset",
+        frozenset(),
+        CaseMetadata(0, 0, DataFormat.SET_OF_VALUES, [], "markdown", False, set),
+    ),
+    (
+        "Range",
+        range(3),
+        CaseMetadata(
+            3, 1, DataFormat.LIST_OF_VALUES, [0, 1, 2], "markdown", False, list
+        ),
+    ),
+    (
+        "Dict Keys",
+        {
+            "st.number_input": "number",
+            "st.text_area": "text",
+            "st.text_input": "text",
+        }.keys(),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "markdown",
+            False,
+            list,
+        ),
+    ),
+    (
+        "Dict Values",
+        {
+            "st.number_input": "number",
+            "st.text_area": "text",
+            "st.text_input": "text",
+        }.values(),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            ["number", "text", "text"],
+            "markdown",
+            False,
+            list,
+        ),
+    ),
+    (
+        "Dict Items",
+        {
+            "st.number_input": "number",
+            "st.text_area": "text",
+            "st.text_input": "text",
+        }.items(),
+        CaseMetadata(
+            3,
+            2,
+            DataFormat.LIST_OF_ROWS,
+            None,
+            "markdown",
+            False,
+            list,
+        ),
+    ),
+    (
+        "collections.OrderedDict",
+        OrderedDict(
             [
-                ["st.text_area", "widget"],
-                ["st.markdown", "element"],
+                ("st.number_input", "number"),
+                ("st.text_area", "text"),
             ]
         ),
-        TestCaseMetadata(2, 2, DataFormat.NUMPY_MATRIX),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.number_input", "st.text_area"],
+            "json",
+            False,
+            dict,
+        ),
     ),
-    # np.array[List[str]]:
     (
-        np.array([["st.text_area"], ["st.number_input"], ["st.text_input"]]),
-        TestCaseMetadata(3, 1, DataFormat.NUMPY_MATRIX),
+        "collections.defaultdict",
+        defaultdict(
+            lambda: "Not Present",
+            {"st.text_area": "widget", "st.markdown": "element"},
+        ),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.text_area", "st.markdown"],
+            "json",
+            False,
+            dict,
+        ),
     ),
-    # Pandas Series (pd.Series):
     (
-        pd.Series(["st.text_area", "st.number_input", "st.text_input"], name="widgets"),
-        TestCaseMetadata(3, 1, DataFormat.PANDAS_SERIES),
+        "collections.Counter",
+        Counter({"st.number_input": 4, "st.text_area": 2}),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.number_input", "st.text_area"],
+            "json",
+            False,
+            dict,
+        ),
     ),
-    # Pandas Styler (pd.Styler):
     (
-        pd.DataFrame(["st.text_area", "st.markdown"]).style,
-        TestCaseMetadata(2, 1, DataFormat.PANDAS_STYLER),
+        "collections.deque",
+        deque(["st.number_input", "st.text_area", "st.text_input"]),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "markdown",
+            False,
+            list,
+        ),
     ),
-    # Pandas Index (pd.Index):
     (
-        pd.Index(["st.text_area", "st.markdown"]),
-        TestCaseMetadata(2, 1, DataFormat.PANDAS_INDEX),
+        "collections.ChainMap",
+        ChainMap(
+            {"st.number_input": "number", "st.text_area": "text"},
+            {"st.text_input": "text"},
+        ),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["number", "text", "text"],
+            "json",
+            False,
+            dict,
+        ),
     ),
-    # Pyarrow Table (pyarrow.Table):
     (
-        pa.Table.from_pandas(pd.DataFrame(["st.text_area", "st.markdown"])),
-        TestCaseMetadata(2, 1, DataFormat.PYARROW_TABLE),
+        "Dataclass",
+        ElementDataClass("st.number_input", is_widget=True, usage=0.32),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.number_input", True, 0.32],
+            "help",
+            False,
+            dict,
+        ),
+    ),
+    (
+        "TypedDict",
+        ElementTypedDict(name="st.number_input", is_widget=True, usage=0.32),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["name", "is_widget", "usage"],
+            "json",
+            False,
+            dict,
+        ),
+    ),
+    (
+        "NamedTuple",
+        ElementNamedTuple("st.number_input", is_widget=True, usage=0.32),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.number_input", True, 0.32],
+            "json",
+            False,
+            dict,
+        ),
+    ),
+    (
+        "String Enum",
+        StrTestEnum,
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "help",
+            False,
+            list,
+        ),
+    ),
+    (
+        "Enum",
+        TestEnum,
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.LIST_OF_VALUES,
+            [TestEnum.NUMBER_INPUT, TestEnum.TEXT_AREA, TestEnum.TEXT_INPUT],
+            "help",
+            False,
+            list,
+        ),
+    ),
+    (
+        "Generator Function",
+        data_generator,
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.UNKNOWN,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "write_stream",
+            True,
+        ),
+    ),
+    (
+        "Empty column value mapping",
+        {"name": [], "type": []},
+        CaseMetadata(
+            0, 2, DataFormat.COLUMN_VALUE_MAPPING, ["name", "type"], "json", False
+        ),
+    ),
+    (
+        "array.array",
+        array.array("i", [1, 2, 3]),
+        CaseMetadata(
+            3, 1, DataFormat.LIST_OF_VALUES, [1, 2, 3], "markdown", False, list
+        ),
+    ),
+    (
+        "MappingProxyType",
+        MappingProxyType({"st.text_area": "widget", "st.markdown": "element"}),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["widget", "element"],
+            "json",
+            False,
+            dict,
+        ),
+    ),
+    (
+        "UserDict",
+        UserDictExample({"st.text_area": "widget", "st.markdown": "element"}),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["widget", "element"],
+            "json",
+            False,
+            dict,
+        ),
     ),
-    # List of rows (List[List[Scalar]]):
     (
+        "List of rows",  # List[list[scalar]]
         [["st.text_area", "widget"], ["st.markdown", "element"]],
-        TestCaseMetadata(2, 2, DataFormat.LIST_OF_ROWS),
+        CaseMetadata(2, 2, DataFormat.LIST_OF_ROWS, None, "json", False),
     ),
-    # List of records (List[Dict[str, Scalar]]):
     (
+        "List of records",  # List[Dict[str, Scalar]]
         [
             {"name": "st.text_area", "type": "widget"},
             {"name": "st.markdown", "type": "element"},
         ],
-        TestCaseMetadata(2, 2, DataFormat.LIST_OF_RECORDS),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.LIST_OF_RECORDS,
+            None,
+            "json",
+            False,
+        ),
     ),
-    # Column-index mapping ({column: {index: value}}):
     (
+        "Column-index mapping",  # ({column: {index: value}})
         {
             "type": {"st.text_area": "widget", "st.markdown": "element"},
             "usage": {"st.text_area": 4.92, "st.markdown": 47.22},
         },
-        TestCaseMetadata(2, 2, DataFormat.COLUMN_INDEX_MAPPING),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.COLUMN_INDEX_MAPPING,
+            ["type", "usage"],
+            "json",
+            False,
+        ),
     ),
-    # Column-value mapping ({column: List[values]}}):
     (
+        "Column-value mapping",  # ({column: List[values]}})
         {
             "name": ["st.text_area", "st.markdown"],
             "type": ["widget", "element"],
         },
-        TestCaseMetadata(2, 2, DataFormat.COLUMN_VALUE_MAPPING),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.COLUMN_VALUE_MAPPING,
+            ["name", "type"],
+            "json",
+            False,
+        ),
     ),
-    # Column-series mapping ({column: Series(values)}):
     (
+        "Column-series mapping",  # ({column: Series(values)})
         {
             "name": pd.Series(["st.text_area", "st.markdown"], name="name"),
             "type": pd.Series(["widget", "element"], name="type"),
         },
-        TestCaseMetadata(2, 2, DataFormat.COLUMN_SERIES_MAPPING),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.COLUMN_SERIES_MAPPING,
+            ["name", "type"],
+            "dataframe",
+            False,
+        ),
     ),
-    # Key-value dict ({index: value}):
     (
+        "Key-value dict",  # ({index: value})
         {"st.text_area": "widget", "st.markdown": "element"},
-        TestCaseMetadata(2, 1, DataFormat.KEY_VALUE_DICT),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.KEY_VALUE_DICT,
+            ["st.text_area", "st.markdown"],
+            "json",
+            False,
+        ),
+    ),
+    ###################################
+    ########## Pandas Types ###########
+    ###################################
+    (
+        "Empty pd.Dataframe",
+        pd.DataFrame(),
+        CaseMetadata(0, 0, DataFormat.PANDAS_DATAFRAME, [], "dataframe", False),
+    ),
+    (
+        "Empty pd.Dataframe with columns",
+        pd.DataFrame(
+            columns=["name", "type"], index=pd.RangeIndex(start=0, step=1)
+        ),  # Explicitly set the range index to have the same behavior across versions
+        CaseMetadata(0, 2, DataFormat.PANDAS_DATAFRAME, [], "dataframe", False),
+    ),
+    (
+        "pd.Dataframe",
+        pd.DataFrame(["st.text_area", "st.markdown"]),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.PANDAS_DATAFRAME,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+        ),
+    ),
+    (
+        "pd.Series[str]",
+        pd.Series(
+            ["st.text_area", "st.number_input", "st.text_input"],
+            name="widgets",
+        ),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.PANDAS_SERIES,
+            ["st.text_area", "st.number_input", "st.text_input"],
+            "dataframe",
+            False,
+        ),
+    ),
+    (
+        "pd.Index",
+        pd.Index(["st.text_area", "st.markdown"]),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.PANDAS_INDEX,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "Pandas Styler",
+        pd.DataFrame(["st.text_area", "st.markdown"]).style,
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.PANDAS_STYLER,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "pd.array",
+        pd.array(["st.number_input", "st.text_area", "st.text_input"]),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.PANDAS_ARRAY,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "dataframe",
+            False,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "pd.DatetimeIndex",
+        pd.DatetimeIndex(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.PANDAS_INDEX,
+            [
+                pd.Timestamp("2020-01-01 10:00:00+0000", tz="UTC"),
+                pd.Timestamp("2020-02-01 11:00:00+0000", tz="UTC"),
+            ],
+            "dataframe",
+            False,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "pd.RangeIndex",
+        pd.RangeIndex(start=0, stop=3, step=1),
+        CaseMetadata(
+            3, 1, DataFormat.PANDAS_INDEX, [0, 1, 2], "dataframe", False, pd.DataFrame
+        ),
+    ),
+    ###################################
+    ########### Numpy Types ###########
+    ###################################
+    (
+        "Empty np.array",
+        # For unknown reasons, pd.DataFrame initializes empty numpy arrays with a single column
+        np.ndarray(0),
+        CaseMetadata(0, 1, DataFormat.NUMPY_LIST, [], "dataframe", False),
+    ),
+    (
+        "np.array[str]",
+        np.array(["st.text_area", "st.number_input", "st.text_input"]),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.NUMPY_LIST,
+            ["st.text_area", "st.number_input", "st.text_input"],
+            "dataframe",
+            False,
+        ),
+    ),
+    (
+        "np.array[int]",
+        np.array([1, 2, 3]),
+        CaseMetadata(3, 1, DataFormat.NUMPY_LIST, [1, 2, 3], "dataframe", False),
+    ),
+    (
+        "np.array[list[scalar]]",
+        np.array(
+            [
+                ["st.text_area", "widget"],
+                ["st.markdown", "element"],
+            ]
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.NUMPY_MATRIX,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+        ),
+    ),
+    (
+        "np.array[list[str]]",  # numpy matrix
+        np.array(
+            [
+                ["st.text_area", "widget"],
+                ["st.markdown", "element"],
+            ]
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.NUMPY_MATRIX,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+        ),
     ),
-    # Snowpark DataFrame:
+    ###################################
+    ########## Pyarrow Types ##########
+    ###################################
     (
-        SnowparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),
-        TestCaseMetadata(2, 2, DataFormat.SNOWPARK_OBJECT),
+        "Pyarrow Table",
+        pa.Table.from_pandas(pd.DataFrame(["st.text_area", "st.markdown"])),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.PYARROW_TABLE,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            False,
+        ),
+    ),
+    (
+        "Pyarrow Array",
+        pa.array(["st.number_input", "st.text_area", "st.text_input"]),
+        CaseMetadata(
+            3,
+            1,
+            DataFormat.PYARROW_ARRAY,
+            ["st.number_input", "st.text_area", "st.text_input"],
+            "dataframe",
+            False,
+        ),
+    ),
+    ###################################
+    ##### Snowflake Types (Mocks) #####
+    ###################################
+    (
+        "Snowpark DataFrame",
+        SnowparkDataFrame(
+            pd.DataFrame(
+                [
+                    {"name": "st.text_area", "type": "widget"},
+                    {"name": "st.markdown", "type": "element"},
+                ]
+            )
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.SNOWPARK_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "Snowpark Table",
+        SnowparkTable(
+            pd.DataFrame(
+                [
+                    {"name": "st.text_area", "type": "widget"},
+                    {"name": "st.markdown", "type": "element"},
+                ]
+            )
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.SNOWPARK_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "Snowpark Row List",
+        [
+            SnowparkRow({"name": "st.text_area", "type": "widget"}),
+            SnowparkRow({"name": "st.markdown", "type": "element"}),
+            SnowparkRow({"name": "st.text_input", "type": "text"}),
+        ],
+        CaseMetadata(
+            3,
+            2,
+            DataFormat.SNOWPARK_OBJECT,
+            ["st.text_area", "st.markdown", "st.text_input"],
+            "dataframe",
+            False,
+            pd.DataFrame,
+        ),
+    ),
+    (
+        "Snowpandas DataFrame",
+        SnowpandasDataFrame(
+            pd.DataFrame(
+                [
+                    {"name": "st.text_area", "type": "widget"},
+                    {"name": "st.markdown", "type": "element"},
+                ]
+            )
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.SNOWPANDAS_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
     ),
-    # Snowpark Table:
     (
-        SnowparkTable(pd.DataFrame(np.random.randn(2, 2))),
-        TestCaseMetadata(2, 2, DataFormat.SNOWPARK_OBJECT),
+        "Snowpandas Series",
+        SnowpandasSeries(pd.Series(["st.text_area", "st.markdown"])),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.SNOWPANDAS_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
     ),
-    # Snowpark Pandas DataFrame:
     (
-        SnowpandasDataFrame(pd.DataFrame(np.random.randn(2, 2))),
-        TestCaseMetadata(2, 2, DataFormat.SNOWPANDAS_OBJECT),
+        "Modin DataFrame",
+        ModinDataFrame(
+            pd.DataFrame(
+                [
+                    {"name": "st.text_area", "type": "widget"},
+                    {"name": "st.markdown", "type": "element"},
+                ]
+            )
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.MODIN_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
     ),
-    # Snowpark Pandas Series:
     (
-        SnowpandasSeries(pd.Series(np.random.randn(2))),
-        TestCaseMetadata(2, 1, DataFormat.SNOWPANDAS_OBJECT),
+        "Modin Series",
+        ModinSeries(pd.Series(["st.text_area", "st.markdown"])),
+        CaseMetadata(
+            2,
+            1,
+            DataFormat.MODIN_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
     ),
-    # Pyspark Dataframe:
+    ###################################
+    ##### External Types (Mocks) ######
+    ###################################
     (
-        PysparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),
-        TestCaseMetadata(2, 2, DataFormat.PYSPARK_OBJECT),
+        "Pyspark DataFrame",
+        PySparkDataFrame(
+            pd.DataFrame(
+                [
+                    {"name": "st.text_area", "type": "widget"},
+                    {"name": "st.markdown", "type": "element"},
+                ]
+            )
+        ),
+        CaseMetadata(
+            2,
+            2,
+            DataFormat.PYSPARK_OBJECT,
+            ["st.text_area", "st.markdown"],
+            "dataframe",
+            True,
+            pd.DataFrame,
+        ),
     ),
 ]
-
-
-class TestObject:
-    def __str__(self):
-        return "TestObject"
-
-
-class StrTestEnum(str, enum.Enum):
-    NUMBER_INPUT = "st.number_input"
-    TEXT_AREA = "st.text_area"
-    TEXT_INPUT = "st.text_input"
-
-
-class TestEnum(enum.Enum):
-    NUMBER_INPUT = "st.number_input"
-    TEXT_AREA = "st.text_area"
-    TEXT_INPUT = "st.text_input"
-
-
-def data_generator():
-    yield "st.number_input"
-    yield "st.text_area"
-    yield "st.text_input"
diff --git a/lib/tests/streamlit/dataframe_util_test.py b/lib/tests/streamlit/dataframe_util_test.py
index bf66bf3e744a..6ecceaee698c 100644
--- a/lib/tests/streamlit/dataframe_util_test.py
+++ b/lib/tests/streamlit/dataframe_util_test.py
@@ -16,7 +16,6 @@
 
 import enum
 import unittest
-from collections import OrderedDict
 from datetime import date
 from decimal import Decimal
 from typing import Any
@@ -34,20 +33,13 @@
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
 from tests.streamlit.data_mocks import (
     SHARED_TEST_CASES,
-    StrTestEnum,
-    TestCaseMetadata,
-    TestEnum,
+    CaseMetadata,
     TestObject,
-    data_generator,
 )
-from tests.streamlit.modin_mocks import DataFrame as ModinDataFrame
-from tests.streamlit.modin_mocks import Series as ModinSeries
-from tests.streamlit.pyspark_mocks import DataFrame as PysparkDataFrame
 from tests.streamlit.snowpandas_mocks import DataFrame as SnowpandasDataFrame
 from tests.streamlit.snowpandas_mocks import Series as SnowpandasSeries
 from tests.streamlit.snowpark_mocks import DataFrame as SnowparkDataFrame
 from tests.streamlit.snowpark_mocks import Row as SnowparkRow
-from tests.streamlit.snowpark_mocks import Table as SnowparkTable
 from tests.testutil import create_snowpark_session, patch_config_options
 
 
@@ -66,8 +58,9 @@ def test_convert_pandas_df_to_arrow_bytes(self):
     )
     def test_convert_anything_to_pandas_df(
         self,
+        name: str,
         input_data: Any,
-        metadata: TestCaseMetadata,
+        metadata: CaseMetadata,
     ):
         """Test that `convert_anything_to_pandas_df` correctly converts
         a variety of types to a DataFrame.
@@ -78,30 +71,33 @@ def test_convert_anything_to_pandas_df(
         self.assertEqual(converted_df.shape[1], metadata.expected_cols)
 
     @parameterized.expand(
-        [
-            (ModinDataFrame(pd.DataFrame(np.random.randn(2000, 2))),),
-            (ModinSeries(pd.Series(np.random.randn(2000))),),
-            (PysparkDataFrame(pd.DataFrame(np.random.randn(2000, 2))),),
-            (SnowpandasDataFrame(pd.DataFrame(np.random.randn(2000, 2))),),
-            (SnowpandasSeries(pd.Series(np.random.randn(2000))),),
-            (SnowparkDataFrame(pd.DataFrame(np.random.randn(2000, 2))),),
-            (SnowparkTable(pd.DataFrame(np.random.randn(2000, 2))),),
-        ]
+        SHARED_TEST_CASES,
     )
-    def test_convert_anything_to_pandas_df_show_warning_for_unevaluated_df(
+    def test_unevaluated_dataframe_handling(
         self,
+        name: str,
         input_data: Any,
+        metadata: CaseMetadata,
     ):
-        """Test that `convert_anything_to_pandas_df` correctly converts
-        a variety unevaluated dataframes and shows a warning if
-        the row count is > 1000.
+        """Test that unevaluated data objects are correctly detected and
+        handled by limiting the number of rows to be displayed.
         """
-        with patch("streamlit.caption") as mock:
-            converted_df = dataframe_util.convert_anything_to_pandas_df(
-                input_data, max_unevaluated_rows=1000
-            )
-            self.assertIsInstance(converted_df, pd.DataFrame)
-            mock.assert_called_once()
+        with patch("streamlit.dataframe_util._show_data_information") as mock:
+            if metadata.is_unevaluated:
+                assert dataframe_util.is_unevaluated_data_object(input_data) is True
+                converted_df = dataframe_util.convert_anything_to_pandas_df(
+                    input_data, max_unevaluated_rows=1
+                )
+                assert isinstance(converted_df, pd.DataFrame)
+                assert converted_df.shape[0] <= 1
+                mock.assert_called_once()
+            else:
+                assert dataframe_util.is_unevaluated_data_object(input_data) is False
+                converted_df = dataframe_util.convert_anything_to_pandas_df(
+                    input_data, max_unevaluated_rows=1
+                )
+                assert converted_df.shape[0] == metadata.expected_rows
+                mock.assert_not_called()
 
     def test_convert_anything_to_pandas_df_ensure_copy(self):
         """Test that `convert_anything_to_pandas_df` creates a copy of the original
@@ -138,11 +134,13 @@ def test_convert_anything_to_pandas_df_supports_key_value_dicts(self):
         """
         data = {"a": 1, "b": 2}
         df = dataframe_util.convert_anything_to_pandas_df(data)
-        pd.testing.assert_frame_equal(df, pd.DataFrame.from_dict(data, orient="index"))
+        pd.testing.assert_frame_equal(
+            df, pd.DataFrame.from_dict(data, orient="index", columns=["value"])
+        )
 
     def test_convert_anything_to_pandas_df_converts_stylers(self):
-        """Test that `convert_anything_to_pandas_df` correctly converts Stylers to DF, without cloning the
-        data.
+        """Test that `convert_anything_to_pandas_df` correctly converts Stylers to DF,
+        without cloning the data.
         """
         original_df = pd.DataFrame(
             {
@@ -194,8 +192,9 @@ def to_pandas(self):
     )
     def test_convert_anything_to_arrow_bytes(
         self,
+        name: str,
         input_data: Any,
-        metadata: TestCaseMetadata,
+        metadata: CaseMetadata,
     ):
         """Test that `convert_anything_to_arrow_bytes` correctly converts
         a variety of types to Arrow bytes.
@@ -415,7 +414,8 @@ class DummyClass:
         self.assertTrue(
             dataframe_util.is_snowpark_row_list(
                 [
-                    SnowparkRow(),
+                    SnowparkRow({"col1": 1, "col2": "foo"}),
+                    SnowparkRow({"col1": 2, "col2": "bar"}),
                 ]
             )
         )
@@ -440,22 +440,36 @@ def test_is_snowpark_dataframe(self):
         self.assertTrue(dataframe_util.is_snowpark_data_object(SnowparkDataFrame(df)))
 
     @pytest.mark.require_snowflake
-    def test_is_snowpark_dataframe_integration(self):
+    def test_verify_snowpark_integration(self):
+        """Integration test snowpark object handling.
+        This is in addition to the tests using the mocks to verify that
+        the latest version of the library is still supported.
+        """
         with create_snowpark_session() as snowpark_session:
-            self.assertTrue(
-                dataframe_util.is_snowpark_data_object(
-                    snowpark_session.sql("SELECT 40+2 as COL1")
-                )
+            snowpark_df = snowpark_session.sql("SELECT 40+2 as COL1")
+
+            assert dataframe_util.is_snowpark_data_object(snowpark_df) is True
+            assert isinstance(
+                dataframe_util.convert_anything_to_pandas_df(snowpark_df),
+                pd.DataFrame,
             )
-            self.assertTrue(
-                dataframe_util.is_snowpark_data_object(
-                    snowpark_session.sql("SELECT 40+2 as COL1").cache_result()
-                )
+
+            snowpark_cached_result = snowpark_session.sql(
+                "SELECT 40+2 as COL1"
+            ).cache_result()
+            assert (
+                dataframe_util.is_snowpark_data_object(snowpark_cached_result) is True
             )
-            self.assertTrue(
-                dataframe_util.is_snowpark_row_list(
-                    snowpark_session.sql("SELECT 40+2 as COL1").collect()
-                )
+            assert isinstance(
+                dataframe_util.convert_anything_to_pandas_df(snowpark_cached_result),
+                pd.DataFrame,
+            )
+
+            snowpark_row_list = snowpark_session.sql("SELECT 40+2 as COL1").collect()
+            assert dataframe_util.is_snowpark_row_list(snowpark_row_list) is True
+            assert isinstance(
+                dataframe_util.convert_anything_to_pandas_df(snowpark_row_list),
+                pd.DataFrame,
             )
 
     @parameterized.expand(
@@ -463,8 +477,9 @@ def test_is_snowpark_dataframe_integration(self):
     )
     def test_determine_data_format(
         self,
+        name: str,
         input_data: Any,
-        metadata: TestCaseMetadata,
+        metadata: CaseMetadata,
     ):
         """Test that `determine_data_format` correctly determines the
         data format of a variety of data structures/types.
@@ -481,8 +496,9 @@ def test_determine_data_format(
     )
     def test_convert_pandas_df_to_data_format(
         self,
+        name: str,
         input_data: Any,
-        metadata: TestCaseMetadata,
+        metadata: CaseMetadata,
     ):
         """Test that `convert_pandas_df_to_data_format` correctly converts a
         DataFrame to the specified data format.
@@ -502,32 +518,26 @@ def test_convert_pandas_df_to_data_format(
                 converted_df, metadata.expected_data_format
             )
 
-            # Some data formats are converted to DataFrames instead of
-            # the original data type/structure.
-            if metadata.expected_data_format in [
-                dataframe_util.DataFormat.SNOWPARK_OBJECT,
-                dataframe_util.DataFormat.PYSPARK_OBJECT,
-                dataframe_util.DataFormat.PANDAS_INDEX,
-                dataframe_util.DataFormat.PANDAS_STYLER,
-                dataframe_util.DataFormat.SNOWPANDAS_OBJECT,
-                dataframe_util.DataFormat.MODIN_OBJECT,
-                dataframe_util.DataFormat.EMPTY,
-            ]:
-                assert isinstance(converted_data, pd.DataFrame)
+            self.assertEqual(
+                type(converted_data),
+                type(input_data)
+                if metadata.expected_type is None
+                else metadata.expected_type,
+            )
+
+            if isinstance(converted_data, pd.DataFrame):
                 self.assertEqual(converted_data.shape[0], metadata.expected_rows)
                 self.assertEqual(converted_data.shape[1], metadata.expected_cols)
-            else:
-                self.assertEqual(type(converted_data), type(input_data))
+            elif (
                 # Sets in python are unordered, so we can't compare them this way.
-                if (
-                    metadata.expected_data_format
-                    != dataframe_util.DataFormat.SET_OF_VALUES
-                ):
-                    self.assertEqual(str(converted_data), str(input_data))
-                    pd.testing.assert_frame_equal(
-                        converted_df,
-                        dataframe_util.convert_anything_to_pandas_df(converted_data),
-                    )
+                metadata.expected_data_format != dataframe_util.DataFormat.SET_OF_VALUES
+                and metadata.expected_type is None
+            ):
+                self.assertEqual(str(converted_data), str(input_data))
+                pd.testing.assert_frame_equal(
+                    converted_df,
+                    dataframe_util.convert_anything_to_pandas_df(converted_data),
+                )
 
     def test_convert_pandas_df_to_data_format_with_unknown_data_format(self):
         """Test that `convert_df_to_data_format` raises a ValueError when
@@ -643,245 +653,25 @@ class StrOpt(str, enum.Enum):
         self.assertEqual(list(StrOpt), converted_list)
 
     @parameterized.expand(
-        [
-            (None, []),
-            # List:
-            ([], []),
-            (
-                ["st.number_input", "st.text_area", "st.text_input"],
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            (
-                [1, 2, 3],
-                [1, 2, 3],
-            ),
-            # Reversed list:
-            (
-                reversed(["st.number_input", "st.text_area", "st.text_input"]),
-                ["st.text_input", "st.text_area", "st.number_input"],
-            ),
-            # Set:
-            (set(), []),
-            (
-                {"st.number_input", "st.text_area", "st.text_input"},
-                ["st.text_input", "st.number_input", "st.text_area"],
-            ),
-            # Tuple:
-            ((), []),
-            (
-                ("st.number_input", "st.text_area", "st.text_input"),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Dict:
-            ({}, []),
-            (
-                {
-                    "st.number_input": "number",
-                    "st.text_area": "text",
-                    "st.text_input": "text",
-                },
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Dict keys:
-            (
-                {
-                    "st.number_input": "number",
-                    "st.text_area": "text",
-                    "st.text_input": "text",
-                }.keys(),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Dict values:
-            (
-                {
-                    "st.number_input": "number",
-                    "st.text_area": "text",
-                    "st.text_input": "text",
-                }.values(),
-                ["number", "text", "text"],
-            ),
-            # OrderedDict:
-            (
-                OrderedDict(
-                    [
-                        ("st.number_input", "number"),
-                        ("st.text_area", "text"),
-                        ("st.text_input", "text"),
-                    ]
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Enum:
-            (
-                TestEnum,
-                [TestEnum.NUMBER_INPUT, TestEnum.TEXT_AREA, TestEnum.TEXT_INPUT],
-            ),
-            (StrTestEnum, ["st.number_input", "st.text_area", "st.text_input"]),
-            # Generator:
-            (data_generator(), ["st.number_input", "st.text_area", "st.text_input"]),
-            # String:
-            ("abc", ["a", "b", "c"]),
-            # Enumerate:
-            (
-                enumerate(["st.number_input", "st.text_area", "st.text_input"]),
-                [0, 1, 2],
-            ),
-            # Range:
-            (range(3), [0, 1, 2]),
-            # Pandas Dataframe:
-            (
-                pd.DataFrame(),
-                [],
-            ),
-            (
-                pd.DataFrame(
-                    columns=["name", "type"], index=pd.RangeIndex(start=0, step=1)
-                ),
-                [],
-            ),
-            (
-                pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"]),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Dataframe with multiple columns (widgets & types)
-            # The first column is expected to be selected as the sequence.
-            (
-                pd.DataFrame(
-                    {
-                        "widgets": ["st.number_input", "st.text_area", "st.text_input"],
-                        "types": ["number", "text", "text"],
-                    }
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pandas Series (pd.Series):
-            (
-                pd.Series(
-                    ["st.number_input", "st.text_area", "st.text_input"], name="widgets"
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pandas Index (pd.Index):
-            (
-                pd.Index(["st.number_input", "st.text_area", "st.text_input"]),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pandas Styler (pd.Styler):
-            (
-                pd.DataFrame(
-                    ["st.number_input", "st.text_area", "st.text_input"]
-                ).style,
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pandas Categorical (pd.Categorical):
-            (
-                pd.Categorical(["st.number_input", "st.text_area", "st.text_input"]),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pandas DatetimeIndex (pd.DatetimeIndex):
-            (
-                pd.DatetimeIndex(
-                    ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
-                ),
-                [
-                    pd.Timestamp("2020-01-01 10:00:00+0000", tz="UTC"),
-                    pd.Timestamp("2020-02-01 11:00:00+0000", tz="UTC"),
-                ],
-            ),
-            # Pandas DatetimeArrayå:
-            (
-                pd.arrays.DatetimeArray(
-                    pd.DatetimeIndex(
-                        ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
-                    ),
-                ),
-                [
-                    pd.Timestamp("2020-01-01 10:00:00+0000", tz="UTC"),
-                    pd.Timestamp("2020-02-01 11:00:00+0000", tz="UTC"),
-                ],
-            ),
-            # Pandas RangeIndex (pd.RangeIndex):
-            (
-                pd.RangeIndex(start=0, stop=3, step=1),
-                [0, 1, 2],
-            ),
-            # Numpy array:
-            (
-                np.array([]),
-                [],
-            ),
-            (
-                np.array(["st.number_input", "st.text_area", "st.text_input"]),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pyarrow Table:
-            (
-                pa.Table.from_pandas(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Snowpark Table:
-            (
-                SnowparkTable(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Snowpark DataFrame:
-            (
-                SnowparkDataFrame(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Snowpark Pandas DataFrame:
-            (
-                SnowpandasDataFrame(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Snowpark Pandas Series:
-            (
-                SnowpandasSeries(
-                    pd.Series(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Pyspark Dataframe:
-            (
-                PysparkDataFrame(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Modin Dataframe:
-            (
-                ModinDataFrame(
-                    pd.DataFrame(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-            # Modin Series:
-            (
-                ModinSeries(
-                    pd.Series(["st.number_input", "st.text_area", "st.text_input"])
-                ),
-                ["st.number_input", "st.text_area", "st.text_input"],
-            ),
-        ]
+        SHARED_TEST_CASES,
     )
     def test_convert_anything_to_sequence(
-        self, input_data: Any, result_sequence: list[Any]
+        self,
+        name: str,
+        input_data: Any,
+        metadata: CaseMetadata,
     ):
         """Test that `convert_anything_to_sequence` correctly converts
         a variety of types to a sequence.
         """
+        if metadata.expected_sequence is None:
+            # Skip all cases where we don't have an expected sequence.
+            return
+
         converted_sequence = dataframe_util.convert_anything_to_sequence(input_data)
         # We convert to a set for the check since some of the formats don't
         # have a guaranteed order.
-        self.assertEqual(set(converted_sequence), set(result_sequence))
+        self.assertEqual(set(converted_sequence), set(metadata.expected_sequence))
         # Check that it is a new object and not the same as the input:
         assert converted_sequence is not input_data
 
diff --git a/lib/tests/streamlit/elements/arrow_dataframe_test.py b/lib/tests/streamlit/elements/arrow_dataframe_test.py
index 58321a5d69c7..2d5e582da44c 100644
--- a/lib/tests/streamlit/elements/arrow_dataframe_test.py
+++ b/lib/tests/streamlit/elements/arrow_dataframe_test.py
@@ -15,24 +15,22 @@
 """Arrow DataFrame tests."""
 
 import json
+from typing import Any
 from unittest.mock import MagicMock, patch
 
 import numpy as np
 import pandas as pd
-import pyarrow as pa
-import pytest
 from pandas.io.formats.style_render import StylerRenderer as Styler
 from parameterized import parameterized
 
 import streamlit as st
 from streamlit.dataframe_util import (
     convert_arrow_bytes_to_pandas_df,
-    convert_arrow_table_to_arrow_bytes,
 )
 from streamlit.elements.lib.column_config_utils import INDEX_IDENTIFIER
 from streamlit.errors import StreamlitAPIException
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
-from tests.testutil import create_snowpark_session
+from tests.streamlit.data_mocks import SHARED_TEST_CASES, CaseMetadata
 
 
 def mock_data_frame():
@@ -67,13 +65,20 @@ def test_empty_column_order_parameter(self):
         proto = self.get_delta_from_queue().new_element.arrow_data_frame
         self.assertEqual(proto.column_order, [])
 
-    def test_pyarrow_table_data(self):
-        df = mock_data_frame()
-        table = pa.Table.from_pandas(df)
-        st.dataframe(table)
+    @parameterized.expand(SHARED_TEST_CASES)
+    def test_with_compatible_data(
+        self,
+        name: str,
+        input_data: Any,
+        metadata: CaseMetadata,
+    ):
+        """Test that it can be called with compatible data."""
+        st.dataframe(input_data)
 
         proto = self.get_delta_from_queue().new_element.arrow_data_frame
-        self.assertEqual(proto.data, convert_arrow_table_to_arrow_bytes(table))
+        reconstructed_df = convert_arrow_bytes_to_pandas_df(proto.data)
+        self.assertEqual(reconstructed_df.shape[0], metadata.expected_rows)
+        self.assertEqual(reconstructed_df.shape[1], metadata.expected_cols)
 
     def test_hide_index_true(self):
         """Test that it can be called with hide_index=True param."""
@@ -190,28 +195,6 @@ def test_dataframe_uses_convert_anything_to_df(self):
             st.dataframe(df)
             convert_anything_to_df.assert_called_once()
 
-    @pytest.mark.require_snowflake
-    def test_snowpark_uncollected(self):
-        """Tests that data can be read from Snowpark's uncollected Dataframe"""
-        with create_snowpark_session() as snowpark_session:
-            df = snowpark_session.sql("SELECT 42 as COL1")
-
-            st.dataframe(df)
-
-        proto = self.get_delta_from_queue().new_element.arrow_data_frame
-        self.assertEqual(convert_arrow_bytes_to_pandas_df(proto.data).iat[0, 0], 42)
-
-    @pytest.mark.require_snowflake
-    def test_snowpark_collected(self):
-        """Tests that data can be read from Snowpark's collected Dataframe"""
-        with create_snowpark_session() as snowpark_session:
-            df = snowpark_session.sql("SELECT 42 as COL1").collect()
-
-            st.dataframe(df)
-
-        proto = self.get_delta_from_queue().new_element.arrow_data_frame
-        self.assertEqual(convert_arrow_bytes_to_pandas_df(proto.data).iat[0, 0], 42)
-
     def test_dataframe_on_select_initial_returns(self):
         """Test st.dataframe returns an empty selection as initial result."""
 
diff --git a/lib/tests/streamlit/elements/data_editor_test.py b/lib/tests/streamlit/elements/data_editor_test.py
index ec12aa284d5e..c374595a02eb 100644
--- a/lib/tests/streamlit/elements/data_editor_test.py
+++ b/lib/tests/streamlit/elements/data_editor_test.py
@@ -48,7 +48,7 @@
 from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
 from streamlit.type_util import is_pandas_version_less_than
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
-from tests.streamlit.data_mocks import SHARED_TEST_CASES, TestCaseMetadata
+from tests.streamlit.data_mocks import SHARED_TEST_CASES, CaseMetadata
 
 
 def _get_arrow_schema(df: pd.DataFrame) -> pa.Schema:
@@ -410,10 +410,16 @@ def test_with_dataframe_data(self):
     @parameterized.expand(SHARED_TEST_CASES)
     def test_with_compatible_data(
         self,
+        name: str,
         input_data: Any,
-        metadata: TestCaseMetadata,
+        metadata: CaseMetadata,
     ):
         """Test that it can be called with compatible data."""
+        if metadata.expected_data_format == DataFormat.UNKNOWN:
+            # We can skip formats where the expected format is unknown
+            # since these cases are not expected to work.
+            return
+
         return_data = st.data_editor(input_data)
 
         proto = self.get_delta_from_queue().new_element.arrow_data_frame
@@ -421,25 +427,22 @@ def test_with_compatible_data(
         self.assertEqual(reconstructed_df.shape[0], metadata.expected_rows)
         self.assertEqual(reconstructed_df.shape[1], metadata.expected_cols)
 
-        # Some data formats are converted to DataFrames instead of
-        # the original data type/structure.
-        if metadata.expected_data_format in [
-            DataFormat.EMPTY,
-            DataFormat.MODIN_OBJECT,
-            DataFormat.PANDAS_INDEX,
-            DataFormat.PANDAS_STYLER,
-            DataFormat.PYSPARK_OBJECT,
-            DataFormat.SNOWPANDAS_OBJECT,
-            DataFormat.SNOWPARK_OBJECT,
-        ]:
-            assert isinstance(return_data, pd.DataFrame)
+        self.assertEqual(
+            type(return_data),
+            type(input_data)
+            if metadata.expected_type is None
+            else metadata.expected_type,
+        )
+
+        if isinstance(return_data, pd.DataFrame):
             self.assertEqual(return_data.shape[0], metadata.expected_rows)
             self.assertEqual(return_data.shape[1], metadata.expected_cols)
-        else:
-            self.assertEqual(type(return_data), type(input_data))
+        elif (
             # Sets in python are unordered, so we can't compare them this way.
-            if metadata.expected_data_format != DataFormat.SET_OF_VALUES:
-                self.assertEqual(str(return_data), str(input_data))
+            metadata.expected_data_format != DataFormat.SET_OF_VALUES
+            and metadata.expected_type is None
+        ):
+            self.assertEqual(str(return_data), str(input_data))
 
     @parameterized.expand(
         [
@@ -455,6 +458,26 @@ def test_with_invalid_data(self, input_data: Any):
         with self.assertRaises(StreamlitAPIException):
             st.data_editor(input_data)
 
+    def test_disables_columns_when_incompatible(self):
+        """Test that Arrow incompatible columns are disabled (configured as non-editable)."""
+        data_df = pd.DataFrame(
+            {
+                "a": pd.Series([1, 2]),
+                "b": pd.Series(["foo", "bar"]),
+                "c": pd.Series([1, "foo"]),  # Incompatible
+                "d": pd.Series([1 + 2j, 3 + 4j]),  # Incompatible
+            }
+        )
+        st.data_editor(data_df)
+
+        proto = self.get_delta_from_queue().new_element.arrow_data_frame
+        columns_config = json.loads(proto.columns)
+
+        self.assertNotIn("a", columns_config)
+        self.assertNotIn("b", columns_config)
+        self.assertTrue(columns_config["c"]["disabled"])
+        self.assertTrue(columns_config["d"]["disabled"])
+
     @parameterized.expand(
         [
             (pd.CategoricalIndex(["a", "b", "c"]),),
diff --git a/lib/tests/streamlit/elements/lib/column_config_utils_test.py b/lib/tests/streamlit/elements/lib/column_config_utils_test.py
index 257fbe10b925..43221e8d09b7 100644
--- a/lib/tests/streamlit/elements/lib/column_config_utils_test.py
+++ b/lib/tests/streamlit/elements/lib/column_config_utils_test.py
@@ -504,8 +504,7 @@ def test_apply_data_specific_configs_hides_index(
     ):
         """Test that the index is hidden for some data formats."""
         columns_config: ColumnConfigMapping = {}
-        data_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
-        apply_data_specific_configs(columns_config, data_df, data_format)
+        apply_data_specific_configs(columns_config, data_format)
 
         if hidden:
             self.assertEqual(
@@ -516,81 +515,6 @@ def test_apply_data_specific_configs_hides_index(
         else:
             self.assertNotIn(INDEX_IDENTIFIER, columns_config)
 
-    def test_apply_data_specific_configs_makes_index_required(self):
-        """Test that a non-range index gets configured as required."""
-        columns_config: ColumnConfigMapping = {}
-        data_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=["a", "b", "c"])
-        apply_data_specific_configs(
-            columns_config, data_df, DataFormat.PANDAS_DATAFRAME
-        )
-        self.assertEqual(
-            columns_config[INDEX_IDENTIFIER]["required"],
-            True,
-            f"Index of type {type(data_df.index)} should be configured as required.",
-        )
-
-    @parameterized.expand(
-        [
-            (DataFormat.SET_OF_VALUES, True),
-            (DataFormat.TUPLE_OF_VALUES, True),
-            (DataFormat.LIST_OF_VALUES, True),
-            (DataFormat.NUMPY_LIST, True),
-            (DataFormat.KEY_VALUE_DICT, True),
-            # Most other data formats which should not rename the first column:
-            (DataFormat.PANDAS_DATAFRAME, False),
-            (DataFormat.PANDAS_SERIES, False),
-            (DataFormat.PANDAS_INDEX, False),
-            (DataFormat.PYARROW_TABLE, False),
-            (DataFormat.PANDAS_STYLER, False),
-            (DataFormat.COLUMN_INDEX_MAPPING, False),
-            (DataFormat.COLUMN_SERIES_MAPPING, False),
-            (DataFormat.LIST_OF_RECORDS, False),
-            (DataFormat.LIST_OF_ROWS, False),
-            (DataFormat.COLUMN_VALUE_MAPPING, False),
-        ]
-    )
-    def test_apply_data_specific_configs_renames_column(
-        self, data_format: DataFormat, renames: bool
-    ):
-        """Test that the column names are changed for some data formats."""
-        data_df = pd.DataFrame([1, 2, 3])
-        apply_data_specific_configs({}, data_df, data_format)
-        if renames:
-            self.assertEqual(
-                data_df.columns[0],
-                "value",
-                f"Data of type {data_format} should be renamed to 'value'",
-            )
-        else:
-            self.assertEqual(
-                data_df.columns[0],
-                0,
-                f"Data of type {data_format} should not be renamed.",
-            )
-
-    def test_apply_data_specific_configs_disables_columns(self):
-        """Test that Arrow incompatible columns are disabled (configured as non-editable)."""
-        columns_config: ColumnConfigMapping = {}
-        data_df = pd.DataFrame(
-            {
-                "a": pd.Series([1, 2]),
-                "b": pd.Series(["foo", "bar"]),
-                "c": pd.Series([1, "foo"]),  # Incompatible
-                "d": pd.Series([1 + 2j, 3 + 4j]),  # Incompatible
-            }
-        )
-
-        apply_data_specific_configs(
-            columns_config,
-            data_df,
-            DataFormat.PANDAS_DATAFRAME,
-            check_arrow_compatibility=True,
-        )
-        self.assertNotIn("a", columns_config)
-        self.assertNotIn("b", columns_config)
-        self.assertTrue(columns_config["c"]["disabled"])
-        self.assertTrue(columns_config["d"]["disabled"])
-
     def test_nan_as_value_raises_exception(self):
         """Test that the usage of `nan` as value in column config raises an exception."""
 
diff --git a/lib/tests/streamlit/elements/map_test.py b/lib/tests/streamlit/elements/map_test.py
index 06fbd078123c..3cd39d203929 100644
--- a/lib/tests/streamlit/elements/map_test.py
+++ b/lib/tests/streamlit/elements/map_test.py
@@ -20,14 +20,13 @@
 
 import numpy as np
 import pandas as pd
-import pytest
 from parameterized import parameterized
 
 import streamlit as st
 from streamlit.elements.map import _DEFAULT_MAP, _DEFAULT_ZOOM_LEVEL
 from streamlit.errors import StreamlitAPIException
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
-from tests.testutil import create_snowpark_session, patch_config_options
+from tests.testutil import patch_config_options
 
 mock_df = pd.DataFrame({"lat": [1, 2, 3, 4], "lon": [10, 20, 30, 40]})
 
@@ -348,44 +347,6 @@ def test_nan_exception(self):
 
         self.assertIn("not allowed to contain null values", str(ctx.exception))
 
-    @pytest.mark.require_snowflake
-    def test_unevaluated_snowpark_table_integration(self):
-        """Test st.map with unevaluated Snowpark DataFrame using real Snowflake instance"""
-        with create_snowpark_session() as snowpark_session:
-            table = snowpark_session.sql(
-                """
-                SELECT V1.$1 AS "lat", V1.$2 AS "lon" FROM
-                    (
-                        VALUES (1, 10), (2, 20), (3, 30), (4, 40)
-                    ) AS V1
-                """
-            ).cache_result()
-            st.map(table)
-
-        c = json.loads(self.get_delta_from_queue().new_element.deck_gl_json_chart.json)
-
-        """Check if map data have 4 rows"""
-        self.assertEqual(len(c["layers"][0]["data"]), 4)
-
-    @pytest.mark.require_snowflake
-    def test_unevaluated_snowpark_dataframe_integration(self):
-        """Test st.map with unevaluated Snowpark DataFrame using real Snowflake instance"""
-        with create_snowpark_session() as snowpark_session:
-            df = snowpark_session.sql(
-                """
-                SELECT V1.$1 AS "lat", V1.$2 AS "lon" FROM
-                    (
-                        VALUES (1, 10), (2, 20), (3, 30), (4, 40)
-                    ) AS V1
-                """
-            )
-            st.map(df)
-
-        c = json.loads(self.get_delta_from_queue().new_element.deck_gl_json_chart.json)
-
-        """Check if map data have 4 rows"""
-        self.assertEqual(len(c["layers"][0]["data"]), 4)
-
     def test_id_changes_when_data_changes(self):
         st.map()
 
diff --git a/lib/tests/streamlit/runtime/caching/cache_errors_test.py b/lib/tests/streamlit/runtime/caching/cache_errors_test.py
index 0ace64eb3417..e91e7194386d 100644
--- a/lib/tests/streamlit/runtime/caching/cache_errors_test.py
+++ b/lib/tests/streamlit/runtime/caching/cache_errors_test.py
@@ -13,30 +13,17 @@
 # limitations under the License.
 
 import threading
-from typing import Any
-
-import numpy as np
-import pandas as pd
-from parameterized import parameterized
 
 import streamlit as st
 from streamlit.elements import exception
 from streamlit.proto.Exception_pb2 import Exception as ExceptionProto
 from streamlit.runtime.caching.cache_errors import (
-    UnevaluatedDataFrameError,
     UnhashableParamError,
     UnserializableReturnValueError,
     get_return_value_type,
 )
 from tests import testutil
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
-from tests.streamlit.modin_mocks import DataFrame as ModinDataFrame
-from tests.streamlit.modin_mocks import Series as ModinSeries
-from tests.streamlit.pyspark_mocks import DataFrame as PysparkDataFrame
-from tests.streamlit.snowpandas_mocks import DataFrame as SnowpandasDataFrame
-from tests.streamlit.snowpandas_mocks import Series as SnowpandasSeries
-from tests.streamlit.snowpark_mocks import DataFrame as SnowparkDataFrame
-from tests.streamlit.snowpark_mocks import Table as SnowparkTable
 
 
 class CacheErrorsTest(DeltaGeneratorTestCase):
@@ -46,8 +33,6 @@ class CacheErrorsTest(DeltaGeneratorTestCase):
     are testing them word-for-word as much as possible. Even though this
     *feels* like an antipattern, it isn't: we're making sure the codepaths
     that pull useful debug info from the code are working.
-
-    TODO: parameterize these tests for both memo + singleton
     """
 
     maxDiff = None
@@ -111,32 +96,3 @@ def unserializable_return_value_func():
         )
         self.assertEqual(ep.message_is_markdown, True)
         self.assertEqual(ep.is_warning, False)
-
-    @parameterized.expand(
-        [
-            (ModinDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (ModinSeries(pd.Series(np.random.randn(2))),),
-            (PysparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (SnowpandasDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (SnowpandasSeries(pd.Series(np.random.randn(2))),),
-            (SnowparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (SnowparkTable(pd.DataFrame(np.random.randn(2, 2))),),
-        ]
-    )
-    def test_unevaluated_dataframe_error(self, data: Any):
-        @st.cache_data
-        def unevaluated_dataframe_func():
-            return data
-
-        with self.assertRaises(UnevaluatedDataFrameError) as cm:
-            unevaluated_dataframe_func()
-
-        ep = ExceptionProto()
-        exception.marshall(ep, cm.exception)
-
-        self.assertEqual(ep.type, "UnevaluatedDataFrameError")
-
-        expected_message = "Please call `collect()` or `to_pandas()` on the dataframe before returning it"
-        self.assertTrue(expected_message in ep.message)
-        self.assertEqual(ep.message_is_markdown, True)
-        self.assertEqual(ep.is_warning, False)
diff --git a/lib/tests/streamlit/snowpandas_mocks.py b/lib/tests/streamlit/snowpandas_mocks.py
index 9a8b13113065..7d93b55e38cf 100644
--- a/lib/tests/streamlit/snowpandas_mocks.py
+++ b/lib/tests/streamlit/snowpandas_mocks.py
@@ -41,7 +41,11 @@ def to_pandas(self) -> pd.DataFrame:
 
     def head(self, n: int) -> DataFrame:
         """Returns the top n element of a mock version of Snowpark Pandas DataFrame"""
-        return DataFrame(self._data.head(n))
+        return DataFrame(self[:n])
+
+    def __getitem__(self, key: slice | int) -> DataFrame:
+        # Allow slicing and integer indexing
+        return DataFrame(self._data[key])
 
 
 class Series:
@@ -65,4 +69,8 @@ def to_pandas(self) -> pd.Series:
 
     def head(self, n: int) -> Series:
         """Returns the top n element of a mock version of Snowpark Pandas Series"""
-        return Series(self._data.head(n))
+        return Series(self[:n])
+
+    def __getitem__(self, key: slice | int) -> Series:
+        # Allow slicing and integer indexing
+        return Series(self._data[key])
diff --git a/lib/tests/streamlit/snowpark_mocks.py b/lib/tests/streamlit/snowpark_mocks.py
index 83ecc1c7ee37..555bd55605ea 100644
--- a/lib/tests/streamlit/snowpark_mocks.py
+++ b/lib/tests/streamlit/snowpark_mocks.py
@@ -14,7 +14,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 if TYPE_CHECKING:
     import pandas as pd
@@ -74,3 +74,9 @@ class Row:
     for testing purposes."""
 
     __module__ = "snowflake.snowpark.row"
+
+    def __init__(self, row_data: dict[str, Any]):
+        self._row_data: dict[str, Any] = row_data
+
+    def as_dict(self) -> dict[str, Any]:
+        return self._row_data
diff --git a/lib/tests/streamlit/type_util_test.py b/lib/tests/streamlit/type_util_test.py
index f43d9de856b0..0731484e9c17 100644
--- a/lib/tests/streamlit/type_util_test.py
+++ b/lib/tests/streamlit/type_util_test.py
@@ -16,6 +16,7 @@
 
 import unittest
 from collections import namedtuple
+from typing import Any
 from unittest.mock import patch
 
 import numpy as np
@@ -26,6 +27,10 @@
 
 from streamlit import type_util
 from streamlit.errors import StreamlitAPIException
+from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
+from streamlit.runtime.secrets import Secrets
+from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
+from streamlit.user_info import UserInfoProxy
 
 
 class TypeUtilTest(unittest.TestCase):
@@ -129,3 +134,35 @@ def test_check_python_comparable_exception(self, sequence, type_str):
             ),
             str(exception_message.value),
         )
+
+    def test_has_callable_attr(self):
+        class TestClass:
+            def __init__(self) -> None:
+                self.also_not_callable = "I am not callable"
+
+            def callable_attr(self):
+                pass
+
+            @property
+            def not_callable_attr(self):
+                return "I am a property"
+
+        assert type_util.has_callable_attr(TestClass, "callable_attr") is True
+        assert type_util.has_callable_attr(TestClass, "not_callable_attr") is False
+        assert type_util.has_callable_attr(TestClass, "also_not_callable") is False
+        assert type_util.has_callable_attr(TestClass, "not_a_real_attr") is False
+
+    @parameterized.expand(
+        [
+            ({"key": "value"}, False),
+            (Secrets(), True),
+            (QueryParamsProxy(), True),
+            (SessionStateProxy(), True),
+            (StreamlitCookies({}), True),
+            (StreamlitHeaders({}), True),
+            (UserInfoProxy(), True),
+        ]
+    )
+    def test_is_custom_dict(self, dict_obj: Any, is_custom_dict: bool):
+        """Test that `is_custom_dict` returns True for all Streamlit custom dicts."""
+        assert type_util.is_custom_dict(dict_obj) is is_custom_dict
diff --git a/lib/tests/streamlit/write_test.py b/lib/tests/streamlit/write_test.py
index 151ebfad86db..852f982fd582 100644
--- a/lib/tests/streamlit/write_test.py
+++ b/lib/tests/streamlit/write_test.py
@@ -34,15 +34,11 @@
 from streamlit.error_util import handle_uncaught_app_exception
 from streamlit.errors import StreamlitAPIException
 from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
-from tests.streamlit.modin_mocks import DataFrame as ModinDataFrame
-from tests.streamlit.modin_mocks import Series as ModinSeries
-from tests.streamlit.pyspark_mocks import DataFrame as PysparkDataFrame
+from tests.streamlit.data_mocks import (
+    SHARED_TEST_CASES,
+    CaseMetadata,
+)
 from tests.streamlit.runtime.secrets_test import MOCK_TOML
-from tests.streamlit.snowpandas_mocks import DataFrame as SnowpandasDataFrame
-from tests.streamlit.snowpandas_mocks import Series as SnowpandasSeries
-from tests.streamlit.snowpark_mocks import DataFrame as SnowparkDataFrame
-from tests.streamlit.snowpark_mocks import Row as SnowparkRow
-from tests.streamlit.snowpark_mocks import Table as SnowparkTable
 
 
 class StreamlitWriteTest(unittest.TestCase):
@@ -168,6 +164,24 @@ class FakePyplot:
 
             p.assert_called_once()
 
+    @parameterized.expand(
+        SHARED_TEST_CASES,
+    )
+    def test_input_data(
+        self,
+        name: str,
+        input_data: Any,
+        metadata: CaseMetadata,
+    ):
+        """Test st.write with various input data and check that it uses
+        the expected command."""
+        with patch(
+            f"streamlit.delta_generator.DeltaGenerator.{metadata.expected_write_command}"
+        ) as p:
+            st.write(input_data)
+
+            p.assert_called_once()
+
     def test_plotly(self):
         import plotly.graph_objs as go
 
@@ -177,13 +191,6 @@ def test_plotly(self):
 
             p.assert_called_once()
 
-    def test_dict(self):
-        """Test st.write with dict."""
-        with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
-            st.write({"a": 1, "b": 2})
-
-            p.assert_called_once()
-
     def test_pil_image(self):
         """Test st.write with PIL image objects."""
         with patch("streamlit.delta_generator.DeltaGenerator.image") as p:
@@ -223,13 +230,6 @@ class FakeOpenaiStream:
 
             p.assert_called_once()
 
-    def test_list(self):
-        """Test st.write with list."""
-        with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
-            st.write([1, 2, 3])
-
-            p.assert_called_once()
-
     def test_namedtuple(self):
         """Test st.write with list."""
         with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
@@ -261,31 +261,6 @@ def test_streamlit_secrets(self, *mocks):
 
             p.assert_called_once()
 
-    @parameterized.expand(
-        [
-            (pd.DataFrame([[20, 30, 50]], columns=["a", "b", "c"]),),
-            (pd.Series(np.array(["a", "b", "c"])),),
-            (pd.Index(list("abc")),),
-            (pd.DataFrame({"a": [1], "b": [2]}).style.format("{:.2%}"),),
-            (np.array(["a", "b", "c"]),),
-            (SnowpandasSeries(pd.Series(np.random.randn(2))),),
-            (SnowpandasDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (SnowparkTable(pd.DataFrame(np.random.randn(2, 2))),),
-            (SnowparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (PysparkDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            (ModinSeries(pd.Series(np.random.randn(2))),),
-            (ModinDataFrame(pd.DataFrame(np.random.randn(2, 2))),),
-            ([SnowparkRow()],),
-        ]
-    )
-    def test_write_compatible_dataframe(
-        self,
-        input_data: Any,
-    ):
-        with patch("streamlit.delta_generator.DeltaGenerator.dataframe") as p:
-            st.write(input_data)
-            p.assert_called_once()
-
     @patch("streamlit.delta_generator.DeltaGenerator.markdown")
     @patch("streamlit.delta_generator.DeltaGenerator.json")
     def test_dict_and_string(self, mock_json, mock_markdown):


Code review comments:
diff_hunk: @@ -599,8 +594,10 @@ def table(self, data: Data = None) -> DeltaGenerator:
 
         """
 
-        # Check if data is uncollected, and collect it but with 100 rows max, instead of 10k rows, which is done in all other cases.
-        # Avoid this and use 100 rows in st.table, because large tables render slowly, take too much screen space, and can crush the app.
+        # Check if data is uncollected, and collect it but with 100 rows max, instead of
+        # 10k rows, which is done in all other cases.
+        # We usse 100 rows in st.table, because large tables render slowly,
path: lib/streamlit/elements/arrow.py
text: @user1:
nit: usse -> use

@author:
fixed 👍

diff_hunk: @@ -817,15 +817,31 @@ def data_editor(
 
         # Convert the user provided column config into the frontend compatible format:
         column_config_mapping = process_config_mapping(column_config)
-        apply_data_specific_configs(
-            column_config_mapping, data_df, data_format, check_arrow_compatibility=True
-        )
+
+        # Deactivate editing for columns that are not compatible with arrow
+        for column_name, column_data in data_df.items():
+            if dataframe_util.is_colum_type_arrow_incompatible(column_data):
+                update_column_config(
+                    column_config_mapping, column_name, {"disabled": True}
+                )
+                # Convert incompatible type to string
+                data_df[column_name] = column_data.astype("string")
+
+        apply_data_specific_configs(column_config_mapping, data_format)
 
         # Fix the column headers to work correctly for data editing:
         _fix_column_headers(data_df)
-        # Temporary workaround: We hide range indices if num_rows is dynamic.
-        # since the current way of handling this index during editing is a bit confusing.
-        if isinstance(data_df.index, pd.RangeIndex) and num_rows == "dynamic":
+
+        if not isinstance(data_df.index, pd.RangeIndex):
+            # If the index is not a range index, we will configure it as required
+            # since the user is required to provide a (unique) value for editing.
+            update_column_config(
+                column_config_mapping, INDEX_IDENTIFIER, {"required": True}
+            )
+        elif num_rows == "dynamic":
path: lib/streamlit/elements/widgets/data_editor.py
text: @user1:
it looks like you could merge the `elif` branch with the `if hide_index` [check below](https://github.com/streamlit/streamlit/pull/9205/files#diff-e591171c22c1c88a8244dbf9fd17e4f30b2d9d9cd93f8bac5479cd1ecd992448R849) since `hidden` will be overridden by that check anyways (which seems to make the comment also a little bit inaccurate).
```python
if hide_index is not None and num_rows == "dynamic":
```

@author:
I think it is not as easy to merge since: 1) this should only happen if the index is a range index and 2) the user should have the possibility to overwrite; and 3) if none of these conditions are True, it shouldn't set hidden at all. 

I pushed a small change to improve readability a bit.