Repository: PrefectHQ/prefect

Issue:
Drop Python 3.5 support
Dask (and Dask Distributed) are core dependencies of Prefect and [have dropped Python 3.5](https://github.com/dask/dask/issues/5478) support.  This means that all Prefect users are effectively stuck on old versions of dask with Prefect.

Given that, and given the occasional frustration of supporting Python 3.5 (typing, dict ordering, etc.) I'd like to open up a discussion for whether we should continue supporting 3.5 or not.  To be clear, if users are not using dask executors or hooks, then they might not care.

If you are a user who requires 3.5 support please chime in here so we can assess this decision!


Discussion:
+1  We're in favor of dropping 3.5 support.
Update on this: what I would like to do is after the next minor release we remove "support" for Python 3.5 by taking out the CircleCI job that runs tests on 3.5 and add a deprecation warning on import. The for the minor release after we update the serializers to use a > 3.5 library such as [pydantic](https://pydantic-docs.helpmanual.io/) and officially remove compatibility. 

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -81,7 +81,7 @@ def run(self):
     package_dir={"": "src"},
     include_package_data=True,
     entry_points={"console_scripts": ["prefect=prefect.cli:cli"]},
-    python_requires=">=3.5.2",
+    python_requires=">=3.6",
     description="The Prefect Core automation and scheduling engine.",
     long_description=open("README.md").read(),
     long_description_content_type="text/markdown",
@@ -95,9 +95,9 @@ def run(self):
         "Intended Audience :: System Administrators",
         "License :: OSI Approved :: Apache Software License",
         "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
         "Topic :: Software Development :: Libraries",
         "Topic :: System :: Monitoring",
     ],
diff --git a/src/prefect/agent/agent.py b/src/prefect/agent/agent.py
--- a/src/prefect/agent/agent.py
+++ b/src/prefect/agent/agent.py
@@ -150,7 +150,7 @@ def start(self) -> None:
                 index = 0
                 remaining_polls = math.inf if self.max_polls is None else self.max_polls
 
-                # the max workers default has changed in 3.5 and 3.8. For stable results the
+                # the max workers default has changed in 3.8. For stable results the
                 # default 3.8 behavior is elected here.
                 max_workers = min(32, (os.cpu_count() or 1) + 4)
 


Test patch:
diff --git a/docs/core/development/tests.md b/docs/core/development/tests.md
--- a/docs/core/development/tests.md
+++ b/docs/core/development/tests.md
@@ -25,7 +25,7 @@ Prefect has a few flags for modifying pytest's behavior. Passing `--skip-formatt
 
 ## CI
 
-In CI, Prefect's unit tests are run against Python 3.5, 3.6, and 3.7. Much as we love `f-strings`, Prefect code must support all versions. A common pitfall is writing tests that implicitly depend on Python 3.6's implicitly-ordered dictionaries and sets; these tests can fail non-deterministically in Python 3.5.
+In CI, Prefect's unit tests are run against Python 3.6, 3.7, and 3.8. As much as we love `f-strings`, Prefect code must support all versions. A common pitfall is writing tests that implicitly depend on Python 3.6's implicitly-ordered dictionaries and sets.
 
 Though Prefect's unit tests include tests for black and mypy (see the [style guide](style.md)), a separate "formatting" CI job is also run. Since formatting errors are common in PRs, we have found this to be a useful early-warning during development.
 
diff --git a/tests/cli/test_describe.py b/tests/cli/test_describe.py
--- a/tests/cli/test_describe.py
+++ b/tests/cli/test_describe.py
@@ -31,9 +31,6 @@ def test_describe_help():
     )
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_describe_flows(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -91,9 +88,6 @@ def test_describe_flows_not_found(monkeypatch):
         assert "flow not found" in result.output
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_describe_flows_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -136,9 +130,6 @@ def test_describe_flows_populated(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_describe_tasks(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -216,9 +207,6 @@ def test_describe_tasks_not_found(monkeypatch):
         assert "No tasks found for flow flow" in result.output
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_describe_flow_runs(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -282,9 +270,6 @@ def test_describe_flow_runs_not_found(monkeypatch):
         assert "flow-run not found" in result.output
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_describe_flow_runs_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
diff --git a/tests/cli/test_get.py b/tests/cli/test_get.py
--- a/tests/cli/test_get.py
+++ b/tests/cli/test_get.py
@@ -29,9 +29,6 @@ def test_get_help():
     )
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_flows(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(flow=[]))))
@@ -70,9 +67,6 @@ def test_get_flows(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_flows_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(flow=[]))))
@@ -119,9 +113,6 @@ def test_get_flows_populated(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_projects(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(project=[]))))
@@ -162,9 +153,6 @@ def test_get_projects(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_projects_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(project=[]))))
@@ -199,9 +187,6 @@ def test_get_projects_populated(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_flow_runs(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -246,9 +231,6 @@ def test_get_flow_runs(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_flow_runs_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -297,9 +279,6 @@ def test_get_flow_runs_populated(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_tasks(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(task=[]))))
@@ -342,9 +321,6 @@ def test_get_tasks(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_tasks_populated(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(json=MagicMock(return_value=dict(data=dict(task=[]))))
@@ -394,9 +370,6 @@ def test_get_tasks_populated(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_logs(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
@@ -453,9 +426,6 @@ def test_get_logs(monkeypatch):
         assert post.call_args[1]["json"]["query"].split() == query.split()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_get_logs_info(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -29,9 +29,6 @@ def test_run_help():
     assert "Run Prefect flows." in result.output
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6), reason="3.5 does not preserve dictionary order"
-)
 def test_run_cloud(monkeypatch):
     post = MagicMock(
         return_value=MagicMock(
diff --git a/tests/core/test_flow.py b/tests/core/test_flow.py
--- a/tests/core/test_flow.py
+++ b/tests/core/test_flow.py
@@ -2127,7 +2127,7 @@ def report_start_time():
 
     def test_flow_dot_run_updates_the_scheduled_start_time_of_each_scheduled_run(self):
 
-        start_times = [pendulum.now().add(seconds=i * 0.1) for i in range(1, 4)]
+        start_times = [pendulum.now().add(seconds=i * 0.2) for i in range(1, 4)]
         REPORTED_START_TIMES = []
 
         @task
diff --git a/tests/engine/cloud/test_cloud_flows.py b/tests/engine/cloud/test_cloud_flows.py
--- a/tests/engine/cloud/test_cloud_flows.py
+++ b/tests/engine/cloud/test_cloud_flows.py
@@ -558,10 +558,6 @@ def test_deep_map_with_a_failure(monkeypatch, executor):
     assert t3_0.state.is_failed()
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 6),
-    reason="Mysteriously fails in different ways on Python 3.5. TODO: Investigate.",
-)
 def test_deep_map_with_a_retry(monkeypatch):
     """
     Creates a situation in which a deeply-mapped Flow encounters a one-time error in one
diff --git a/tests/environments/storage/test_docker_healthcheck.py b/tests/environments/storage/test_docker_healthcheck.py
--- a/tests/environments/storage/test_docker_healthcheck.py
+++ b/tests/environments/storage/test_docker_healthcheck.py
@@ -192,9 +192,6 @@ class NewEnvironment(Environment):
 
         assert healthchecks.environment_dependency_check([flow]) is None
 
-    @pytest.mark.skipif(
-        sys.version_info < (3, 6), reason="3.5 does not support ModuleNotFoundError"
-    )
     def test_raise_on_missing_imports(self, monkeypatch):
         class NewEnvironment(Environment):
             @property
diff --git a/tests/utilities/test_serialization.py b/tests/utilities/test_serialization.py
--- a/tests/utilities/test_serialization.py
+++ b/tests/utilities/test_serialization.py
@@ -236,7 +236,6 @@ def test_serialize_outer_no_state(self):
         assert not serialized["f"]["kwargs"]
 
     def test_serialize_outer_with_state(self):
-        """Have to account for order because of Python 3.5"""
         serialized = self.Schema().dump(dict(f=outer(x=1, y=2, z=99)))
         assert serialized["f"]["fn"] == "tests.utilities.test_serialization.outer"
         assert serialized["f"]["kwargs"] == {"x": 1, "y": 2, "z": 99}