Repository: pandas-dev/pandas

Issue:
Drop Python 3.5 support
As discussed in https://github.com/pandas-dev/pandas/issues/27492.

The 0.25.x branch should be stable for the next release, so we can start on this at any time.


Discussion:
I'd be positive to deprecate python 3.5. It's old and 3.6 brings a lot of changes, so make sense to not support 3.5 in a long-term stable version.

An idea could be to make a dual release 0.26 and 1.0. They would have identical code bases, but 0.26 would support py3.5 and 1.0 would support py3.6, so it would only be a minor chang in setup.py.

Why do you think we need a deprecation instead of just dropping it?
Sorry, I miswrote that, i meant drop.
Gotcha, thanks.

FTR, https://numpy.org/neps/nep-0029-deprecation_policy.html has September 2019 as the cutoff for 3.5.
@pandas-dev/pandas-core do we have thoughts on this? The only thing I can think of that warrants a 0.25.3 release right now might be the fix in #29173

If there isn't an appetite for that release I think we should drop 35 from CI now
+1 in dripping 3.5 for 1.0

this wouldn’t affect 0.25.x
I’m talking specifically about dropping from CI. I’m leaning towards maybe cutting a 25.3 for the regression fix and dropping 35 after that
whether we cut 0.25.3 is irrelevant
we have no reason to change the 0.25.x CI
Ah OK. So OK to drop from CI on master and if we want a 0.25.3 can target the 0.25.x branch directly?
right as usual would merge a patch to master and back port to 0.25.x
+1 for dropping from master.

On Thu, Oct 24, 2019 at 12:57 PM Jeff Reback <notifications@github.com>
wrote:

> right as usual would merge a patch to master and back port to 0.25.x
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/29034?email_source=notifications&email_token=AAKAOIRSQCH5QHYVW7R2RCTQQHO2HA5CNFSM4JBOAZ42YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECF4VZI#issuecomment-546032357>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAKAOISCNGAIQ5334Z3NR4DQQHO2HANCNFSM4JBOAZ4Q>
> .
>

+1 from me as well for `master`

Resolving patch:
diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py
--- a/pandas/compat/__init__.py
+++ b/pandas/compat/__init__.py
@@ -12,7 +12,6 @@
 import sys
 import warnings
 
-PY35 = sys.version_info[:2] == (3, 5)
 PY36 = sys.version_info >= (3, 6)
 PY37 = sys.version_info >= (3, 7)
 PY38 = sys.version_info >= (3, 8)
diff --git a/pandas/io/json/_json.py b/pandas/io/json/_json.py
--- a/pandas/io/json/_json.py
+++ b/pandas/io/json/_json.py
@@ -12,7 +12,7 @@
 
 from pandas.core.dtypes.common import ensure_str, is_period_dtype
 
-from pandas import DataFrame, MultiIndex, Series, compat, isna, to_datetime
+from pandas import DataFrame, MultiIndex, Series, isna, to_datetime
 from pandas._typing import JSONSerializable
 from pandas.core.reshape.concat import concat
 
@@ -1115,8 +1115,6 @@ def _parse_no_numpy(self):
                 dtype=None,
                 orient="index",
             )
-            if compat.PY35:
-                self.obj = self.obj.sort_index(axis="columns").sort_index(axis="index")
         elif orient == "table":
             self.obj = parse_table_schema(json, precise_float=self.precise_float)
         else:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -223,7 +223,6 @@ def build_extensions(self):
     "Intended Audience :: Science/Research",
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.5",
     "Programming Language :: Python :: 3.6",
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
@@ -812,7 +811,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
     long_description=LONG_DESCRIPTION,
     classifiers=CLASSIFIERS,
     platforms="any",
-    python_requires=">=3.5.3",
+    python_requires=">=3.6.1",
     extras_require={
         "test": [
             # sync with setup.cfg minversion & install.rst


Test patch:
diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py
--- a/pandas/tests/groupby/aggregate/test_aggregate.py
+++ b/pandas/tests/groupby/aggregate/test_aggregate.py
@@ -598,10 +598,7 @@ def test_agg_with_one_lambda(self):
             }
         )
 
-        # sort for 35 and earlier
         columns = ["height_sqr_min", "height_max", "weight_max"]
