Repository: streamlit/streamlit

Issue:
Make individual columns editable
It would be cool if in the experimental data editor function it was possible to select only the necessary columns / keys for editing, since the data may contain critical values ​​that the user cannot change.

Usage example:
- there is a boolean column in the data, the user will be able to mark only those lines with which he will continue to work without interfering with the data;
- when importing tables, the user will be able to mark only the necessary columns for import without interfering with the data

etc.

Code example:

```
import streamlit as st
import pandas as pd

df = pd.DataFrame(
    [
       {"command": "st.selectbox", "rating": 4, "is_widget": True},
       {"command": "st.balloons", "rating": 5, "is_widget": False},
       {"command": "st.time_input", "rating": 3, "is_widget": True},
   ]
)
edited_df = st.experimental_data_editor(df, edit_columns=["is_widget"])
```

---

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.**



Discussion:
@versus666jzx Thanks for suggesting this! If I understand correctly, you would like to deactivate editing for specific columns. For example, in the example below, only editing for `rating` but not for `command` and `is_widget`?
@LukasMasuch Yes that's right. But in the above example, one `is_widget` column is available for editing, the rest are inactive for editing.
I added the `edit_columns` parameter which specifies the list of columns available for editing as an example of the final implementation.
Ahh, I see! I haven't looked that closely at your example. Configuring columns will be one of the next features we will release. It's very likely that we will allow configuring column editability
Forgot to update, we're currently working on a solution for this!

Resolving patch:
diff --git a/e2e/scripts/st_data_editor_config.py b/e2e/scripts/st_data_editor_config.py
new file mode 100644
--- /dev/null
+++ b/e2e/scripts/st_data_editor_config.py
@@ -0,0 +1,34 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import random
+
+import numpy as np
+import pandas as pd
+
+import streamlit as st
+
+np.random.seed(0)
+random.seed(0)
+
+st.set_page_config(layout="wide")
+
+# Generate a random dataframe
+df = pd.DataFrame(
+    np.random.randn(5, 5),
+    columns=("col_%d" % i for i in range(5)),
+)
+
+st.experimental_data_editor(df, disabled=True)
+st.experimental_data_editor(df, disabled=["col_4", "col_1"])
diff --git a/lib/streamlit/elements/data_editor.py b/lib/streamlit/elements/data_editor.py
--- a/lib/streamlit/elements/data_editor.py
+++ b/lib/streamlit/elements/data_editor.py
@@ -20,6 +20,7 @@
     TYPE_CHECKING,
     Any,
     Dict,
+    Iterable,
     List,
     Mapping,
     Optional,
