Repository: pandas-dev/pandas

Issue:
ENH: Support rename on MultiIndex
Change up `MultiIndex.rename` to be an alias for `MultiIndex.set_names`.
- `Index.rename` : takes single object 
- `MultiIndex.set_names`, `Index.set_names` - same type signature: take list of names [Index.set_names can only take iterable of length 1)
- `MultiIndex.rename` - synonym for `set_names`

So if you want to do something generically, you should use `set_names` (just like you would with other attributes). If you expect a non-MI index, then you could use `rename` with a string, etc.

cc @jreback 

I personally think that this behavior is how it should work  for Index. Everyone okay with this?  I'd prefer not to do type sniffing if at all possible.

``` python
ind = Index(range(10))
ind.rename(["apple"], inplace=True)
print(ind.name) # ['apple']
print(ind.names) # [['apple']]
ind.set_names('apple') # Fails with error that it's too long
```



Discussion:
`ind.set_name('a')` would work (but is wrong), maybe just disallow its `not com.is_list_like`


Resolving patch:
diff --git a/pandas/core/base.py b/pandas/core/base.py
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -114,7 +114,7 @@ def __hash__(self):
     def _disabled(self, *args, **kwargs):
         """This method will not function because object is immutable."""
         raise TypeError("'%s' does not support mutable operations." %
-                        self.__class__)
+                        self.__class__.__name__)
 
     def __unicode__(self):
         from pandas.core.common import pprint_thing
diff --git a/pandas/core/common.py b/pandas/core/common.py
--- a/pandas/core/common.py
+++ b/pandas/core/common.py
@@ -1783,7 +1783,7 @@ def is_re_compilable(obj):
 
 
 def is_list_like(arg):
-    return hasattr(arg, '__iter__') and not isinstance(arg, compat.string_types)
+    return hasattr(arg, '__iter__') and not isinstance(arg, compat.string_and_binary_types)
 
 
 def _is_sequence(x):
diff --git a/pandas/core/index.py b/pandas/core/index.py
--- a/pandas/core/index.py
+++ b/pandas/core/index.py
@@ -273,16 +273,33 @@ def set_names(self, names, inplace=False):
 
         Returns
         -------
-        new index (of same type and class...etc)
+        new index (of same type and class...etc) [if inplace, returns None]
         """
+        if not com.is_list_like(names):
+            raise TypeError("Must pass list-like as `names`.")
         if inplace:
             idx = self
         else:
             idx = self._shallow_copy()
         idx._set_names(names)
-        return idx
+        if not inplace:
+            return idx
 
     def rename(self, name, inplace=False):
+        """
+        Set new names on index. Defaults to returning new index.
+
+        Parameters
+        ----------
+        name : str or list
+            name to set
+        inplace : bool
+            if True, mutates in place
+
+        Returns
+        -------
+        new index (of same type and class...etc) [if inplace, returns None]
+        """
         return self.set_names([name], inplace=inplace)
 
     @property
@@ -1556,6 +1573,7 @@ class MultiIndex(Index):
     _levels = FrozenList()
     _labels = FrozenList()
     _comparables = ['names']
+    rename = Index.set_names
 
     def __new__(cls, levels=None, labels=None, sortorder=None, names=None,
                 copy=False):


Test patch:
diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py
--- a/pandas/tests/test_index.py
+++ b/pandas/tests/test_index.py
@@ -4,6 +4,7 @@
 from pandas.compat import range, lrange, lzip, u, zip
 import operator
 import pickle
+import re
 import unittest
 import nose
 import os
@@ -44,10 +45,35 @@ def test_wrong_number_names(self):
         def testit(ind):
             ind.names = ["apple", "banana", "carrot"]
 
-        indices = (self.dateIndex, self.unicodeIndex, self.strIndex, self.intIndex, self.floatIndex, self.empty, self.tuples)
+        indices = (self.dateIndex, self.unicodeIndex, self.strIndex,
+                   self.intIndex, self.floatIndex, self.empty, self.tuples)
         for ind in indices:
             assertRaisesRegexp(ValueError, "^Length", testit, ind)
 
+    def test_set_name_methods(self):
+        new_name = "This is the new name for this index"
+        indices = (self.dateIndex, self.intIndex, self.unicodeIndex,
+                   self.empty)
+        for ind in indices:
+            original_name = ind.name
+            new_ind = ind.set_names([new_name])
+            self.assertEqual(new_ind.name, new_name)
+            self.assertEqual(ind.name, original_name)
+            res = ind.rename(new_name, inplace=True)
+            # should return None
+            self.assert_(res is None)
+            self.assertEqual(ind.name, new_name)
+            self.assertEqual(ind.names, [new_name])
+            with assertRaisesRegexp(TypeError, "list-like"):
+                # should still fail even if it would be the right length
+                ind.set_names("a")
+        # rename in place just leaves tuples and other containers alone
+        name = ('A', 'B')
+        ind = self.intIndex
+        ind.rename(name, inplace=True)
+        self.assertEqual(ind.name, name)
+        self.assertEqual(ind.names, [name])
+
     def test_hash_error(self):
         self.assertRaises(TypeError, hash, self.strIndex)
 
@@ -1018,6 +1044,48 @@ def setUp(self):
                                 labels=[major_labels, minor_labels],
                                 names=self.index_names)
 
+    def test_set_names_and_rename(self):
+        # so long as these are synonyms, we don't need to test set_names
+        self.assert_(self.index.rename == self.index.set_names)
+        new_names = [name + "SUFFIX" for name in self.index_names]
+        ind = self.index.set_names(new_names)
+        self.assertEqual(self.index.names, self.index_names)
+        self.assertEqual(ind.names, new_names)
+        with assertRaisesRegexp(ValueError, "^Length"):
+            ind.set_names(new_names + new_names)
+        new_names2 = [name + "SUFFIX2" for name in new_names]
+        res = ind.set_names(new_names2, inplace=True)
+        self.assert_(res is None)
+        self.assertEqual(ind.names, new_names2)
+
+    def test_set_levels_and_set_labels(self):
+        # side note - you probably wouldn't want to use levels and labels
+        # directly like this - but it is possible.
+        levels, labels = self.index.levels, self.index.labels
+        new_levels = [[lev + 'a' for lev in level] for level in levels]
+        major_labels, minor_labels = labels
+        major_labels = [(x + 1) % 3 for x in major_labels]
+        minor_labels = [(x + 1) % 1 for x in minor_labels]
+        new_labels = [major_labels, minor_labels]
+
+    def test_metadata_immutable(self):
+        levels, labels = self.index.levels, self.index.labels
+        # shouldn't be able to set at either the top level or base level
+        mutable_regex = re.compile('does not support mutable operations')
+        with assertRaisesRegexp(TypeError, mutable_regex):
+            levels[0] = levels[0]
+        with assertRaisesRegexp(TypeError, mutable_regex):
+            levels[0][0] = levels[0][0]
+        # ditto for labels
+        with assertRaisesRegexp(TypeError, mutable_regex):
+            labels[0] = labels[0]
+        with assertRaisesRegexp(TypeError, mutable_regex):
+            labels[0][0] = labels[0][0]
+        # and for names
+        names = self.index.names
+        with assertRaisesRegexp(TypeError, mutable_regex):
+            names[0] = names[0]
+
     def test_copy_in_constructor(self):
         levels = np.array(["a", "b", "c"])
         labels = np.array([1, 1, 2, 0, 0, 1, 1])