Repository: dask/dask

Issue:
Better error when creating Dask DataFrames from a source with columns named after special attributes
<!-- Please include a self-contained copy-pastable example that generates the issue if possible.

Please be concise with code posted. See guidelines below on how to provide a good bug report:

- Craft Minimal Bug Reports http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports
- Minimal Complete Verifiable Examples https://stackoverflow.com/help/mcve

Bug reports that follow these guidelines are easier to diagnose, and so are often handled much more quickly.
-->

Hello,

I was trying to use Dask to handle a dataset that contained a column named "divisions". This was a large dataset with 90.000 columns or so (one for each word in a set of documents), so I did not know beforehand that a column existed with that particular name.

Whenever I tried to load that data into a Dask DataFrame, either by using `read_csv` or `read_pandas`, an Exception occurred ("TypeError: Column assignment doesn't support type tuple" [(Full trace here)](https://gist.github.com/jppcr/90aea59c4585daa0dd5f854e5cb12de6)). With a little debugging using pdb, I ended up discovering that the problem was occurring because of the name of that particular column. It makes some sense, since "divisions" is the name of a DataFrame attribute. However, just looking at the error would never have led me to discover that.

I suggest that the error message is improved in the cases where the user tries to create a DataFrame with columns containing "reserved words" like attribute names. That would have helped me in this case and I think it would help other people in the future too. My suggestion would be that whenever a word from a forbidden list was used in a column, the error would be "Error: Source contains a column with a reserved name: '\<name of the column here\>'."

To reproduce the current behaviour:

Create a CSV with the following content:

```
a,b,divisions,c
1,2,3,4
```
Both code snippets below result in an Exception:
```
import dask
import dask.dataframe as dd
ddf = dd.read_csv("test_dask_error.csv")
```

```
import pandas as pd
df = pd.read_csv("test_dask_error.csv")
ddf2 = dd.from_pandas(df, npartitions=2)
```

...the Exception being [(full trace in this gist)](https://gist.github.com/jppcr/90aea59c4585daa0dd5f854e5cb12de6)

`TypeError: Column assignment doesn't support type tuple`

My environment:

- Dask version: 2021.4.0
- Python version: 3.7.3
- Operating System: Linux 4.19 (Debian)
- Install method (conda, pip, source): pip



Discussion:
Thanks for writing this up so clearly! I think Dask can handle this like how pandas handles a regular column named `index` where you can't access the regular index column using `df.index`, but you can do `df["index"]`. I'll open up a PR to test out this approach. 

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -3902,7 +3902,8 @@ def __setattr__(self, key, value):
         except AttributeError:
             columns = ()
 
-        if key in columns:
+        # exclude protected attributes from setitem
+        if key in columns and key not in ["divisions", "dask", "_name", "_meta"]:
             self[key] = value
         else:
             object.__setattr__(self, key, value)


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -214,6 +214,20 @@ def test_column_names():
     assert (d["a"] + d["b"]).name is None
 
 
+def test_columns_named_divisions_and_meta():
+    # https://github.com/dask/dask/issues/7599
+    df = pd.DataFrame(
+        {"_meta": [1, 2, 3, 4], "divisions": ["a", "b", "c", "d"]},
+        index=[0, 1, 3, 5],
+    )
+    ddf = dd.from_pandas(df, 2)
+
+    assert ddf.divisions == (0, 3, 5)
+    assert_eq(ddf["divisions"], df.divisions)
+    assert all(ddf._meta.columns == ["_meta", "divisions"])
+    assert_eq(ddf["_meta"], df._meta)
+
+
 def test_index_names():
     assert d.index.name is None