Repository: dask/dask

Issue:
ddf.to_parquet(..., write_index=False) writes bad parquet metadata
# Summary
- `ddf.to_parquet(..., write_index=False)` writes parquet metadata `'index_columns':[[]]`, which appears incorrect because it breaks both the fastparquet and dask.dataframe consumers.
- `ddf.to_parquet(..., write_index=True)` works fine and avoids the bug
- `fastparquet.write(..., write_index=False)` also works fine, suggesting the bug is in dask

# Repro
```py
# Setup

import dask.dataframe as dd
import fastparquet
import os
import pandas as pd
import time
from distributed import Client

def fresh_file():
    return '/tmp/x-%s.parquet' % time.time()

def fresh_dir():
    path = fresh_file()
    os.mkdir(path)
    return path

# Install local dask client as global default
Client()

df = pd.DataFrame({'a':[1,2,3],'b':[4,5,6]})
ddf = dd.from_pandas(df, npartitions=2)

# Tests

# Good: all combinations of no-args {read local, read dask} x {write local, write dask}
path = fresh_file(); fastparquet.write(path, df); fastparquet.ParquetFile(path).to_pandas()
path = fresh_file(); fastparquet.write(path, df); dd.read_parquet(path).compute()
path = fresh_dir(); ddf.to_parquet(path); fastparquet.ParquetFile(path).to_pandas()
path = fresh_dir(); ddf.to_parquet(path); dd.read_parquet(path).compute()

# Good: ddf.to_parquet(..., write_index=True) with any {read local, read dask} x {no args, index=None}
path = fresh_dir(); ddf.to_parquet(path, write_index=True); fastparquet.ParquetFile(path).to_pandas()
path = fresh_dir(); ddf.to_parquet(path, write_index=True); fastparquet.ParquetFile(path).to_pandas(index=None)
path = fresh_dir(); ddf.to_parquet(path, write_index=True); dd.read_parquet(path).compute()
path = fresh_dir(); ddf.to_parquet(path, write_index=True); dd.read_parquet(path, index=None).compute()

# Bad: ddf.to_parquet(..., write_index=True) with any {read local, read dask} x {no args, index=None}
path = fresh_dir(); ddf.to_parquet(path, write_index=False); fastparquet.ParquetFile(path).to_pandas()
path = fresh_dir(); ddf.to_parquet(path, write_index=False); fastparquet.ParquetFile(path).to_pandas(index=None)
path = fresh_dir(); ddf.to_parquet(path, write_index=False); dd.read_parquet(path).compute()
path = fresh_dir(); ddf.to_parquet(path, write_index=False); dd.read_parquet(path, index=None).compute()

# Good: fastparquet.write(..., write_index=False) with any {read local, read dask} x {no args, index=None}
path = fresh_file(); fastparquet.write(path, df, write_index=True); fastparquet.ParquetFile(path).to_pandas()
path = fresh_file(); fastparquet.write(path, df, write_index=True); fastparquet.ParquetFile(path).to_pandas(index=None)
path = fresh_file(); fastparquet.write(path, df, write_index=True); dd.read_parquet(path).compute()
path = fresh_file(); fastparquet.write(path, df, write_index=True); dd.read_parquet(path, index=None).compute()
```

