Repository: pandas-dev/pandas

Issue:
Wrong orientation of operations between DataFrame and range()
#### Code Sample, a copy-pastable example if possible

```python
In [3]: df = pd.DataFrame(np.ones((2,2)), columns=['A', 'B'])

In [4]: df + list(range(2))
Out[4]: 
     A    B
0  1.0  2.0
1  1.0  2.0

In [5]: df + pd.Series(range(2), index=df.columns)
Out[5]: 
     A    B
0  1.0  2.0
1  1.0  2.0

In [6]: df + range(2)
Out[6]: 
     A    B
0  1.0  1.0
1  2.0  2.0

```
#### Problem description

Operations where the operand is a ``range`` operate in the wrong direction compared to all other 1-d objects (I can think of).

#### Expected Output

Same as ``In [4]:`` and ``In [5]:``.

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: 8a69543ab876e11ffa73b31e3acbd89bfafb8ab4
python: 3.5.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.9.0-3-amd64
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: it_IT.UTF-8
LOCALE: it_IT.UTF-8

pandas: 0.21.0rc1+19.g8a69543ab.dirty
pytest: 3.0.6
pip: 9.0.1
setuptools: None
Cython: 0.25.2
numpy: 1.12.1
scipy: 0.19.0
pyarrow: None
xarray: None
IPython: 5.1.0.dev
sphinx: 1.5.6
patsy: 0.4.1
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: 1.2.1
tables: 3.3.0
numexpr: 2.6.1
feather: 0.3.1
matplotlib: 2.0.0
openpyxl: None
xlrd: 1.0.0
xlwt: 1.1.2
xlsxwriter: 0.9.6
lxml: None
bs4: 4.5.3
html5lib: 0.999999999
sqlalchemy: 1.0.15
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: 0.2.1


</details>



Discussion:
A bit obscure, but `collections.deque` has the same behavior as `range` in this case:

```python
In [3]: df + collections.deque([0, 1])
Out[3]:
     A    B
0  1.0  1.0
1  2.0  2.0
```
Yeah, I bet this happens with anything which is (conceptually) 1-d but not recognized as list-like by pandas.
must be a too specific path somewhere, these are both considered list-like (ala ``is_list_like``)
`is_list_like` indeed considers them as list-like (not saying that the actual code path is using `is_list_like`):

```
In [38]: pd.api.types.is_list_like(range(3))
Out[38]: True
```
OK, bet lost :-)

Resolving patch:
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -36,6 +36,7 @@
 from pandas.core.dtypes.cast import maybe_upcast_putmask, find_common_type
 from pandas.core.dtypes.generic import (
     ABCSeries,
+    ABCDataFrame,
     ABCIndex,
     ABCPeriodIndex,
     ABCDateOffset)
@@ -710,7 +711,7 @@ def safe_na_op(lvalues, rvalues):
 
     def wrapper(left, right, name=name, na_op=na_op):
 
-        if isinstance(right, pd.DataFrame):
+        if isinstance(right, ABCDataFrame):
             return NotImplemented
 
         left, right = _align_method_SERIES(left, right)
@@ -834,7 +835,7 @@ def wrapper(self, other, axis=None):
                 raise ValueError(msg)
             return self._constructor(na_op(self.values, other.values),
                                      index=self.index, name=name)
-        elif isinstance(other, pd.DataFrame):  # pragma: no cover
+        elif isinstance(other, ABCDataFrame):  # pragma: no cover
             return NotImplemented
         elif isinstance(other, (np.ndarray, pd.Index)):
             # do not check length of zerodim array
@@ -941,7 +942,7 @@ def wrapper(self, other):
             return filler(self._constructor(na_op(self.values, other.values),
                                             index=self.index, name=name))
 
-        elif isinstance(other, pd.DataFrame):
+        elif isinstance(other, ABCDataFrame):
             return NotImplemented
 
         else:
@@ -1165,10 +1166,7 @@ def to_series(right):
             right = left._constructor_sliced(right, index=left.columns)
         return right
 
-    if isinstance(right, (list, tuple)):
-        right = to_series(right)
-
-    elif isinstance(right, np.ndarray) and right.ndim:  # skips np scalar
+    if isinstance(right, np.ndarray):
 
         if right.ndim == 1:
             right = to_series(right)
@@ -1182,10 +1180,15 @@ def to_series(right):
 
             right = left._constructor(right, index=left.index,
                                       columns=left.columns)
-        else:
+        elif right.ndim > 2:
             raise ValueError('Unable to coerce to Series/DataFrame, dim '
                              'must be <= 2: {dim}'.format(dim=right.shape))
 
