Repository: pandas-dev/pandas

Issue:
"to_hdf()" with "format='table'" ignores encoder "errors" argument.
#### Code Sample, a copy-pastable example if possible

```python
s = '\ud800'
srs = _pd.Series()
srs.loc[ 0 ] = s

srs.to_hdf( 'srs.hdf', key = 'srs', mode = 'w', format = 'fixed' )  # works fine
srs.to_hdf( 'srs.hdf', key = 'srs', mode = 'w', format = 'table' )
        # fails with "UnicodeEncodeError: 'utf-8' codec can't encode character
        # '\ud800' in position 0: surrogates not allowed"
```
#### Problem description

The default encoder options for `to_hdf()` with `format = 'table'` seem to specify `errors = 'strict'` as a default for the encoding. The problem is that while one can specify the encoding with, e.g.,  `encoding = 'utf-8'`, the encoder `errors` cannot be specified.

Below is an iPython stacktrace:
<details>

srs.to_hdf( 'srs.hdf', key = 'srs', mode = 'w', format = 'table' )
---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)
<ipython-input-51-b53cbd49769b> in <module>()
----> 1 srs.to_hdf( 'srs.hdf', key = 'srs', mode = 'w', format = 'table' )

/usr/lib/python3.6/site-packages/pandas/core/generic.py in to_hdf(self, path_or_buf, key, **kwargs)
   1469 
   1470         from pandas.io import pytables
-> 1471         return pytables.to_hdf(path_or_buf, key, self, **kwargs)
   1472 
   1473     def to_msgpack(self, path_or_buf=None, encoding='utf-8', **kwargs):

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in to_hdf(path_or_buf, key, value, mode, complevel, complib, append, **kwargs)
    279         with HDFStore(path_or_buf, mode=mode, complevel=complevel,
    280                       complib=complib) as store:
--> 281             f(store)
    282     else:
    283         f(path_or_buf)

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in <lambda>(store)
    273         f = lambda store: store.append(key, value, **kwargs)
    274     else:
--> 275         f = lambda store: store.put(key, value, **kwargs)
    276 
    277     path_or_buf = _stringify_path(path_or_buf)

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in put(self, key, value, format, append, **kwargs)
    864             format = get_option("io.hdf.default_format") or 'fixed'
    865         kwargs = self._validate_format(format, kwargs)
--> 866         self._write_to_group(key, value, append=append, **kwargs)
    867 
    868     def remove(self, key, where=None, start=None, stop=None):

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in _write_to_group(self, key, value, format, index, append, complib, encoding, **kwargs)
   1339 
   1340         # write the object
-> 1341         s.write(obj=value, append=append, complib=complib, **kwargs)
   1342 
   1343         if s.is_table and index:

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in write(self, obj, data_columns, **kwargs)
   4208             obj.columns = [name]
   4209         return super(AppendableSeriesTable, self).write(
-> 4210             obj=obj, data_columns=obj.columns.tolist(), **kwargs)
   4211 
   4212     def read(self, columns=None, **kwargs):

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in write(self, obj, axes, append, complib, complevel, fletcher32, min_itemsize, chunksize, expectedrows, dropna, **kwargs)
   3905         self.create_axes(axes=axes, obj=obj, validate=append,
   3906                          min_itemsize=min_itemsize,
-> 3907                          **kwargs)
   3908 
   3909         for a in self.axes:

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in create_axes(self, axes, obj, validate, nan_rep, data_columns, min_itemsize, **kwargs)
   3577                 self.values_axes.append(col)
   3578             except (NotImplementedError, ValueError, TypeError) as e:
-> 3579                 raise e
   3580             except Exception as detail:
   3581                 raise Exception(

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in create_axes(self, axes, obj, validate, nan_rep, data_columns, min_itemsize, **kwargs)
   3572                              encoding=self.encoding,
   3573                              info=self.info,
-> 3574                              **kwargs)
   3575                 col.set_pos(j)
   3576 

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in set_atom(self, block, block_items, existing_col, min_itemsize, nan_rep, info, encoding, **kwargs)
   1923                 min_itemsize,
   1924                 nan_rep,