-        if compat.PY35:
-            columns = ["height_max", "height_sqr_min", "weight_max"]
         expected = pd.DataFrame(
             {
                 "height_sqr_min": [82.81, 36.00],
@@ -640,7 +637,6 @@ def test_agg_multiple_lambda(self):
                 "weight": [7.9, 7.5, 9.9, 198.0],
             }
         )
-        # sort for 35 and earlier
         columns = [
             "height_sqr_min",
             "height_max",
@@ -648,14 +644,6 @@ def test_agg_multiple_lambda(self):
             "height_max_2",
             "weight_min",
         ]
-        if compat.PY35:
-            columns = [
-                "height_max",
-                "height_max_2",
-                "height_sqr_min",
-                "weight_max",
-                "weight_min",
-            ]
         expected = pd.DataFrame(
             {
                 "height_sqr_min": [82.81, 36.00],
diff --git a/pandas/tests/io/json/test_json_table_schema.py b/pandas/tests/io/json/test_json_table_schema.py
--- a/pandas/tests/io/json/test_json_table_schema.py
+++ b/pandas/tests/io/json/test_json_table_schema.py
@@ -5,8 +5,6 @@
 import numpy as np
 import pytest
 
-from pandas.compat import PY35
-
 from pandas.core.dtypes.dtypes import CategoricalDtype, DatetimeTZDtype, PeriodDtype
 
 import pandas as pd
@@ -22,14 +20,6 @@
 )
 
 
-def assert_results_equal(result, expected):
-    """Helper function for comparing deserialized JSON with Py35 compat."""
-    if PY35:
-        assert sorted(result.items()) == sorted(expected.items())
-    else:
-        assert result == expected
-
-
 class TestBuildSchema:
     def setup_method(self, method):
         self.df = DataFrame(
@@ -245,7 +235,7 @@ def test_build_series(self):
             ]
         )
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     def test_to_json(self):
         df = self.df.copy()
@@ -335,7 +325,7 @@ def test_to_json(self):
         ]
         expected = OrderedDict([("schema", schema), ("data", data)])
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     def test_to_json_float_index(self):
         data = pd.Series(1, index=[1.0, 2.0])
@@ -365,7 +355,7 @@ def test_to_json_float_index(self):
             ]
         )
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     def test_to_json_period_index(self):
         idx = pd.period_range("2016", freq="Q-JAN", periods=2)
@@ -386,7 +376,7 @@ def test_to_json_period_index(self):
         ]
         expected = OrderedDict([("schema", schema), ("data", data)])
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     def test_to_json_categorical_index(self):
         data = pd.Series(1, pd.CategoricalIndex(["a", "b"]))
@@ -421,7 +411,7 @@ def test_to_json_categorical_index(self):
             ]
         )
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     def test_date_format_raises(self):
         with pytest.raises(ValueError):
@@ -558,7 +548,7 @@ def test_categorical(self):
             ]
         )
 
-        assert_results_equal(result, expected)
+        assert result == expected
 
     @pytest.mark.parametrize(
         "idx,nm,prop",
diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py
--- a/pandas/tests/io/json/test_pandas.py
+++ b/pandas/tests/io/json/test_pandas.py
@@ -7,7 +7,7 @@
 import numpy as np
 import pytest
 
-from pandas.compat import PY35, is_platform_32bit, is_platform_windows
+from pandas.compat import is_platform_32bit, is_platform_windows
 import pandas.util._test_decorators as td
 
 import pandas as pd
@@ -160,9 +160,6 @@ def test_roundtrip_simple(self, orient, convert_axes, numpy, dtype):
 
         expected = self.frame.copy()
 
-        if not numpy and PY35 and orient in ("index", "columns"):
-            expected = expected.sort_index()
-
         assert_json_roundtrip_equal(result, expected, orient)
 
     @pytest.mark.parametrize("dtype", [False, np.int64])
@@ -174,9 +171,6 @@ def test_roundtrip_intframe(self, orient, convert_axes, numpy, dtype):
             data, orient=orient, convert_axes=convert_axes, numpy=numpy, dtype=dtype
         )
         expected = self.intframe.copy()
-        if not numpy and PY35 and orient in ("index", "columns"):
-            expected = expected.sort_index()
-
         if (
             numpy
             and (is_platform_32bit() or is_platform_windows())
@@ -209,9 +203,6 @@ def test_roundtrip_str_axes(self, orient, convert_axes, numpy, dtype):
         )
 
         expected = df.copy()
-        if not numpy and PY35 and orient in ("index", "columns"):
-            expected = expected.sort_index()
-
         if not dtype:
             expected = expected.astype(np.int64)
 
@@ -250,7 +241,7 @@ def test_roundtrip_categorical(self, orient, convert_axes, numpy):
         expected.index = expected.index.astype(str)  # Categorical not preserved
         expected.index.name = None  # index names aren't preserved in JSON
 
-        if not numpy and (orient == "index" or (PY35 and orient == "columns")):
+        if not numpy and orient == "index":
             expected = expected.sort_index()
 
         assert_json_roundtrip_equal(result, expected, orient)
@@ -317,7 +308,7 @@ def test_roundtrip_mixed(self, orient, convert_axes, numpy):
         expected = df.copy()
         expected = expected.assign(**expected.select_dtypes("number").astype(np.int64))
 
-        if not numpy and (orient == "index" or (PY35 and orient == "columns")):
+        if not numpy and orient == "index":
             expected = expected.sort_index()
 
         assert_json_roundtrip_equal(result, expected, orient)
@@ -652,8 +643,6 @@ def test_series_roundtrip_simple(self, orient, numpy):
         result = pd.read_json(data, typ="series", orient=orient, numpy=numpy)
         expected = self.series.copy()
 
-        if not numpy and PY35 and orient in ("index", "columns"):
-            expected = expected.sort_index()
         if orient in ("values", "records"):
             expected = expected.reset_index(drop=True)
         if orient != "split":
@@ -670,8 +659,6 @@ def test_series_roundtrip_object(self, orient, numpy, dtype):
         )
         expected = self.objSeries.copy()
 
-        if not numpy and PY35 and orient in ("index", "columns"):
-            expected = expected.sort_index()
         if orient in ("values", "records"):
             expected = expected.reset_index(drop=True)
         if orient != "split":
@@ -686,8 +673,6 @@ def test_series_roundtrip_empty(self, orient, numpy):
         expected = self.empty_series.copy()
 
         # TODO: see what causes inconsistency
-        if not numpy and PY35 and orient == "index":
-            expected = expected.sort_index()
         if orient in ("values", "records"):
             expected = expected.reset_index(drop=True)
         else:
@@ -1611,11 +1596,7 @@ def test_json_indent_all_orients(self, orient, expected):
         # GH 12004
         df = pd.DataFrame([["foo", "bar"], ["baz", "qux"]], columns=["a", "b"])
         result = df.to_json(orient=orient, indent=4)
-
-        if PY35:
-            assert json.loads(result) == json.loads(expected)
-        else:
-            assert result == expected
+        assert result == expected
 
     def test_json_negative_indent_raises(self):
         with pytest.raises(ValueError, match="must be a nonnegative integer"):
diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py
--- a/pandas/tests/scalar/period/test_period.py
+++ b/pandas/tests/scalar/period/test_period.py
@@ -1,5 +1,7 @@
 from datetime import date, datetime, timedelta
+from distutils.version import StrictVersion
 
+import dateutil
 import numpy as np
 import pytest
 import pytz
@@ -10,7 +12,6 @@
 from pandas._libs.tslibs.parsing import DateParseError
 from pandas._libs.tslibs.period import IncompatibleFrequency
 from pandas._libs.tslibs.timezones import dateutil_gettz, maybe_get_tz
-from pandas.compat import PY35
 from pandas.compat.numpy import np_datetime64_compat
 
 import pandas as pd
@@ -1546,10 +1547,8 @@ def test_period_immutable():
 
 
 @pytest.mark.xfail(
-    # xpassing on MacPython with strict=False
-    # https://travis-ci.org/MacPython/pandas-wheels/jobs/574706922
-    PY35,
-    reason="Parsing as Period('0007-01-01', 'D') for reasons unknown",
+    StrictVersion(dateutil.__version__.split(".dev")[0]) < StrictVersion("2.7.0"),
+    reason="Bug in dateutil < 2.7.0 when parsing old dates: Period('0001-01-07', 'D')",
     strict=False,
 )
 def test_small_year_parsing():