diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index 579d3a20506f3..0c5def97996c1 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -271,6 +271,7 @@ Other Deprecations - Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`) - Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`) - Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`) +- Deprecated allowing non-integer ``periods`` argument in :func:`date_range`, :func:`timedelta_range`, :func:`period_range`, and :func:`interval_range` (:issue:`56036`) - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`) - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`) - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_dict`. (:issue:`54229`) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 76dccc49d6620..f0b9219682350 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -2454,6 +2454,14 @@ def validate_periods(periods: int | float | None) -> int | None: """ if periods is not None: if lib.is_float(periods): + warnings.warn( + # GH#56036 + "Non-integer 'periods' in pd.date_range, pd.timedelta_range, " + "pd.period_range, and pd.interval_range are deprecated and " + "will raise in a future version.", + FutureWarning, + stacklevel=find_stack_level(), + ) periods = int(periods) elif not lib.is_integer(periods): raise TypeError(f"periods must be a number, got {periods}") diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 0982b376a27d5..4fcdb87974511 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -43,7 +43,6 @@ ) from pandas.core.dtypes.common import ( ensure_platform_int, - is_float, is_float_dtype, is_integer, is_integer_dtype, @@ -60,6 +59,7 @@ from pandas.core.dtypes.missing import is_valid_na_for_dtype from pandas.core.algorithms import unique +from pandas.core.arrays.datetimelike import validate_periods from pandas.core.arrays.interval import ( IntervalArray, _interval_shared_docs, @@ -1076,10 +1076,7 @@ def interval_range( if not _is_valid_endpoint(end): raise ValueError(f"end must be numeric or datetime-like, got {end}") - if is_float(periods): - periods = int(periods) - elif not is_integer(periods) and periods is not None: - raise TypeError(f"periods must be a number, got {periods}") + periods = validate_periods(periods) if freq is not None and not is_number(freq): try: diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 80b5880257483..4ccc9a323a6c1 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -139,9 +139,10 @@ def test_date_range_invalid_periods(self): with pytest.raises(TypeError, match=msg): date_range(start="1/1/2000", periods="foo", freq="D") - def test_date_range_float_periods(self): - # TODO: reconsider allowing this? - rng = date_range("1/1/2000", periods=10.5) + def test_date_range_fractional_period(self): + msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range" + with tm.assert_produces_warning(FutureWarning, match=msg): + rng = date_range("1/1/2000", periods=10.5) exp = date_range("1/1/2000", periods=10) tm.assert_index_equal(rng, exp) diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py index 6c531fb0428a3..37606bda9efca 100644 --- a/pandas/tests/indexes/interval/test_interval_range.py +++ b/pandas/tests/indexes/interval/test_interval_range.py @@ -219,12 +219,15 @@ def test_float_subtype(self, start, end, freq): expected = "int64" if is_integer(start + end) else "float64" assert result == expected - def test_constructor_coverage(self): + def test_interval_range_fractional_period(self): # float value for periods expected = interval_range(start=0, periods=10) - result = interval_range(start=0, periods=10.5) + msg = "Non-integer 'periods' in pd.date_range, .* pd.interval_range" + with tm.assert_produces_warning(FutureWarning, match=msg): + result = interval_range(start=0, periods=10.5) tm.assert_index_equal(result, expected) + def test_constructor_coverage(self): # equivalent timestamp-like start/end start, end = Timestamp("2017-01-01"), Timestamp("2017-01-15") expected = interval_range(start=start, end=end) diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index a1923d29d3d0e..aecd3b3bace9a 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -176,8 +176,10 @@ def test_constructor_invalid_quarters(self): year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC" ) - def test_constructor_corner(self): - result = period_range("2007-01", periods=10.5, freq="M") + def test_period_range_fractional_period(self): + msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range" + with tm.assert_produces_warning(FutureWarning, match=msg): + result = period_range("2007-01", periods=10.5, freq="M") exp = period_range("2007-01", periods=10, freq="M") tm.assert_index_equal(result, exp) diff --git a/pandas/tests/indexes/timedeltas/test_constructors.py b/pandas/tests/indexes/timedeltas/test_constructors.py index ccbd61c4d6693..8c51f6c0a1d25 100644 --- a/pandas/tests/indexes/timedeltas/test_constructors.py +++ b/pandas/tests/indexes/timedeltas/test_constructors.py @@ -174,11 +174,14 @@ def test_constructor_iso(self): result = to_timedelta(durations) tm.assert_index_equal(result, expected) - def test_constructor_coverage(self): - rng = timedelta_range("1 days", periods=10.5) + def test_timedelta_range_fractional_period(self): + msg = "Non-integer 'periods' in pd.date_range, pd.timedelta_range" + with tm.assert_produces_warning(FutureWarning, match=msg): + rng = timedelta_range("1 days", periods=10.5) exp = timedelta_range("1 days", periods=10) tm.assert_index_equal(rng, exp) + def test_constructor_coverage(self): msg = "periods must be a number, got foo" with pytest.raises(TypeError, match=msg): timedelta_range(start="1 days", periods="foo", freq="D")