Repository: marshmallow-code/marshmallow
Pull request #2318 (2024-10-09 13:34:56)

Drop Python 3.8
https://endoflife.date/python


Patch:
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 2232d637..9d56b13b 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -34,7 +34,7 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - { name: "3.8", python: "3.8", tox: py38 }
+          - { name: "3.9", python: "3.9", tox: py39 }
           - { name: "3.12", python: "3.12", tox: py312 }
     steps:
       - uses: actions/checkout@v4.0.0
diff --git a/README.rst b/README.rst
index 2fc69d2d..8ffb37c7 100644
--- a/README.rst
+++ b/README.rst
@@ -86,7 +86,7 @@ Full documentation is available at https://marshmallow.readthedocs.io/ .
 Requirements
 ============
 
-- Python >= 3.8
+- Python >= 3.9
 
 Ecosystem
 =========
diff --git a/pyproject.toml b/pyproject.toml
index 115a1916..bbef97df 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,13 +15,12 @@ classifiers = [
   "Intended Audience :: Developers",
   "License :: OSI Approved :: MIT License",
   "Programming Language :: Python :: 3",
-  "Programming Language :: Python :: 3.8",
   "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
   "Programming Language :: Python :: 3.11",
   "Programming Language :: Python :: 3.12",
 ]
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 dependencies = ["packaging>=17.0"]
 
 [project.urls]
@@ -39,7 +38,7 @@ docs = [
   "sphinx-version-warning==1.1.2",
   "autodocsumm==0.2.13",
 ]
-tests = ["pytest", "pytz", "simplejson"]
+tests = ["pytest", "simplejson"]
 dev = ["marshmallow[tests]", "tox", "pre-commit>=3.5,<5.0"]
 
 [build-system]
diff --git a/src/marshmallow/class_registry.py b/src/marshmallow/class_registry.py
index 810d1155..d41b112e 100644
--- a/src/marshmallow/class_registry.py
+++ b/src/marshmallow/class_registry.py
@@ -17,7 +17,7 @@ from marshmallow.exceptions import RegistryError
 if typing.TYPE_CHECKING:
     from marshmallow import Schema
 
-    SchemaType = typing.Type[Schema]
+    SchemaType = type[Schema]
 
 # {
 #   <class_name>: <list of class objects>
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index bdfd66ba..f366b83d 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -1240,7 +1240,7 @@ class DateTime(Field):
         "rfc822": utils.rfcformat,
         "timestamp": utils.timestamp,
         "timestamp_ms": utils.timestamp_ms,
-    }  # type: typing.Dict[str, typing.Callable[[typing.Any], str | float]]
+    }  # type: dict[str, typing.Callable[[typing.Any], str | float]]
 
     DESERIALIZATION_FUNCS = {
         "iso": utils.from_iso_datetime,
@@ -1249,7 +1249,7 @@ class DateTime(Field):
         "rfc822": utils.from_rfc,
         "timestamp": utils.from_timestamp,
         "timestamp_ms": utils.from_timestamp_ms,
-    }  # type: typing.Dict[str, typing.Callable[[str], typing.Any]]
+    }  # type: dict[str, typing.Callable[[str], typing.Any]]
 
     DEFAULT_FORMAT = "iso"
 
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index 9d7dccfe..e21768e9 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -154,7 +154,7 @@ class SchemaMeta(ABCMeta):
         """
         mro = inspect.getmro(cls)
 
-        hooks = defaultdict(list)  # type: typing.Dict[str, typing.List[typing.Tuple[str, bool, dict]]]
+        hooks = defaultdict(list)  # type: dict[str, list[tuple[str, bool, dict]]]
 
         for attr_name in dir(cls):
             # Need to look up the actual descriptor, not whatever might be
@@ -174,7 +174,7 @@ class SchemaMeta(ABCMeta):
                 continue
 
             try:
-                hook_config = attr.__marshmallow_hook__  # type: typing.Dict[str, typing.List[typing.Tuple[bool, dict]]]
+                hook_config = attr.__marshmallow_hook__  # type: dict[str, list[tuple[bool, dict]]]
             except AttributeError:
                 pass
             else:
@@ -304,14 +304,14 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         dt.date: ma_fields.Date,
         dt.timedelta: ma_fields.TimeDelta,
         decimal.Decimal: ma_fields.Decimal,
-    }  # type: typing.Dict[type, typing.Type[ma_fields.Field]]
+    }  # type: dict[type, typing.Type[ma_fields.Field]]
     #: Overrides for default schema-level error messages
-    error_messages = {}  # type: typing.Dict[str, str]
+    error_messages = {}  # type: dict[str, str]
 
     _default_error_messages = {
         "type": "Invalid input type.",
         "unknown": "Unknown field.",
-    }  # type: typing.Dict[str, str]
+    }  # type: dict[str, str]
 
     OPTIONS_CLASS = SchemaOpts  # type: type
 
@@ -319,8 +319,8 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
 
     # These get set by SchemaMeta
     opts = None  # type: SchemaOpts
-    _declared_fields = {}  # type: typing.Dict[str, ma_fields.Field]
-    _hooks = {}  # type: typing.Dict[str, typing.List[typing.Tuple[str, bool, dict]]]
+    _declared_fields = {}  # type: dict[str, ma_fields.Field]
+    _hooks = {}  # type: dict[str, list[tuple[str, bool, dict]]]
 
     class Meta:
         """Options object for a Schema.
@@ -398,9 +398,9 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         self.context = context or {}
         self._normalize_nested_options()
         #: Dictionary mapping field_names -> :class:`Field` objects
-        self.fields = {}  # type: typing.Dict[str, ma_fields.Field]
-        self.load_fields = {}  # type: typing.Dict[str, ma_fields.Field]
-        self.dump_fields = {}  # type: typing.Dict[str, ma_fields.Field]
+        self.fields = {}  # type: dict[str, ma_fields.Field]
+        self.load_fields = {}  # type: dict[str, ma_fields.Field]
+        self.dump_fields = {}  # type: dict[str, ma_fields.Field]
         self._init_fields()
         messages = {}
         messages.update(self._default_error_messages)
@@ -802,7 +802,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         try:
             self._do_load(data, many=many, partial=partial, postprocess=False)
         except ValidationError as exc:
-            return typing.cast(typing.Dict[str, typing.List[str]], exc.messages)
+            return typing.cast(dict[str, list[str]], exc.messages)
         return {}
 
     ##### Private Helpers #####
diff --git a/tox.ini b/tox.ini
index bfb9b83e..ac8d64d5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = lint,mypy-test,py{38,39,310,311,312},docs
+envlist = lint,mypy-test,py{39,310,311,312},docs
 
 [testenv]
 extras = tests


Test patch:
diff --git a/tests/base.py b/tests/base.py
index cbf5bff8..ae1bed0b 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -4,14 +4,14 @@ import datetime as dt
 import functools
 import uuid
 from enum import Enum, IntEnum
+from zoneinfo import ZoneInfo
 
-import pytz
 import simplejson
 
 from marshmallow import Schema, fields, missing, post_load, validate
 from marshmallow.exceptions import ValidationError
 
-central = pytz.timezone("America/Chicago")
+central = ZoneInfo("America/Chicago")
 
 
 class GenderEnum(IntEnum):
@@ -104,9 +104,7 @@ class User:
         # A naive datetime
         self.created = dt.datetime(2013, 11, 10, 14, 20, 58)
         # A TZ-aware datetime
-        self.updated = central.localize(
-            dt.datetime(2013, 11, 10, 14, 20, 58), is_dst=False
-        )
+        self.updated = dt.datetime(2013, 11, 10, 14, 20, 58, tzinfo=central)
         self.id = id_
         self.homepage = homepage
         self.email = email
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index 442b5efb..80d44272 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -477,7 +477,7 @@ class TestFieldDeserialization:
             ),
             (
                 "Sun, 10 Nov 2013 01:23:45 -0600",
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
                 True,
             ),
         ],
@@ -521,7 +521,7 @@ class TestFieldDeserialization:
             ),
             (
                 "2013-11-10T01:23:45-06:00",
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
                 True,
             ),
         ],
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index fcb5a5e5..df778ce8 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -565,7 +565,7 @@ class TestFieldSerialization:
                 "Sun, 10 Nov 2013 01:23:45 +0000",
             ),
             (
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
                 "Sun, 10 Nov 2013 01:23:45 -0600",
             ),
         ],
@@ -586,7 +586,7 @@ class TestFieldSerialization:
             ),
             (
                 "timestamp",
-                central.localize(dt.datetime(2013, 11, 10, 0, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 0, 23, 45, tzinfo=central),
                 1384064625,
             ),
             ("timestamp_ms", dt.datetime(2013, 11, 10, 0, 23, 45), 1384043025000),
@@ -597,7 +597,7 @@ class TestFieldSerialization:
             ),
             (
                 "timestamp_ms",
-                central.localize(dt.datetime(2013, 11, 10, 0, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 0, 23, 45, tzinfo=central),
                 1384064625000,
             ),
         ],
@@ -620,7 +620,7 @@ class TestFieldSerialization:
                 "2013-11-10T01:23:45+00:00",
             ),
             (
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+                dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
                 "2013-11-10T01:23:45-06:00",
             ),
         ],
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 55c1bef6..44baa71f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -124,7 +124,7 @@ def test_is_collection():
             "Sun, 10 Nov 2013 01:23:45 +0000",
         ),
         (
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
             "Sun, 10 Nov 2013 01:23:45 -0600",
         ),
     ],
@@ -146,7 +146,7 @@ def test_rfc_format(value, expected):
             "2013-11-10T01:23:45+00:00",
         ),
         (
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
             "2013-11-10T01:23:45-06:00",
         ),
     ],
@@ -165,7 +165,7 @@ def test_isoformat(value, expected):
         ),
         (
             "Sun, 10 Nov 2013 01:23:45 -0600",
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
         ),
     ],
 )
@@ -194,7 +194,7 @@ def test_from_rfc(value, expected):
         ),
         (
             "2013-11-10T01:23:45-06:00",
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
+            dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central),
         ),
     ],
 )