+    elif (is_list_like(right) and
+          not isinstance(right, (ABCSeries, ABCDataFrame))):
+        # GH17901
+        right = to_series(right)
+
     return right
 
 
@@ -1252,7 +1255,7 @@ def f(self, other, axis=default_axis, level=None, fill_value=None):
 
         other = _align_method_FRAME(self, other, axis)
 
-        if isinstance(other, pd.DataFrame):  # Another DataFrame
+        if isinstance(other, ABCDataFrame):  # Another DataFrame
             return self._combine_frame(other, na_op, fill_value, level)
         elif isinstance(other, ABCSeries):
             return self._combine_series(other, na_op, fill_value, axis, level)
@@ -1300,7 +1303,7 @@ def f(self, other, axis=default_axis, level=None):
 
         other = _align_method_FRAME(self, other, axis)
 
-        if isinstance(other, pd.DataFrame):  # Another DataFrame
+        if isinstance(other, ABCDataFrame):  # Another DataFrame
             return self._flex_compare_frame(other, na_op, str_rep, level,
                                             try_cast=False)
 
@@ -1318,7 +1321,7 @@ def f(self, other, axis=default_axis, level=None):
 def _comp_method_FRAME(func, name, str_rep, masker=False):
     @Appender('Wrapper for comparison method {name}'.format(name=name))
     def f(self, other):
-        if isinstance(other, pd.DataFrame):  # Another DataFrame
+        if isinstance(other, ABCDataFrame):  # Another DataFrame
             return self._compare_frame(other, func, str_rep)
         elif isinstance(other, ABCSeries):
             return self._combine_series_infer(other, func, try_cast=False)
@@ -1411,7 +1414,7 @@ def f(self, other, axis=None):
 
         if isinstance(other, self._constructor):
             return self._compare_constructor(other, na_op, try_cast=False)
-        elif isinstance(other, (self._constructor_sliced, pd.DataFrame,
+        elif isinstance(other, (self._constructor_sliced, ABCDataFrame,
                                 ABCSeries)):
             raise Exception("input needs alignment for this object [{object}]"
                             .format(object=self._constructor))


Test patch:
diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py
--- a/pandas/tests/frame/test_operators.py
+++ b/pandas/tests/frame/test_operators.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from __future__ import print_function
-
+from collections import deque
 from datetime import datetime
 import operator
 
@@ -10,7 +10,7 @@
 from numpy import nan, random
 import numpy as np
 
-from pandas.compat import lrange
+from pandas.compat import lrange, range
 from pandas import compat
 from pandas import (DataFrame, Series, MultiIndex, Timestamp,
                     date_range)
@@ -749,6 +749,15 @@ def test_arith_non_pandas_object(self):
         added = DataFrame(df.values + val3, index=df.index, columns=df.columns)
         assert_frame_equal(df.add(val3), added)
 
+    @pytest.mark.parametrize('values', [[1, 2], (1, 2), np.array([1, 2]),
+                                        range(1, 3), deque([1, 2])])
+    def test_arith_alignment_non_pandas_object(self, values):
+        # GH 17901
+        df = DataFrame({'A': [1, 1], 'B': [1, 1]})
+        expected = DataFrame({'A': [2, 2], 'B': [3, 3]})
+        result = df + values
+        assert_frame_equal(result, expected)
+
     def test_combineFrame(self):
         frame_copy = self.frame.reindex(self.frame.index[::2])
 
@@ -1200,8 +1209,8 @@ def test_alignment_non_pandas(self):
         df = pd.DataFrame(np.random.randn(3, 3), index=index, columns=columns)
 
         align = pd.core.ops._align_method_FRAME
-
-        for val in [[1, 2, 3], (1, 2, 3), np.array([1, 2, 3], dtype=np.int64)]:
+        for val in [[1, 2, 3], (1, 2, 3), np.array([1, 2, 3], dtype=np.int64),
+                    range(1, 4)]:
 
             tm.assert_series_equal(align(df, val, 'index'),
                                    Series([1, 2, 3], index=df.index))
@@ -1210,7 +1219,8 @@ def test_alignment_non_pandas(self):
 
         # length mismatch
         msg = 'Unable to coerce to Series, length must be 3: given 2'
-        for val in [[1, 2], (1, 2), np.array([1, 2])]:
+        for val in [[1, 2], (1, 2), np.array([1, 2]), range(1, 3)]:
+
             with tm.assert_raises_regex(ValueError, msg):
                 align(df, val, 'index')