-> 1925                 encoding)
   1926 
   1927         # set as a data block

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in set_atom_string(self, block, block_items, existing_col, min_itemsize, nan_rep, encoding)
   1959 
   1960         # itemsize is the maximum length of a string (along any dimension)
-> 1961         data_converted = _convert_string_array(data, encoding)
   1962         itemsize = data_converted.itemsize
   1963 

/usr/lib/python3.6/site-packages/pandas/io/pytables.py in _convert_string_array(data, encoding, itemsize)
   4569     if encoding is not None and len(data):
   4570         data = Series(data.ravel()).str.encode(
-> 4571             encoding).values.reshape(data.shape)
   4572 
   4573     # create the sized dtype

/usr/lib/python3.6/site-packages/pandas/core/strings.py in encode(self, encoding, errors)
   1655     @copy(str_encode)
   1656     def encode(self, encoding, errors="strict"):
-> 1657         result = str_encode(self._data, encoding, errors)
   1658         return self._wrap_result(result)
   1659 

/usr/lib/python3.6/site-packages/pandas/core/strings.py in str_encode(arr, encoding, errors)
   1309         encoder = codecs.getencoder(encoding)
   1310         f = lambda x: encoder(x, errors)[0]
-> 1311     return _na_map(f, arr)
   1312 
   1313 

/usr/lib/python3.6/site-packages/pandas/core/strings.py in _na_map(f, arr, na_result, dtype)
    154 def _na_map(f, arr, na_result=np.nan, dtype=object):
    155     # should really _check_ for NA
--> 156     return _map(f, arr, na_mask=True, na_value=na_result, dtype=dtype)
    157 
    158 

/usr/lib/python3.6/site-packages/pandas/core/strings.py in _map(f, arr, na_mask, na_value, dtype)
    169         try:
    170             convert = not all(mask)
--> 171             result = lib.map_infer_mask(arr, f, mask.view(np.uint8), convert)
    172         except (TypeError, AttributeError) as e:
    173             # Reraise the exception if callable `f` got wrong number of args.

pandas/_libs/src/inference.pyx in pandas._libs.lib.map_infer_mask()

/usr/lib/python3.6/site-packages/pandas/core/strings.py in <lambda>(x)
   1308     else:
   1309         encoder = codecs.getencoder(encoding)
-> 1310         f = lambda x: encoder(x, errors)[0]
   1311     return _na_map(f, arr)
   1312 

UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800' in position 0: surrogates not allowed

</details>

#### Expected Output

What I'd like to see is the following working:
```python
srs.to_hdf( 'srs.hdf', key = 'srs', mode = 'w', format = 'table', \
        encoding = 'utf-8', errors = 'surrogatepass' )
```
That is to say, I'd like to be able to specify the `error` argument for an encoder (and, of course, for the decoder also).

Perhaps better yet would be to also change the default errors (at least in this case) to `errors = 'surrogatepass'` for both the encoder and decoder, so as to preserve the strings as they are without errors or surprises.

As an example in plain Python:
```python
s = '\ud800'
b = s.encode( 'utf-8', errors = 'surrogatepass' )
s == b.decode( 'utf-8', errors = 'surrogatepass' )  # True, and no errors - it just 'works'
```

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.5.final.0
python-bits: 64
OS: Linux
OS-release: 4.16.3-1-ARCH
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.22.0
pytest: 3.5.0
pip: 9.0.1
setuptools: 39.0.1
Cython: None
numpy: 1.14.2
scipy: 1.0.1
pyarrow: None
xarray: None
IPython: 6.3.1
sphinx: None
patsy: 0.5.0
dateutil: 2.7.2
pytz: 2018.4
blosc: 1.5.1
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.4
feather: None
matplotlib: 2.2.2
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: None
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: 0.6.0

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -1946,6 +1946,10 @@ def to_hdf(self, path_or_buf, key, **kwargs):
             If applying compression use the fletcher32 checksum.
         dropna : bool, default False
             If true, ALL nan rows will not be written to store.
+        errors : str, default 'strict'
+            Specifies how encoding and decoding errors are to be handled.
+            See the errors argument for :func:`open` for a full list
+            of options.
 
         See Also
         --------
diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py
--- a/pandas/io/pytables.py
+++ b/pandas/io/pytables.py
@@ -317,7 +317,11 @@ def read_hdf(path_or_buf, key=None, mode='r', **kwargs):
         Return an iterator object.
     chunksize : int, optional
         Number of rows to include in an iteration when using an iterator.
-    kwargs : dict
+    errors : str, default 'strict'
+        Specifies how encoding and decoding errors are to be handled.
+        See the errors argument for :func:`open` for a full list
+        of options.
+    **kwargs
         Additional keyword arguments passed to HDFStore.
 
     Returns
@@ -727,7 +731,7 @@ def select(self, key, where=None, start=None, stop=None, columns=None,
         def func(_start, _stop, _where):
             return s.read(start=_start, stop=_stop,
                           where=_where,
-                          columns=columns, **kwargs)
+                          columns=columns)
 
         # create the iterator
         it = TableIterator(self, s, func, where=where, nrows=s.nrows,
@@ -1588,14 +1592,14 @@ def infer(self, handler):
         new_self.read_metadata(handler)
         return new_self
 
-    def convert(self, values, nan_rep, encoding):
+    def convert(self, values, nan_rep, encoding, errors):
         """ set the values from this selection: take = take ownership """
 
         # values is a recarray
         if values.dtype.fields is not None:
             values = values[self.cname]
 
-        values = _maybe_convert(values, self.kind, encoding)
+        values = _maybe_convert(values, self.kind, encoding, errors)
 
         kwargs = dict()
         if self.freq is not None:
@@ -1770,7 +1774,7 @@ class GenericIndexCol(IndexCol):
     def is_indexed(self):
         return False
 
-    def convert(self, values, nan_rep, encoding):
+    def convert(self, values, nan_rep, encoding, errors):
         """ set the values from this selection: take = take ownership """
 
         self.values = Int64Index(np.arange(self.table.nrows))
@@ -1899,7 +1903,7 @@ def set_kind(self):
                 self.typ = getattr(self.description, self.cname, None)
 
     def set_atom(self, block, block_items, existing_col, min_itemsize,
-                 nan_rep, info, encoding=None, **kwargs):
+                 nan_rep, info, encoding=None, errors='strict'):
         """ create and setup my atom from the block b """
 
         self.values = list(block_items)
@@ -1944,7 +1948,8 @@ def set_atom(self, block, block_items, existing_col, min_itemsize,
                 existing_col,
                 min_itemsize,
                 nan_rep,
-                encoding)
+                encoding,
+                errors)
 
         # set as a data block
         else:
@@ -1954,7 +1959,7 @@ def get_atom_string(self, block, itemsize):
         return _tables().StringCol(itemsize=itemsize, shape=block.shape[0])
 
     def set_atom_string(self, block, block_items, existing_col, min_itemsize,
-                        nan_rep, encoding):
+                        nan_rep, encoding, errors):
         # fill nan items with myself, don't disturb the blocks by
         # trying to downcast
         block = block.fillna(nan_rep, downcast=False)
@@ -1980,7 +1985,7 @@ def set_atom_string(self, block, block_items, existing_col, min_itemsize,
                     )
 
         # itemsize is the maximum length of a string (along any dimension)
-        data_converted = _convert_string_array(data, encoding)
+        data_converted = _convert_string_array(data, encoding, errors)
         itemsize = data_converted.itemsize
 
         # specified min_itemsize?
@@ -2111,7 +2116,7 @@ def validate_attr(self, append):
                 raise ValueError("appended items dtype do not match existing "
                                  "items dtype in table!")
 
-    def convert(self, values, nan_rep, encoding):
+    def convert(self, values, nan_rep, encoding, errors):
         """set the data from this selection (and convert to the correct dtype
         if we can)
         """
@@ -2185,7 +2190,7 @@ def convert(self, values, nan_rep, encoding):
         # convert nans / decode
         if _ensure_decoded(self.kind) == u('string'):
             self.data = _unconvert_string_array(
-                self.data, nan_rep=nan_rep, encoding=encoding)
+                self.data, nan_rep=nan_rep, encoding=encoding, errors=errors)
 
         return self
 
@@ -2251,10 +2256,12 @@ class Fixed(StringMixin):
     ndim = None
     is_table = False
 
