Skip to content

Commit

Permalink
BUG/API: Accessors like .cat raise AttributeError when invalid
Browse files Browse the repository at this point in the history
`AttributeError` is really the appropriate error to raise for an invalid
attribute. In particular, it is necessary to ensure that tests like
`hasattr(s, 'cat')` work consistently on Python 2 and 3: on Python 2,
`hasattr(s, 'cat')` will return `False` even if a `TypeError` was raised, but
Python 3 more strictly requires `AttributeError`.

This is an unfortunate trap that we should avoid. See this discussion in
Seaborn for a full report:
mwaskom/seaborn#361 (comment)

Note that technically, this is an API change, since these accessors (all but
`.str`, I think) raised TypeError in the last release.

This also suggests another possibility for testing for Series with a
Categorical dtype (GH8814): just use `hasattr(s, 'cat')` (at least for Python
2 or pandas >=0.16).

CC mwaskom jorisvandenbossche JanSchulz
  • Loading branch information
shoyer committed Mar 9, 2015
1 parent d876a9f commit 0b02747
Show file tree
Hide file tree
Showing 6 changed files with 28 additions and 19 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.16.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ Backwards incompatible API changes

- Bar and horizontal bar plots no longer add a dashed line along the info axis. The prior style can be achieved with matplotlib's ``axhline`` or ``axvline`` methods (:issue:`9088`).

- ``Series`` accessors ``.dt``, ``.cat`` and ``.str`` now raise ``AttributeError`` instead of ``TypeError`` if the series does not contain the appropriate type of data (:issue:`9617`). This follows Python's built-in exception hierarchy more closely and ensures that tests like ``hasattr(s, 'cat')`` are consistent on both Python 2 and 3.

- ``Series`` now supports bitwise operation for integral types (:issue:`9016`)

Expand Down
5 changes: 3 additions & 2 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ class NDFrame(PandasObject):
copy : boolean, default False
"""
_internal_names = ['_data', '_cacher', '_item_cache', '_cache',
'is_copy', 'str', '_subtyp', '_index', '_default_kind',
'_default_fill_value','__array_struct__','__array_interface__']
'is_copy', 'dt', 'cat', 'str', '_subtyp', '_index',
'_default_kind', '_default_fill_value',
'__array_struct__','__array_interface__']
_internal_names_set = set(_internal_names)
_metadata = []
is_copy = None
Expand Down
13 changes: 8 additions & 5 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2521,8 +2521,9 @@ def _make_str_accessor(self):
# this really should exclude all series with any non-string values,
# but that isn't practical for performance reasons until we have a
# str dtype (GH 9343)
raise TypeError("Can only use .str accessor with string values, "
"which use np.object_ dtype in pandas")
raise AttributeError("Can only use .str accessor with string "
"values, which use np.object_ dtype in "
"pandas")
return StringMethods(self)

str = base.AccessorProperty(StringMethods, _make_str_accessor)
Expand All @@ -2533,8 +2534,9 @@ def _make_str_accessor(self):
def _make_dt_accessor(self):
try:
return maybe_to_datetimelike(self)
except (Exception):
raise TypeError("Can only use .dt accessor with datetimelike values")
except Exception:
raise AttributeError("Can only use .dt accessor with datetimelike "
"values")

dt = base.AccessorProperty(CombinedDatetimelikeProperties, _make_dt_accessor)

Expand All @@ -2543,7 +2545,8 @@ def _make_dt_accessor(self):

def _make_cat_accessor(self):
if not com.is_categorical_dtype(self.dtype):
raise TypeError("Can only use .cat accessor with a 'category' dtype")
raise AttributeError("Can only use .cat accessor with a "
"'category' dtype")
return CategoricalAccessor(self.values, self.index)

cat = base.AccessorProperty(CategoricalAccessor, _make_cat_accessor)
Expand Down
7 changes: 5 additions & 2 deletions pandas/tests/test_categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -2631,8 +2631,11 @@ def test_cat_accessor_api(self):
self.assertIs(Series.cat, CategoricalAccessor)
s = Series(list('aabbcde')).astype('category')
self.assertIsInstance(s.cat, CategoricalAccessor)
with tm.assertRaisesRegexp(TypeError, "only use .cat accessor"):
Series([1]).cat

invalid = Series([1])
with tm.assertRaisesRegexp(AttributeError, "only use .cat accessor"):
invalid.cat
self.assertFalse(hasattr(invalid, 'cat'))

def test_pickle_v0_14_1(self):
cat = pd.Categorical(values=['a', 'b', 'c'],
Expand Down
15 changes: 7 additions & 8 deletions pandas/tests/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,6 @@ def compare(s, name):
else:
tm.assert_series_equal(a,b)

# invalids
for s in [Series(np.arange(5)),
Series(list('abcde')),
Series(np.random.randn(5))]:
self.assertRaises(TypeError, lambda : s.dt)

# datetimeindex
for s in [Series(date_range('20130101',periods=5)),
Series(date_range('20130101',periods=5,freq='s')),
Expand Down Expand Up @@ -240,8 +234,13 @@ def test_dt_accessor_api(self):
s = Series(date_range('2000-01-01', periods=3))
self.assertIsInstance(s.dt, DatetimeProperties)

with tm.assertRaisesRegexp(TypeError, "only use .dt accessor"):
Series([1]).dt
for s in [Series(np.arange(5)),
Series(list('abcde')),
Series(np.random.randn(5))]:
with tm.assertRaisesRegexp(AttributeError,
"only use .dt accessor"):
s.dt
self.assertFalse(hasattr(s, 'dt'))

def test_binop_maybe_preserve_name(self):

Expand Down
6 changes: 4 additions & 2 deletions pandas/tests/test_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ def test_api(self):
self.assertIsInstance(Series(['']).str, strings.StringMethods)

# GH 9184
with tm.assertRaisesRegexp(TypeError, "only use .str accessor"):
Series([1]).str
invalid = Series([1])
with tm.assertRaisesRegexp(AttributeError, "only use .str accessor"):
invalid.str
self.assertFalse(hasattr(invalid, 'str'))

def test_iter(self):
# GH3638
Expand Down

0 comments on commit 0b02747

Please sign in to comment.