From: Benjamin Drung <benjamin.drung@canonical.com>
Date: Fri, 14 Nov 2025 00:30:45 +0100
Subject: Replace pytz by zoneinfo

pytz has served the Python community well for many years, but it is no
longer the best option for providing time zones. pytz has a non-standard
interface that is very easy to misuse (see
https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html);
this interface was necessary when pytz was created, because datetime had
no way to represent ambiguous datetimes, but this was solved in Python
3.6, which added a fold attribute to datetimes in PEP 495. With the
addition of the zoneinfo module in Python 3.9 (PEP 615), there has never
been a better time to migrate away from pytz.

Forwarded: https://github.com/flask-restful/flask-restful/pull/989
---
 flask_restful/inputs.py | 17 +++++-----
 setup.py                |  1 -
 tests/test_fields.py    | 18 +++++-----
 tests/test_inputs.py    | 89 ++++++++++++++++++++++++-------------------------
 4 files changed, 61 insertions(+), 64 deletions(-)

diff --git a/flask_restful/inputs.py b/flask_restful/inputs.py
index 1b36c85..335844a 100644
--- a/flask_restful/inputs.py
+++ b/flask_restful/inputs.py
@@ -1,14 +1,13 @@
 from calendar import timegm
-from datetime import datetime, time, timedelta
+from datetime import datetime, time, timedelta, timezone
 from email.utils import parsedate_tz, mktime_tz
 import re
 
 import aniso8601
-import pytz
 
 # Constants for upgrading date-based intervals to full datetimes.
-START_OF_DAY = time(0, 0, 0, tzinfo=pytz.UTC)
-END_OF_DAY = time(23, 59, 59, 999999, tzinfo=pytz.UTC)
+START_OF_DAY = time(0, 0, 0, tzinfo=timezone.utc)
+END_OF_DAY = time(23, 59, 59, 999999, tzinfo=timezone.utc)
 
 # https://code.djangoproject.com/browser/django/trunk/django/core/validators.py
 # basic auth added by frank
@@ -93,11 +92,11 @@ def _normalize_interval(start, end, value):
         end = datetime.combine(end, START_OF_DAY)
 
     if start.tzinfo is None:
-        start = pytz.UTC.localize(start)
-        end = pytz.UTC.localize(end)
+        start = start.replace(tzinfo=timezone.utc)
+        end = end.replace(tzinfo=timezone.utc)
     else:
-        start = start.astimezone(pytz.UTC)
-        end = end.astimezone(pytz.UTC)
+        start = start.astimezone(timezone.utc)
+        end = end.astimezone(timezone.utc)
 
     return start, end
 
@@ -265,7 +264,7 @@ def datetime_from_rfc822(datetime_str):
     :type datetime_str: str
     :return: A datetime
     """
-    return datetime.fromtimestamp(mktime_tz(parsedate_tz(datetime_str)), pytz.utc)
+    return datetime.fromtimestamp(mktime_tz(parsedate_tz(datetime_str)), timezone.utc)
 
 
 def datetime_from_iso8601(datetime_str):
diff --git a/setup.py b/setup.py
index 676450f..935beb3 100755
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,6 @@ from setuptools import setup, find_packages
 requirements = [
     'aniso8601>=0.82',
     'Flask>=0.8',
-    'pytz',
 ]
 
 
diff --git a/tests/test_fields.py b/tests/test_fields.py
index cc6e6ee..38cab49 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,12 +1,12 @@
 from decimal import Decimal
 from functools import partial
-import pytz
 import unittest
+import zoneinfo
 from unittest.mock import Mock
 from flask_restful.fields import MarshallingException
 from flask_restful.utils import OrderedDict
 from flask_restful import fields
-from datetime import datetime, timedelta, tzinfo
+from datetime import datetime, timedelta, timezone, tzinfo
 from flask import Flask, Blueprint
 #noinspection PyUnresolvedReferences
 from tests import assert_equals
@@ -52,9 +52,9 @@ def test_rfc822_datetime_formatters():
         (datetime(2011, 1, 1), "Sat, 01 Jan 2011 00:00:00 -0000"),
         (datetime(2011, 1, 1, 23, 59, 59),
          "Sat, 01 Jan 2011 23:59:59 -0000"),
-        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc),
+        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc),
          "Sat, 01 Jan 2011 23:59:59 -0000"),
-        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.timezone('CET')),
+        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo('Africa/Lagos')),
          "Sat, 01 Jan 2011 22:59:59 -0000")
     ]
     for date_obj, expected in dates:
@@ -68,11 +68,11 @@ def test_iso8601_datetime_formatters():
          "2011-01-01T23:59:59"),
         (datetime(2011, 1, 1, 23, 59, 59, 1000),
          "2011-01-01T23:59:59.001000"),
-        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc),
+        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc),
          "2011-01-01T23:59:59+00:00"),
-        (datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=pytz.utc),
+        (datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=timezone.utc),
          "2011-01-01T23:59:59.001000+00:00"),
-        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.timezone('CET')),
+        (datetime(2011, 1, 1, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo('Africa/Lagos')),
          "2011-01-01T23:59:59+01:00")
     ]
     for date_obj, expected in dates:
@@ -363,7 +363,7 @@ class FieldsTestCase(unittest.TestCase):
         self.assertEqual("Mon, 22 Aug 2011 20:58:45 -0000", field.output("bar", obj))
 
     def test_rfc822_date_field_with_offset(self):
-        obj = {"bar": datetime(2011, 8, 22, 20, 58, 45, tzinfo=pytz.timezone('CET'))}
+        obj = {"bar": datetime(2011, 8, 22, 20, 58, 45, tzinfo=zoneinfo.ZoneInfo('Africa/Lagos'))}
         field = fields.DateTime()
         self.assertEqual("Mon, 22 Aug 2011 19:58:45 -0000", field.output("bar", obj))
 
@@ -373,7 +373,7 @@ class FieldsTestCase(unittest.TestCase):
         self.assertEqual("2011-08-22T20:58:45", field.output("bar", obj))
 
     def test_iso8601_date_field_with_offset(self):
-        obj = {"bar": datetime(2011, 8, 22, 20, 58, 45, tzinfo=pytz.timezone('CET'))}
+        obj = {"bar": datetime(2011, 8, 22, 20, 58, 45, tzinfo=zoneinfo.ZoneInfo('Africa/Lagos'))}
         field = fields.DateTime(dt_format='iso8601')
         self.assertEqual("2011-08-22T20:58:45+01:00", field.output("bar", obj))
 
diff --git a/tests/test_inputs.py b/tests/test_inputs.py
index a1f0557..d1e7f70 100644
--- a/tests/test_inputs.py
+++ b/tests/test_inputs.py
@@ -1,6 +1,5 @@
-from datetime import datetime, timedelta, tzinfo
+from datetime import datetime, timedelta, timezone, tzinfo
 import unittest
-import pytz
 import re
 
 #noinspection PyUnresolvedReferences
@@ -12,9 +11,9 @@ from tests import assert_equal
 
 def test_reverse_rfc822_datetime():
     dates = [
-        ("Sat, 01 Jan 2011 00:00:00 -0000", datetime(2011, 1, 1, tzinfo=pytz.utc)),
-        ("Sat, 01 Jan 2011 23:59:59 -0000", datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc)),
-        ("Sat, 01 Jan 2011 21:59:59 -0200", datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc)),
+        ("Sat, 01 Jan 2011 00:00:00 -0000", datetime(2011, 1, 1, tzinfo=timezone.utc)),
+        ("Sat, 01 Jan 2011 23:59:59 -0000", datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc)),
+        ("Sat, 01 Jan 2011 21:59:59 -0200", datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc)),
     ]
 
     for date_string, expected in dates:
@@ -23,10 +22,10 @@ def test_reverse_rfc822_datetime():
 
 def test_reverse_iso8601_datetime():
     dates = [
-        ("2011-01-01T00:00:00+00:00", datetime(2011, 1, 1, tzinfo=pytz.utc)),
-        ("2011-01-01T23:59:59+00:00", datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc)),
-        ("2011-01-01T23:59:59.001000+00:00", datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=pytz.utc)),
-        ("2011-01-01T23:59:59+02:00", datetime(2011, 1, 1, 21, 59, 59, tzinfo=pytz.utc))
+        ("2011-01-01T00:00:00+00:00", datetime(2011, 1, 1, tzinfo=timezone.utc)),
+        ("2011-01-01T23:59:59+00:00", datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc)),
+        ("2011-01-01T23:59:59.001000+00:00", datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=timezone.utc)),
+        ("2011-01-01T23:59:59+02:00", datetime(2011, 1, 1, 21, 59, 59, tzinfo=timezone.utc))
     ]
 
     for date_string, expected in dates:
@@ -245,64 +244,64 @@ def test_isointerval():
             # Full precision with explicit UTC.
             "2013-01-01T12:30:00Z/P1Y2M3DT4H5M6S",
             (
-                datetime(2013, 1, 1, 12, 30, 0, tzinfo=pytz.utc),
-                datetime(2014, 3, 5, 16, 35, 6, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
+                datetime(2014, 3, 5, 16, 35, 6, tzinfo=timezone.utc),
             ),
         ),
         (
             # Full precision with alternate UTC indication
             "2013-01-01T12:30+00:00/P2D",
             (
-                datetime(2013, 1, 1, 12, 30, 0, tzinfo=pytz.utc),
-                datetime(2013, 1, 3, 12, 30, 0, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
+                datetime(2013, 1, 3, 12, 30, 0, tzinfo=timezone.utc),
             ),
         ),
         (
             # Implicit UTC with time
             "2013-01-01T15:00/P1M",
             (
-                datetime(2013, 1, 1, 15, 0, 0, tzinfo=pytz.utc),
-                datetime(2013, 1, 31, 15, 0, 0, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 15, 0, 0, tzinfo=timezone.utc),
+                datetime(2013, 1, 31, 15, 0, 0, tzinfo=timezone.utc),
             ),
         ),
         (
             # TZ conversion
             "2013-01-01T17:00-05:00/P2W",
             (
-                datetime(2013, 1, 1, 22, 0, 0, tzinfo=pytz.utc),
-                datetime(2013, 1, 15, 22, 0, 0, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 22, 0, 0, tzinfo=timezone.utc),
+                datetime(2013, 1, 15, 22, 0, 0, tzinfo=timezone.utc),
             ),
         ),
         (
             # Date upgrade to midnight-midnight period
             "2013-01-01/P3D",
             (
-                datetime(2013, 1, 1, 0, 0, 0, tzinfo=pytz.utc),
-                datetime(2013, 1, 4, 0, 0, 0, 0, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
+                datetime(2013, 1, 4, 0, 0, 0, 0, tzinfo=timezone.utc),
             ),
         ),
         (
             # Start/end with UTC
             "2013-01-01T12:00:00Z/2013-02-01T12:00:00Z",
             (
-                datetime(2013, 1, 1, 12, 0, 0, tzinfo=pytz.utc),
-                datetime(2013, 2, 1, 12, 0, 0, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
+                datetime(2013, 2, 1, 12, 0, 0, tzinfo=timezone.utc),
             ),
         ),
         (
             # Start/end with time upgrade
             "2013-01-01/2013-06-30",
             (
-                datetime(2013, 1, 1, tzinfo=pytz.utc),
-                datetime(2013, 6, 30, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, tzinfo=timezone.utc),
+                datetime(2013, 6, 30, tzinfo=timezone.utc),
             ),
         ),
         (
             # Start/end with TZ conversion
             "2013-02-17T12:00:00-07:00/2013-02-28T15:00:00-07:00",
             (
-                datetime(2013, 2, 17, 19, 0, 0, tzinfo=pytz.utc),
-                datetime(2013, 2, 28, 22, 0, 0, tzinfo=pytz.utc),
+                datetime(2013, 2, 17, 19, 0, 0, tzinfo=timezone.utc),
+                datetime(2013, 2, 28, 22, 0, 0, tzinfo=timezone.utc),
             ),
         ),
         # Resolution expansion for single date(time)
@@ -310,72 +309,72 @@ def test_isointerval():
             # Second with UTC
             "2013-01-01T12:30:45Z",
             (
-                datetime(2013, 1, 1, 12, 30, 45, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 12, 30, 46, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, 45, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 12, 30, 46, tzinfo=timezone.utc),
             ),
         ),
         (
             # Second with tz conversion
             "2013-01-01T12:30:45+02:00",
             (
-                datetime(2013, 1, 1, 10, 30, 45, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 10, 30, 46, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 10, 30, 45, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 10, 30, 46, tzinfo=timezone.utc),
             ),
         ),
         (
             # Second with implicit UTC
             "2013-01-01T12:30:45",
             (
-                datetime(2013, 1, 1, 12, 30, 45, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 12, 30, 46, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, 45, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 12, 30, 46, tzinfo=timezone.utc),
             ),
         ),
         (
             # Minute with UTC
             "2013-01-01T12:30+00:00",
             (
-                datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 12, 31, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 12, 31, tzinfo=timezone.utc),
             ),
         ),
         (
             # Minute with conversion
             "2013-01-01T12:30+04:00",
             (
-                datetime(2013, 1, 1, 8, 30, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 8, 31, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 8, 30, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 8, 31, tzinfo=timezone.utc),
             ),
         ),
         (
             # Minute with implicit UTC
             "2013-01-01T12:30",
             (
-                datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 12, 31, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, 30, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 12, 31, tzinfo=timezone.utc),
             ),
         ),
         (
             # Hour, explicit UTC
             "2013-01-01T12Z",
             (
-                datetime(2013, 1, 1, 12, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 13, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 13, tzinfo=timezone.utc),
             ),
         ),
         (
             # Hour with offset
             "2013-01-01T12-07:00",
             (
-                datetime(2013, 1, 1, 19, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 20, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 19, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 20, tzinfo=timezone.utc),
             ),
         ),
         (
             # Hour with implicit UTC
             "2013-01-01T12",
             (
-                datetime(2013, 1, 1, 12, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 13, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 13, tzinfo=timezone.utc),
             ),
         ),
         (
@@ -383,8 +382,8 @@ def test_isointerval():
             # be accepted.
             "2013-01-01T12:00:00.0/2013-01-01T12:30:00.000000",
             (
-                datetime(2013, 1, 1, 12, tzinfo=pytz.utc),
-                datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc),
+                datetime(2013, 1, 1, 12, tzinfo=timezone.utc),
+                datetime(2013, 1, 1, 12, 30, tzinfo=timezone.utc),
             ),
         ),
     ]