-    def __init__(self, parent, group, encoding=None, **kwargs):
+    def __init__(self, parent, group, encoding=None, errors='strict',
+                 **kwargs):
         self.parent = parent
         self.group = group
         self.encoding = _ensure_encoding(encoding)
+        self.errors = errors
         self.set_version()
 
     @property
@@ -2458,10 +2465,12 @@ def is_exists(self):
     def set_attrs(self):
         """ set our object attributes """
         self.attrs.encoding = self.encoding
+        self.attrs.errors = self.errors
 
     def get_attrs(self):
         """ retrieve our attributes """
         self.encoding = _ensure_encoding(getattr(self.attrs, 'encoding', None))
+        self.errors = getattr(self.attrs, 'errors', 'strict')
         for n in self.attributes:
             setattr(self, n, _ensure_decoded(getattr(self.attrs, n, None)))
 
@@ -2528,7 +2537,7 @@ def write_index(self, key, index):
             self.write_sparse_intindex(key, index)
         else:
             setattr(self.attrs, '%s_variety' % key, 'regular')
-            converted = _convert_index(index, self.encoding,
+            converted = _convert_index(index, self.encoding, self.errors,
                                        self.format_type).set_name('index')
 
             self.write_array(key, converted.values)
@@ -2574,7 +2583,7 @@ def write_multi_index(self, key, index):
                                                  index.names)):
             # write the level
             level_key = '%s_level%d' % (key, i)
-            conv_level = _convert_index(lev, self.encoding,
+            conv_level = _convert_index(lev, self.encoding, self.errors,
                                         self.format_type).set_name(level_key)
             self.write_array(level_key, conv_level.values)
             node = getattr(self.group, level_key)
@@ -2635,11 +2644,13 @@ def read_index_node(self, node, start=None, stop=None):
 
         if kind in (u('date'), u('datetime')):
             index = factory(_unconvert_index(data, kind,
-                                             encoding=self.encoding),
+                                             encoding=self.encoding,
+                                             errors=self.errors),
                             dtype=object, **kwargs)
         else:
             index = factory(_unconvert_index(data, kind,
-                                             encoding=self.encoding), **kwargs)
+                                             encoding=self.encoding,
+                                             errors=self.errors), **kwargs)
 
         index.name = name
 
@@ -2752,7 +2763,8 @@ def read_index_legacy(self, key, start=None, stop=None):
         node = getattr(self.group, key)
         data = node[start:stop]
         kind = node._v_attrs.kind
-        return _unconvert_index_legacy(data, kind, encoding=self.encoding)
+        return _unconvert_index_legacy(data, kind, encoding=self.encoding,
+                                       errors=self.errors)
 
 
 class LegacySeriesFixed(LegacyFixed):