# Stacktraces
```py
>>> path = fresh_dir(); ddf.to_parquet(path, write_index=False); fastparquet.ParquetFile(path).to_pandas()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 342, in to_pandas
    timestamp96=timestamp96)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 368, in pre_allocate
    self._dtypes(categories), timestamp96=timestamp96)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'

>>> path = fresh_dir(); ddf.to_parquet(path, write_index=False); fastparquet.ParquetFile(path).to_pandas(index=None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 342, in to_pandas
    timestamp96=timestamp96)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 368, in pre_allocate
    self._dtypes(categories), timestamp96=timestamp96)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'

>>> path = fresh_dir(); ddf.to_parquet(path, write_index=False); dd.read_parquet(path).compute()
distributed.utils - ERROR - unhashable type: 'list'
Traceback (most recent call last):
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 212, in f
    result[0] = yield make_coro()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1544, in _get
    result = yield self._gather(packed)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 989, in _gather
    traceback)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/dataframe/io/parquet.py", line 134, in _read_parquet_row_group
    df, views = _pre_allocate(rg.num_rows, columns, categories, index, cs, dt)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/base.py", line 97, in compute
    (result,) = compute(self, traverse=False, **kwargs)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/base.py", line 204, in compute
    results = get(dsk, keys, **kwargs)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1583, in get
    resources=resources)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 223, in sync
    six.reraise(*error[0])
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 686, in reraise
    raise value
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 212, in f
    result[0] = yield make_coro()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1544, in _get
    result = yield self._gather(packed)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 989, in _gather
    traceback)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/dataframe/io/parquet.py", line 134, in _read_parquet_row_group
    df, views = _pre_allocate(rg.num_rows, columns, categories, index, cs, dt)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'

>>> path = fresh_dir(); ddf.to_parquet(path, write_index=False); dd.read_parquet(path, index=None).compute()
distributed.utils - ERROR - unhashable type: 'list'
Traceback (most recent call last):
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 212, in f
    result[0] = yield make_coro()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1544, in _get
    result = yield self._gather(packed)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 989, in _gather
    traceback)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/dataframe/io/parquet.py", line 134, in _read_parquet_row_group
    df, views = _pre_allocate(rg.num_rows, columns, categories, index, cs, dt)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/base.py", line 97, in compute
    (result,) = compute(self, traverse=False, **kwargs)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/base.py", line 204, in compute
    results = get(dsk, keys, **kwargs)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1583, in get
    resources=resources)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 223, in sync
    six.reraise(*error[0])
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 686, in reraise
    raise value
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/utils.py", line 212, in f
    result[0] = yield make_coro()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 1544, in _get
    result = yield self._gather(packed)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1015, in run
    value = future.result()
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/concurrent.py", line 237, in result
    raise_exc_info(self._exc_info)
  File "<string>", line 3, in raise_exc_info
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/tornado/gen.py", line 1021, in run
    yielded = self.gen.throw(*exc_info)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/distributed/client.py", line 989, in _gather
    traceback)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/dask/dataframe/io/parquet.py", line 134, in _read_parquet_row_group
    df, views = _pre_allocate(rg.num_rows, columns, categories, index, cs, dt)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 457, in _pre_allocate
    index_type = get_type(index)
  File "/Users/danb/miniconda3/envs/signals/lib/python3.6/site-packages/fastparquet/api.py", line 450, in get_type
    if name in categories:
TypeError: unhashable type: 'list'
```

# Versions
```sh
$ python --version
Python 3.6.0

$ cat requirements.txt | egrep 'dask|fastparquet'
git+https://github.com/dask/dask.git@8e4137f
git+https://github.com/dask/fastparquet.git@a711c78
```


Discussion:
(Oops, forgot to include stacktraces. Added above.)

Resolving patch:
diff --git a/dask/dataframe/io/parquet.py b/dask/dataframe/io/parquet.py
--- a/dask/dataframe/io/parquet.py
+++ b/dask/dataframe/io/parquet.py
@@ -362,7 +362,7 @@ def to_parquet(path, df, compression=None, write_index=None, has_nulls=True,
         index_col = df.columns[0]
     else:
         ignore_divisions = True
-        index_col = []
+        index_col = None
 
     object_encoding = object_encoding or 'utf8'
     if object_encoding == 'infer' or (isinstance(object_encoding, dict) and


Test patch:
diff --git a/dask/dataframe/io/tests/test_parquet.py b/dask/dataframe/io/tests/test_parquet.py
--- a/dask/dataframe/io/tests/test_parquet.py
+++ b/dask/dataframe/io/tests/test_parquet.py
@@ -438,3 +438,10 @@ def test_filters(fn):
     ddf.repartition(npartitions=2, force=True).to_parquet(fn)
     dd.read_parquet(fn, filters=[('at', '==', 'aa')]).compute()
     assert len(ddf2) > 0
+
+
+def test_no_index(fn):
+    df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
+    ddf = dd.from_pandas(df, npartitions=2)
+    ddf.to_parquet(fn, write_index=False)
+    dd.read_parquet(fn).compute()