@@ -415,7 +416,7 @@ def experimental_data_editor(
         height: Optional[int] = None,
         use_container_width: bool = False,
         num_rows: Literal["fixed", "dynamic"] = "fixed",
-        disabled: bool = False,
+        disabled: bool | Iterable[str] = False,
         key: Optional[Key] = None,
         on_change: Optional[WidgetCallback] = None,
         args: Optional[WidgetArgs] = None,
@@ -432,7 +433,7 @@ def experimental_data_editor(
         height: Optional[int] = None,
         use_container_width: bool = False,
         num_rows: Literal["fixed", "dynamic"] = "fixed",
-        disabled: bool = False,
+        disabled: bool | Iterable[str] = False,
         key: Optional[Key] = None,
         on_change: Optional[WidgetCallback] = None,
         args: Optional[WidgetArgs] = None,
@@ -449,7 +450,7 @@ def experimental_data_editor(
         height: Optional[int] = None,
         use_container_width: bool = False,
         num_rows: Literal["fixed", "dynamic"] = "fixed",
-        disabled: bool = False,
+        disabled: bool | Iterable[str] = False,
         key: Optional[Key] = None,
         on_change: Optional[WidgetCallback] = None,
         args: Optional[WidgetArgs] = None,
@@ -484,8 +485,11 @@ def experimental_data_editor(
             Defaults to "fixed".
 
         disabled : bool
-            An optional boolean which, if True, disables the data editor and prevents
-            any edits. Defaults to False. This argument can only be supplied by keyword.
+            Controls the editing of columns. If set to True, editing
+            is disabled for all columns. If an iterable of column names is provided
+            (e.g., `disabled=("col1", "col2")`), only the specified columns will be
+            disabled for editing. By default, all columns that support editing
+            are editable.
 
         key : str
             An optional string to use as the unique key for this widget. If this
@@ -593,6 +597,14 @@ def experimental_data_editor(
                 columns_config[INDEX_IDENTIFIER] = {}
             columns_config[INDEX_IDENTIFIER]["hidden"] = True
 
+        # If disabled not a boolean, we assume it is a list of columns to disable.
+        # This gets translated into the columns configuration:
+        if not isinstance(disabled, bool):
+            for column in disabled:
+                if column not in columns_config:
+                    columns_config[column] = {}
+                columns_config[column]["disabled"] = True
+
         # Convert the dataframe to an arrow table which is used as the main
         # serialization format for sending the data to the frontend.
         # We also utilize the arrow schema to determine the data kinds of every column.
@@ -611,7 +623,9 @@ def experimental_data_editor(
         if height:
             proto.height = height
 
-        proto.disabled = disabled
+        # Only set disabled to true if it is actually true
+        # It can also be a list of columns, which should result in false here.
+        proto.disabled = disabled is True
         proto.editing_mode = (
             ArrowProto.EditingMode.DYNAMIC
             if num_rows == "dynamic"


Test patch:
diff --git a/lib/tests/streamlit/elements/data_editor_test.py b/lib/tests/streamlit/elements/data_editor_test.py
--- a/lib/tests/streamlit/elements/data_editor_test.py
+++ b/lib/tests/streamlit/elements/data_editor_test.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import datetime
+import json
 import unittest
 from typing import Any, Dict, List, Mapping
 from unittest.mock import MagicMock, patch
@@ -269,13 +270,20 @@ def test_apply_data_specific_configs_disables_columns(self):
 
 
 class DataEditorTest(DeltaGeneratorTestCase):
-    def test_just_disabled(self):
-        """Test that it can be called with disabled param."""
+    def test_just_disabled_true(self):
+        """Test that it can be called with disabled=True param."""
         st.experimental_data_editor(pd.DataFrame(), disabled=True)
 
         proto = self.get_delta_from_queue().new_element.arrow_data_frame
         self.assertEqual(proto.disabled, True)
 
+    def test_just_disabled_false(self):
+        """Test that it can be called with disabled=False param."""
+        st.experimental_data_editor(pd.DataFrame(), disabled=False)
+
+        proto = self.get_delta_from_queue().new_element.arrow_data_frame
+        self.assertEqual(proto.disabled, False)
+
     def test_just_width_height(self):
         """Test that it can be called with width and height."""
         st.experimental_data_editor(pd.DataFrame(), width=300, height=400)
@@ -305,6 +313,26 @@ def test_just_use_container_width(self):
         proto = self.get_delta_from_queue().new_element.arrow_data_frame
         self.assertEqual(proto.use_container_width, True)
 
+    def test_disable_individual_columns(self):
+        """Test that disable can be used to disable individual columns."""
+        data_df = pd.DataFrame(
+            {
+                "a": pd.Series([1, 2]),
+                "b": pd.Series(["foo", "bar"]),
+                "c": pd.Series([1, 2]),
+                "d": pd.Series(["foo", "bar"]),
+            }
+        )
+
+        st.experimental_data_editor(data_df, disabled=["a", "b"])
+
+        proto = self.get_delta_from_queue().new_element.arrow_data_frame
+        self.assertEqual(proto.disabled, False)
+        self.assertEqual(
+            proto.columns,
+            json.dumps({"a": {"disabled": True}, "b": {"disabled": True}}),
+        )
+
     def test_outside_form(self):
         """Test that form id is marshalled correctly outside of a form."""
         st.experimental_data_editor(pd.DataFrame())