@@ -3171,7 +3183,8 @@ def write_metadata(self, key, values):
         """
         values = Series(values)
         self.parent.put(self._get_metadata_path(key), values, format='table',
-                        encoding=self.encoding, nan_rep=self.nan_rep)
+                        encoding=self.encoding, errors=self.errors,
+                        nan_rep=self.nan_rep)
 
     def read_metadata(self, key):
         """ return the meta data array for this key """
@@ -3192,6 +3205,7 @@ def set_attrs(self):
         self.attrs.data_columns = self.data_columns
         self.attrs.nan_rep = self.nan_rep
         self.attrs.encoding = self.encoding
+        self.attrs.errors = self.errors
         self.attrs.levels = self.levels
         self.attrs.metadata = self.metadata
         self.set_info()
@@ -3207,6 +3221,7 @@ def get_attrs(self):
         self.nan_rep = getattr(self.attrs, 'nan_rep', None)
         self.encoding = _ensure_encoding(
             getattr(self.attrs, 'encoding', None))
+        self.errors = getattr(self.attrs, 'errors', 'strict')
         self.levels = getattr(
             self.attrs, 'levels', None) or []
         self.index_axes = [
@@ -3364,7 +3379,8 @@ def read_axes(self, where, **kwargs):
         # convert the data
         for a in self.axes:
             a.set_info(self.info)
-            a.convert(values, nan_rep=self.nan_rep, encoding=self.encoding)
+            a.convert(values, nan_rep=self.nan_rep, encoding=self.encoding,
+                      errors=self.errors)
 
         return True
 
@@ -3446,6 +3462,7 @@ def create_axes(self, axes, obj, validate=True, nan_rep=None,
             data_columns = existing_table.data_columns
             nan_rep = existing_table.nan_rep
             self.encoding = existing_table.encoding
+            self.errors = existing_table.errors
             self.info = copy.copy(existing_table.info)
         else:
             existing_table = None
@@ -3472,7 +3489,7 @@ def create_axes(self, axes, obj, validate=True, nan_rep=None,
             if i in axes:
                 name = obj._AXIS_NAMES[i]
                 index_axes_map[i] = _convert_index(
-                    a, self.encoding, self.format_type
+                    a, self.encoding, self.errors, self.format_type
                 ).set_name(name).set_axis(i)
             else:
 
@@ -3591,8 +3608,8 @@ def get_blk_items(mgr, blocks):
                              min_itemsize=min_itemsize,
                              nan_rep=nan_rep,
                              encoding=self.encoding,
-                             info=self.info,
-                             **kwargs)
+                             errors=self.errors,
+                             info=self.info)
                 col.set_pos(j)
 
                 self.values_axes.append(col)
@@ -3756,7 +3773,8 @@ def read_column(self, column, where=None, start=None, stop=None, **kwargs):
                 a.set_info(self.info)
                 return Series(_set_tz(a.convert(c[start:stop],
                                                 nan_rep=self.nan_rep,
-                                                encoding=self.encoding
+                                                encoding=self.encoding,
+                                                errors=self.errors
                                                 ).take_data(),
                                       a.tz, True), name=column)
 
@@ -4437,7 +4455,7 @@ def _set_tz(values, tz, preserve_UTC=False, coerce=False):
     return values
 
 
-def _convert_index(index, encoding=None, format_type=None):
+def _convert_index(index, encoding=None, errors='strict', format_type=None):
     index_name = getattr(index, 'name', None)
 
     if isinstance(index, DatetimeIndex):
@@ -4491,7 +4509,7 @@ def _convert_index(index, encoding=None, format_type=None):
         # atom = _tables().ObjectAtom()
         # return np.asarray(values, dtype='O'), 'object', atom
 
-        converted = _convert_string_array(values, encoding)
+        converted = _convert_string_array(values, encoding, errors)
         itemsize = converted.dtype.itemsize
         return IndexCol(
             converted, 'string', _tables().StringCol(itemsize),
@@ -4522,7 +4540,7 @@ def _convert_index(index, encoding=None, format_type=None):
                         index_name=index_name)
 
 
-def _unconvert_index(data, kind, encoding=None):
+def _unconvert_index(data, kind, encoding=None, errors='strict'):
     kind = _ensure_decoded(kind)
     if kind == u('datetime64'):
         index = DatetimeIndex(data)
@@ -4541,7 +4559,8 @@ def _unconvert_index(data, kind, encoding=None):
     elif kind in (u('integer'), u('float')):
         index = np.asarray(data)
     elif kind in (u('string')):
-        index = _unconvert_string_array(data, nan_rep=None, encoding=encoding)
+        index = _unconvert_string_array(data, nan_rep=None, encoding=encoding,
+                                        errors=errors)
     elif kind == u('object'):
         index = np.asarray(data[0])
     else:  # pragma: no cover
@@ -4549,20 +4568,22 @@ def _unconvert_index(data, kind, encoding=None):
     return index
 
 
-def _unconvert_index_legacy(data, kind, legacy=False, encoding=None):
+def _unconvert_index_legacy(data, kind, legacy=False, encoding=None,
+                            errors='strict'):
     kind = _ensure_decoded(kind)
     if kind == u('datetime'):
         index = to_datetime(data)
     elif kind in (u('integer')):
         index = np.asarray(data, dtype=object)
     elif kind in (u('string')):
-        index = _unconvert_string_array(data, nan_rep=None, encoding=encoding)
+        index = _unconvert_string_array(data, nan_rep=None, encoding=encoding,
+                                        errors=errors)
     else:  # pragma: no cover
         raise ValueError('unrecognized index type %s' % kind)
     return index
 
 
-def _convert_string_array(data, encoding, itemsize=None):
+def _convert_string_array(data, encoding, errors, itemsize=None):
     """
     we take a string-like that is object dtype and coerce to a fixed size
     string type
