Repository: pandas-dev/pandas

Issue:
BUG: Wrong dtype using `range` in DataFrame constructor on Windows
When using `range` (python 3.5) in the `DataFrame` constructor I get different dtypes depending on the system I'm running on:

```python
>>> import pandas as pd
>>> from collections import OrderedDict
>>> data = OrderedDict([
...    ('a', range(5)),
...    ('b', [-10, -5, 0, 5, 10])
... ])
>>> df = pd.DataFrame(data)
>>> df.info()
```
#### Problem description
On Unix:
```
RangeIndex: 5 entries, 0 to 4
Data columns (total 2 columns):
a    5 non-null int64
b    5 non-null int64
dtypes: int64(2)
```
On Windows:
```
RangeIndex: 5 entries, 0 to 4
Data columns (total 2 columns):
a    5 non-null int32
b    5 non-null int64
dtypes: int32(1), int64(1)
```

The problem appeared in an arrow PR unittest
https://github.com/apache/arrow/pull/790
Windows tests on appveyor:
https://ci.appveyor.com/project/ApacheSoftwareFoundation/arrow/build/1.0.2169
Unix tests on travis:
https://travis-ci.org/apache/arrow/builds/248514835

#### Expected Output

All systems create the same dtypes. In this case int64

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.3.final.0
python-bits: 64
OS: Windows
OS-release: 2012ServerR2
machine: AMD64
processor: Intel64 Family 6 Model 63 Stepping 2, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None
pandas: 0.20.2
pytest: 3.1.2
pip: 9.0.1
setuptools: 27.2.0
Cython: 0.25.2
numpy: 1.13.0
scipy: None
xarray: None
IPython: None
sphinx: None
patsy: None
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: None
s3fs: None
pandas_gbq: None
pandas_datareader: None
</details>



Discussion:
I believe we're following the behavior of numpy here, where the size of `int` depends on the platform. From https://docs.scipy.org/doc/numpy-1.10.1/user/basics.types.html

> Some types, such as int and intp, have differing bitsizes, dependent on the platforms (e.g. 32-bit vs. 64-bit machines). This should be taken into account when interfacing with low-level code (such as C or Fortran) where the raw memory is addressed.


Though that not really a good explanation to the user for why the list would be int64, while the `range` is int32.
This is annoying, but agree with @TomAugspurger it's correct.  Ultimately the range object is passed on to numpy which expands using the `int` dtype.

```
In [14]: np.array(range(100))
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
       34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
       51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
       68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
       85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99])
```


I suppose we could intercept it and force `int64` like we do with lists.
we structure our tests to not use range at all for this reason

however it is possible to change this by explicitly intercepting a range object in the Series constructor and introspecting the indices (we do this for RangeIndex already)

so we could mark this if someone wants to do it
it's not a bug rather a numpy work around
Yeah, I actually think this would be a good idea.  Not a big deal, but also perf to be picked up, as apparently numpy expands the list.

```python
In [24]: r = range(1000000)

In [25]: %timeit np.array(r)
139 ms ± 755 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [27]: %timeit np.arange(r.start, r.stop, r.step)
1.36 ms ± 8.26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```


Resolving patch:
diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py
--- a/pandas/compat/__init__.py
+++ b/pandas/compat/__init__.py
@@ -100,6 +100,10 @@ def signature(f):
                                            'varargs', 'keywords'])
         return argspec(args, defaults, varargs, keywords)
 
+    def get_range_parameters(data):
+        """Gets the start, stop, and step parameters from a range object"""
+        return data.start, data.stop, data.step
+
     # have to explicitly put builtins into the namespace
     range = range
     map = map
@@ -146,6 +150,24 @@ def bytes_to_str(b, encoding='ascii'):
     def signature(f):
         return inspect.getargspec(f)
 
+    def get_range_parameters(data):
+        """Gets the start, stop, and step parameters from a range object"""
+        # seems we only have indexing ops to infer
+        # rather than direct accessors
+        if len(data) > 1:
+            step = data[1] - data[0]
+            stop = data[-1] + step
+            start = data[0]
+        elif len(data):
+            start = data[0]
+            stop = data[0] + 1
+            step = 1
+        else:
+            start = stop = 0
+            step = 1
+
+        return start, stop, step
+
     # import iterator versions of these functions
     range = xrange
     intern = intern
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -10,7 +10,7 @@
     is_int64_dtype)
 
 from pandas import compat
-from pandas.compat import lrange, range
+from pandas.compat import lrange, range, get_range_parameters
 from pandas.compat.numpy import function as nv
 from pandas.core.common import _all_none
 from pandas.core.indexes.base import Index, _index_shared_docs
@@ -113,24 +113,7 @@ def from_range(cls, data, name=None, dtype=None, **kwargs):
                 '{0}(...) must be called with object coercible to a '
                 'range, {1} was passed'.format(cls.__name__, repr(data)))
 
-        if compat.PY3:
-            step = data.step
-            stop = data.stop
-            start = data.start
-        else:
-            # seems we only have indexing ops to infer
-            # rather than direct accessors
-            if len(data) > 1:
-                step = data[1] - data[0]
-                stop = data[-1] + step
-                start = data[0]
-            elif len(data):
-                start = data[0]
-                stop = data[0] + 1
-                step = 1
-            else:
-                start = stop = 0
-                step = 1
+        start, stop, step = get_range_parameters(data)
         return RangeIndex(start, stop, step, dtype=dtype, name=name, **kwargs)
 
     @classmethod
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -60,7 +60,8 @@
 from pandas.core.indexes.period import PeriodIndex
 from pandas import compat
 from pandas.io.formats.terminal import get_terminal_size
-from pandas.compat import zip, u, OrderedDict, StringIO
+from pandas.compat import (
+    zip, u, OrderedDict, StringIO, range, get_range_parameters)
 from pandas.compat.numpy import function as nv
 
 from pandas.core import accessor
@@ -3177,6 +3178,11 @@ def _try_cast(arr, take_fast_path):
 
         subarr = maybe_cast_to_datetime(subarr, dtype)
 
+    elif isinstance(data, range):
+        # GH 16804
+        start, stop, step = get_range_parameters(data)
+        arr = np.arange(start, stop, step, dtype='int64')
+        subarr = _try_cast(arr, False)
     else:
         subarr = _try_cast(data, False)
 


Test patch:
diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py
--- a/pandas/tests/frame/test_constructors.py
+++ b/pandas/tests/frame/test_constructors.py
@@ -829,7 +829,7 @@ def test_constructor_list_of_lists(self):
 
         # GH 4851
         # list of 0-dim ndarrays
-        expected = DataFrame({0: range(10)})
+        expected = DataFrame({0: np.arange(10)})
         data = [np.array(x) for x in range(10)]
         result = DataFrame(data)
         tm.assert_frame_equal(result, expected)
@@ -1927,6 +1927,13 @@ def test_to_frame_with_falsey_names(self):
         result = DataFrame(Series(name=0)).dtypes
         tm.assert_series_equal(result, expected)
 
+    @pytest.mark.parametrize('dtype', [None, 'uint8', 'category'])
+    def test_constructor_range_dtype(self, dtype):
+        # GH 16804
+        expected = DataFrame({'A': [0, 1, 2, 3, 4]}, dtype=dtype or 'int64')
+        result = DataFrame({'A': range(5)}, dtype=dtype)
+        tm.assert_frame_equal(result, expected)
+
 
 class TestDataFrameConstructorWithDatetimeTZ(TestData):
 
diff --git a/pandas/tests/indexing/test_partial.py b/pandas/tests/indexing/test_partial.py
--- a/pandas/tests/indexing/test_partial.py
+++ b/pandas/tests/indexing/test_partial.py
@@ -532,7 +532,7 @@ def f():
 
         def f():
             df = DataFrame()
-            df['foo'] = Series(range(len(df)))
+            df['foo'] = Series(np.arange(len(df)), dtype='float64')
             return df
 
         tm.assert_frame_equal(f(), expected)
diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py
--- a/pandas/tests/series/test_alter_axes.py
+++ b/pandas/tests/series/test_alter_axes.py
@@ -219,7 +219,7 @@ def test_reorder_levels(self):
                            labels=[[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
                                    [0, 0, 0, 0, 0, 0]],
                            names=['L0', 'L0', 'L0'])
-        expected = Series(range(6), index=e_idx)
+        expected = Series(np.arange(6), index=e_idx)
         assert_series_equal(result, expected)
 
         result = s.reorder_levels(['L0', 'L0', 'L0'])
diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py
--- a/pandas/tests/series/test_constructors.py
+++ b/pandas/tests/series/test_constructors.py
@@ -846,3 +846,10 @@ def test_constructor_generic_timestamp_deprecated(self):
         msg = "cannot convert datetimelike"
         with tm.assert_raises_regex(TypeError, msg):
             Series([], dtype='M8[ps]')
+
+    @pytest.mark.parametrize('dtype', [None, 'uint8', 'category'])
+    def test_constructor_range_dtype(self, dtype):
+        # GH 16804
+        expected = Series([0, 1, 2, 3, 4], dtype=dtype or 'int64')
+        result = Series(range(5), dtype=dtype)
+        tm.assert_series_equal(result, expected)
diff --git a/pandas/tests/test_compat.py b/pandas/tests/test_compat.py
--- a/pandas/tests/test_compat.py
+++ b/pandas/tests/test_compat.py
@@ -3,9 +3,10 @@
 Testing that functions from compat work as expected
 """
 
+import pytest
 from pandas.compat import (range, zip, map, filter, lrange, lzip, lmap,
                            lfilter, builtins, iterkeys, itervalues, iteritems,
-                           next)
+                           next, get_range_parameters, PY2)
 
 
 class TestBuiltinIterators(object):
@@ -69,3 +70,22 @@ def test_dict_iterators(self):
         assert next(itervalues({1: 2})) == 2
         assert next(iterkeys({1: 2})) == 1
         assert next(iteritems({1: 2})) == (1, 2)
+
+
+class TestCompatFunctions(object):
+
+    @pytest.mark.parametrize(
+        'start,stop,step', [(0, 10, 2), (11, -2, -1), (0, -5, 1), (2, 4, 8)])
+    def test_get_range_parameters(self, start, stop, step):
+        rng = range(start, stop, step)
+        if PY2 and len(rng) == 0:
+            start_expected, stop_expected, step_expected = 0, 0, 1
+        elif PY2 and len(rng) == 1:
+            start_expected, stop_expected, step_expected = start, start + 1, 1
+        else:
+            start_expected, stop_expected, step_expected = start, stop, step
+
+        start_result, stop_result, step_result = get_range_parameters(rng)
+        assert start_result == start_expected
+        assert stop_result == stop_expected
+        assert step_result == step_expected
diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py
--- a/pandas/tests/test_resample.py
+++ b/pandas/tests/test_resample.py
@@ -3033,7 +3033,7 @@ def test_nearest(self):
         result = pd.Series(range(3), index=index).resample('20s').nearest()
 
         expected = pd.Series(
-            np.array([0, 0, 1, 1, 1, 2, 2]),
+            [0, 0, 1, 1, 1, 2, 2],
             index=pd.DatetimeIndex(
                 ['2000-01-01 00:00:00', '2000-01-01 00:00:20',
                  '2000-01-01 00:00:40', '2000-01-01 00:01:00',
diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py
--- a/pandas/tests/test_window.py
+++ b/pandas/tests/test_window.py
@@ -698,8 +698,8 @@ def get_expects(self):
         return expects
 
     def _create_dtype_data(self, dtype):
-        sr1 = Series(range(5), dtype=dtype)
-        sr2 = Series(range(10, 0, -2), dtype=dtype)
+        sr1 = Series(np.arange(5), dtype=dtype)
+        sr2 = Series(np.arange(10, 0, -2), dtype=dtype)
         df = DataFrame(np.arange(10).reshape((5, 2)), dtype=dtype)
 
         data = {