@@ -4571,6 +4592,7 @@ def _convert_string_array(data, encoding, itemsize=None):
     ----------
     data : a numpy array of object dtype
     encoding : None or string-encoding
+    errors : handler for encoding errors
     itemsize : integer, optional, defaults to the max length of the strings
 
     Returns
@@ -4581,7 +4603,7 @@ def _convert_string_array(data, encoding, itemsize=None):
     # encode if needed
     if encoding is not None and len(data):
         data = Series(data.ravel()).str.encode(
-            encoding).values.reshape(data.shape)
+            encoding, errors).values.reshape(data.shape)
 
     # create the sized dtype
     if itemsize is None:
@@ -4592,7 +4614,8 @@ def _convert_string_array(data, encoding, itemsize=None):
     return data
 
 
-def _unconvert_string_array(data, nan_rep=None, encoding=None):
+def _unconvert_string_array(data, nan_rep=None, encoding=None,
+                            errors='strict'):
     """
     inverse of _convert_string_array
 
@@ -4601,6 +4624,7 @@ def _unconvert_string_array(data, nan_rep=None, encoding=None):
     data : fixed length string dtyped array
     nan_rep : the storage repr of NaN, optional
     encoding : the encoding of the data, optional
+    errors : handler for encoding errors, default 'strict'
 
     Returns
     -------
@@ -4622,7 +4646,7 @@ def _unconvert_string_array(data, nan_rep=None, encoding=None):
             dtype = "S{0}".format(itemsize)
 
         if isinstance(data[0], compat.binary_type):
-            data = Series(data).str.decode(encoding).values
+            data = Series(data).str.decode(encoding, errors=errors).values
         else:
             data = data.astype(dtype, copy=False).astype(object, copy=False)
 
@@ -4633,22 +4657,23 @@ def _unconvert_string_array(data, nan_rep=None, encoding=None):
     return data.reshape(shape)
 
 
-def _maybe_convert(values, val_kind, encoding):
+def _maybe_convert(values, val_kind, encoding, errors):
     if _need_convert(val_kind):
-        conv = _get_converter(val_kind, encoding)
+        conv = _get_converter(val_kind, encoding, errors)
         # conv = np.frompyfunc(conv, 1, 1)
         values = conv(values)
     return values
 
 
-def _get_converter(kind, encoding):
+def _get_converter(kind, encoding, errors):
     kind = _ensure_decoded(kind)
     if kind == 'datetime64':
         return lambda x: np.asarray(x, dtype='M8[ns]')
     elif kind == 'datetime':
         return lambda x: to_datetime(x, cache=True).to_pydatetime()
     elif kind == 'string':
-        return lambda x: _unconvert_string_array(x, encoding=encoding)
+        return lambda x: _unconvert_string_array(x, encoding=encoding,
+                                                 errors=errors)
     else:  # pragma: no cover
         raise ValueError('invalid kind %s' % kind)
 


Test patch:
diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py
--- a/pandas/tests/io/test_pytables.py
+++ b/pandas/tests/io/test_pytables.py
@@ -1462,6 +1462,18 @@ def test_to_hdf_with_min_itemsize(self):
             tm.assert_series_equal(pd.read_hdf(path, 'ss4'),
                                    pd.concat([df['B'], df2['B']]))
 
+    @pytest.mark.parametrize("format", ['fixed', 'table'])
+    def test_to_hdf_errors(self, format):
+
+        data = ['\ud800foo']
+        ser = pd.Series(data, index=pd.Index(data))
+        with ensure_clean_path(self.path) as path:
+            # GH 20835
+            ser.to_hdf(path, 'table', format=format, errors='surrogatepass')
+
+            result = pd.read_hdf(path, 'table', errors='surrogatepass')
+            tm.assert_series_equal(result, ser)
+
     def test_append_with_data_columns(self):
 
         with ensure_clean_store(self.path) as store: