Repository: arrow-py/arrow

Issue:
Consider backporting PEP 615
Just to make clear, [`backports.zoneinfo` already exists](https://pypi.org/project/backports.zoneinfo/) and in Python 3.6+, I (as the maintainer of `dateutil` and the creator of PEP 615) highly recommend you use it whenever possible. The "backport" I'm referring to is that I'm planning to take the underlying logic from the pure-Python implementation of `zoneinfo` and move it into `dateutil.tz`, to the extent possible. That will fix a number of issues, but my major efforts to get PEP 615 ready and working in Python 3.9 made my backlog of work grow enormously ☹.

_Originally posted by @pganssle in https://github.com/crsmithdev/arrow/pull/802#issuecomment-660503253_
[Question] Can remove `dateutil` from dependency?
## Feature Request

Python 3.9 introduced `ZoneInfo` and it backported 3.6+ (https://pypi.org/project/backports.zoneinfo/).
Looking at the code, except for `dateutil.rrule` and `dateutil.relativedelta`, it seems that `dateutil` is being used for some reason related to timezone. Can `ZoneInfo` replace the part and delete `dateutil`?


Discussion:


@jadchaar, @krisfremen and @systemcatch are more well-versed on this than I am. From what I recall, there was talk about removing dateutil as dependency earlier last year.  
Yup, it would be great to lose a dependency wit ZoneInfo, but we'd need to port over rrule and relativedelta or find potential alternatives before dropping the library. A PR would be appreciate @isac322 :)!
Related to:
https://github.com/arrow-py/arrow/issues/825
https://github.com/arrow-py/arrow/issues/825



Patch:
diff --git a/.gitignore b/.gitignore
index 0448d0cf..26f42fb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -209,3 +209,7 @@ $RECYCLE.BIN/
 
 # Windows shortcuts
 *.lnk
+
+# Claude Code configurations
+CLAUDE*.md
+.claude/
diff --git a/arrow/arrow.py b/arrow/arrow.py
index 9d1f5e30..99452556 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -157,7 +157,7 @@ def __init__(
         **kwargs: Any,
     ) -> None:
         if tzinfo is None:
-            tzinfo = dateutil_tz.tzutc()
+            tzinfo = timezone.utc
         # detect that tzinfo is a pytz object (issue #626)
         elif (
             isinstance(tzinfo, dt_tzinfo)
@@ -192,7 +192,7 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow":
         """
 
         if tzinfo is None:
-            tzinfo = dateutil_tz.tzlocal()
+            tzinfo = dt_datetime.now().astimezone().tzinfo
 
         dt = dt_datetime.now(tzinfo)
 
@@ -220,7 +220,7 @@ def utcnow(cls) -> "Arrow":
 
         """
 
-        dt = dt_datetime.now(dateutil_tz.tzutc())
+        dt = dt_datetime.now(timezone.utc)
 
         return cls(
             dt.year,
@@ -249,7 +249,7 @@ def fromtimestamp(
         """
 
         if tzinfo is None:
-            tzinfo = dateutil_tz.tzlocal()
+            tzinfo = dt_datetime.now().astimezone().tzinfo
         elif isinstance(tzinfo, str):
             tzinfo = parser.TzinfoParser.parse(tzinfo)
 
@@ -283,7 +283,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow":
             raise ValueError(f"The provided timestamp {timestamp!r} is invalid.")
 
         timestamp = util.normalize_timestamp(float(timestamp))
-        dt = dt_datetime.utcfromtimestamp(timestamp)
+        dt = dt_datetime.fromtimestamp(timestamp, timezone.utc)
 
         return cls(
             dt.year,
@@ -293,7 +293,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow":
             dt.minute,
             dt.second,
             dt.microsecond,
-            dateutil_tz.tzutc(),
+            timezone.utc,
             fold=getattr(dt, "fold", 0),
         )
 
@@ -317,7 +317,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr
 
         if tzinfo is None:
             if dt.tzinfo is None:
-                tzinfo = dateutil_tz.tzutc()
+                tzinfo = timezone.utc
             else:
                 tzinfo = dt.tzinfo
 
@@ -344,7 +344,7 @@ def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow":
         """
 
         if tzinfo is None:
-            tzinfo = dateutil_tz.tzutc()
+            tzinfo = timezone.utc
 
         return cls(date.year, date.month, date.day, tzinfo=tzinfo)
 
@@ -1151,7 +1151,7 @@ def humanize(
         locale = locales.get_locale(locale)
 
         if other is None:
-            utc = dt_datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.tzutc())
+            utc = dt_datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
             dt = utc.astimezone(self._datetime.tzinfo)
 
         elif isinstance(other, Arrow):
@@ -1792,7 +1792,7 @@ def __le__(self, other: Any) -> bool:
     def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo:
         """Get normalized tzinfo object from various inputs."""
         if tz_expr is None:
-            return dateutil_tz.tzutc()
+            return timezone.utc
         if isinstance(tz_expr, dt_tzinfo):
             return tz_expr
         else:
diff --git a/arrow/factory.py b/arrow/factory.py
index 53eb8d12..3e138a6a 100644
--- a/arrow/factory.py
+++ b/arrow/factory.py
@@ -6,14 +6,12 @@
 """
 
 import calendar
-from datetime import date, datetime
+from datetime import date, datetime, timezone
 from datetime import tzinfo as dt_tzinfo
 from decimal import Decimal
 from time import struct_time
 from typing import Any, List, Optional, Tuple, Type, Union, overload
 
-from dateutil import tz as dateutil_tz
-
 from arrow import parser
 from arrow.arrow import TZ_EXPR, Arrow
 from arrow.constants import DEFAULT_LOCALE
@@ -229,7 +227,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow:
             elif not isinstance(arg, str) and is_timestamp(arg):
                 if tz is None:
                     # set to UTC by default
-                    tz = dateutil_tz.tzutc()
+                    tz = timezone.utc
                 return self.type.fromtimestamp(arg, tzinfo=tz)
 
             # (Arrow) -> from the object's datetime @ tzinfo
@@ -337,7 +335,7 @@ def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow:
         """
 
         if tz is None:
-            tz = dateutil_tz.tzlocal()
+            tz = datetime.now().astimezone().tzinfo
         elif not isinstance(tz, dt_tzinfo):
             tz = parser.TzinfoParser.parse(tz)
 
diff --git a/arrow/formatter.py b/arrow/formatter.py
index 6634545f..6c6a718c 100644
--- a/arrow/formatter.py
+++ b/arrow/formatter.py
@@ -1,11 +1,9 @@
 """Provides the :class:`Arrow <arrow.formatter.DateTimeFormatter>` class, an improved formatter for datetimes."""
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import Final, Optional, Pattern, cast
 
-from dateutil import tz as dateutil_tz
-
 from arrow import locales
 from arrow.constants import DEFAULT_LOCALE
 
@@ -122,7 +120,7 @@ def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]:
 
         if token in ["ZZ", "Z"]:
             separator = ":" if token == "ZZ" else ""
-            tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo
+            tz = timezone.utc if dt.tzinfo is None else dt.tzinfo
             # `dt` must be aware object. Otherwise, this line will raise AttributeError
             # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834
             # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects
diff --git a/arrow/parser.py b/arrow/parser.py
index 39297887..4a623291 100644
--- a/arrow/parser.py
+++ b/arrow/parser.py
@@ -1,7 +1,7 @@
 """Provides the :class:`Arrow <arrow.parser.DateTimeParser>` class, a better way to parse datetime strings."""
 
 import re
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from datetime import tzinfo as dt_tzinfo
 from functools import lru_cache
 from typing import (
@@ -23,7 +23,10 @@
     overload,
 )
 
-from dateutil import tz
+try:
+    from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
+except ImportError:
+    from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError  # type: ignore[no-redef]
 
 from arrow import locales
 from arrow.constants import DEFAULT_LOCALE
@@ -727,14 +730,14 @@ def _build_datetime(parts: _Parts) -> datetime:
         timestamp = parts.get("timestamp")
 
         if timestamp is not None:
-            return datetime.fromtimestamp(timestamp, tz=tz.tzutc())
+            return datetime.fromtimestamp(timestamp, tz=timezone.utc)
 
         expanded_timestamp = parts.get("expanded_timestamp")
 
         if expanded_timestamp is not None:
             return datetime.fromtimestamp(
                 normalize_timestamp(expanded_timestamp),
-                tz=tz.tzutc(),
+                tz=timezone.utc,
             )
 
         day_of_year = parts.get("day_of_year")
@@ -907,10 +910,10 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo:
         tzinfo: Optional[dt_tzinfo] = None
 
         if tzinfo_string == "local":
-            tzinfo = tz.tzlocal()
+            tzinfo = datetime.now().astimezone().tzinfo
 
         elif tzinfo_string in ["utc", "UTC", "Z"]:
-            tzinfo = tz.tzutc()
+            tzinfo = timezone.utc
 
         else:
             iso_match = cls._TZINFO_RE.match(tzinfo_string)
@@ -925,10 +928,13 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo:
                 if sign == "-":
                     seconds *= -1
 
-                tzinfo = tz.tzoffset(None, seconds)
+                tzinfo = timezone(timedelta(seconds=seconds))
 
             else:
-                tzinfo = tz.gettz(tzinfo_string)
+                try:
+                    tzinfo = ZoneInfo(tzinfo_string)
+                except ZoneInfoNotFoundError:
+                    tzinfo = None
 
         if tzinfo is None:
             raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.")
diff --git a/pyproject.toml b/pyproject.toml
index 3e236917..71e75010 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,8 @@ classifiers = [
 ]
 dependencies = [
     "python-dateutil>=2.7.0",
+    "backports.zoneinfo==0.2.1;python_version<'3.9'",
+    "tzdata;python_version>='3.9'",
 ]
 requires-python = ">=3.8"
 description = "Better dates & times for Python"
@@ -41,13 +43,12 @@ dynamic = ["version"]
 
 [project.optional-dependencies]
 test = [
-    "backports.zoneinfo==0.2.1;python_version<'3.9'",
     "dateparser==1.*",
     "pre-commit",
     "pytest",
     "pytest-cov",
     "pytest-mock",
-    "pytz==2021.1",
+    "pytz==2025.2",
     "simplejson==3.*",
 ]
 doc = [
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 65134a19..fa333102 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,1 +1,3 @@
+backports.zoneinfo==0.2.1;python_version<'3.9'
 python-dateutil>=2.7.0
+tzdata;python_version>='3.9'


Test patch:
diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt
index 94e5cc84..d9f660e3 100644
--- a/requirements/requirements-tests.txt
+++ b/requirements/requirements-tests.txt
@@ -1,10 +1,9 @@
 -r requirements.txt
-backports.zoneinfo==0.2.1;python_version<'3.9'
 dateparser==1.*
 pre-commit
 pytest
 pytest-cov
 pytest-mock
-pytz==2021.1
+pytz==2025.2
 simplejson==3.*
 types-python-dateutil>=2.8.10
diff --git a/tests/conftest.py b/tests/conftest.py
index 5d5b9980..b6127073 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,11 @@
 from datetime import datetime
 
 import pytest
-from dateutil import tz as dateutil_tz
+
+try:
+    from zoneinfo import ZoneInfo
+except ImportError:
+    from backports.zoneinfo import ZoneInfo
 
 from arrow import arrow, factory, formatter, locales, parser
 
@@ -32,7 +36,7 @@ def time_2013_02_15(request):
 @pytest.fixture(scope="class")
 def time_1975_12_25(request):
     request.cls.datetime = datetime(
-        1975, 12, 25, 14, 15, 16, tzinfo=dateutil_tz.gettz("America/New_York")
+        1975, 12, 25, 14, 15, 16, tzinfo=ZoneInfo("America/New_York")
     )
     request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime)
 
diff --git a/tests/test_arrow.py b/tests/test_arrow.py
index 5afe9baa..c682fa3d 100644
--- a/tests/test_arrow.py
+++ b/tests/test_arrow.py
@@ -1,7 +1,7 @@
 try:
-    import zoneinfo
+    from zoneinfo import ZoneInfo
 except ImportError:
-    from backports import zoneinfo
+    from backports.zoneinfo import ZoneInfo
 
 import pickle
 import sys
@@ -54,10 +54,10 @@ def test_init(self):
         assert result._datetime == self.expected
 
         result = arrow.Arrow(
-            2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris")
+            2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
         )
         self.expected = datetime(
-            2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris")
+            2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
         )
         assert result._datetime == self.expected
 
@@ -67,17 +67,27 @@ def test_init_pytz_timezone(self):
             2013, 2, 2, 12, 30, 45, 999999, tzinfo=pytz.timezone("Europe/Paris")
         )
         self.expected = datetime(
-            2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris")
+            2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
         )
         assert result._datetime == self.expected
         assert_datetime_equality(result._datetime, self.expected, 1)
 
     def test_init_zoneinfo_timezone(self):
         result = arrow.Arrow(
-            2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris")
+            2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
         )
         self.expected = datetime(
-            2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris")
+            2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
+        )
+        assert result._datetime == self.expected
+        assert_datetime_equality(result._datetime, self.expected, 1)
+
+    def test_init_dateutil_timezone(self):
+        result = arrow.Arrow(
+            2024, 7, 10, 18, 55, 45, 999999, tzinfo=tz.gettz("Europe/Paris")
+        )
+        self.expected = datetime(
+            2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris")
         )
         assert result._datetime == self.expected
         assert_datetime_equality(result._datetime, self.expected, 1)
@@ -98,15 +108,13 @@ class TestTestArrowFactory:
     def test_now(self):
         result = arrow.Arrow.now()
 
-        assert_datetime_equality(
-            result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())
-        )
+        assert_datetime_equality(result._datetime, datetime.now().astimezone())
 
     def test_utcnow(self):
         result = arrow.Arrow.utcnow()
 
         assert_datetime_equality(
-            result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc())
+            result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
         )
 
         assert result.fold == 0
@@ -115,20 +123,18 @@ def test_fromtimestamp(self):
         timestamp = time.time()
 
         result = arrow.Arrow.fromtimestamp(timestamp)
-        assert_datetime_equality(
-            result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())
-        )
+        assert_datetime_equality(result._datetime, datetime.now().astimezone())
 
-        result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris"))
+        result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=ZoneInfo("Europe/Paris"))
         assert_datetime_equality(
             result._datetime,
-            datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")),
+            datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")),
         )
 
         result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris")
         assert_datetime_equality(
             result._datetime,
-            datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")),
+            datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")),
         )
 
         with pytest.raises(ValueError):
@@ -139,7 +145,7 @@ def test_utcfromtimestamp(self):
 
         result = arrow.Arrow.utcfromtimestamp(timestamp)
         assert_datetime_equality(
-            result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc())
+            result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
         )
 
         with pytest.raises(ValueError):
@@ -153,25 +159,25 @@ def test_fromdatetime(self):
         assert result._datetime == dt.replace(tzinfo=tz.tzutc())
 
     def test_fromdatetime_dt_tzinfo(self):
-        dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=tz.gettz("US/Pacific"))
+        dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=ZoneInfo("US/Pacific"))
 
         result = arrow.Arrow.fromdatetime(dt)
 
-        assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific"))
 
     def test_fromdatetime_tzinfo_arg(self):
         dt = datetime(2013, 2, 3, 12, 30, 45, 1)
 
-        result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific"))
+        result = arrow.Arrow.fromdatetime(dt, ZoneInfo("US/Pacific"))
 
-        assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific"))
 
     def test_fromdate(self):
         dt = date(2013, 2, 3)
 
-        result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific"))
+        result = arrow.Arrow.fromdate(dt, ZoneInfo("US/Pacific"))
 
-        assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == datetime(2013, 2, 3, tzinfo=ZoneInfo("US/Pacific"))
 
     def test_strptime(self):
         formatted = datetime(2013, 2, 3, 12, 30, 45).strftime("%Y-%m-%d %H:%M:%S")
@@ -180,10 +186,10 @@ def test_strptime(self):
         assert result._datetime == datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc())
 
         result = arrow.Arrow.strptime(
-            formatted, "%Y-%m-%d %H:%M:%S", tzinfo=tz.gettz("Europe/Paris")
+            formatted, "%Y-%m-%d %H:%M:%S", tzinfo=ZoneInfo("Europe/Paris")
         )
         assert result._datetime == datetime(
-            2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris")
+            2013, 2, 3, 12, 30, 45, tzinfo=ZoneInfo("Europe/Paris")
         )
 
     def test_fromordinal(self):
@@ -277,7 +283,7 @@ def test_getattr_dt_value(self):
         assert self.arrow.year == 2013
 
     def test_tzinfo(self):
-        assert self.arrow.tzinfo == tz.tzutc()
+        assert self.arrow.tzinfo == timezone.utc
 
     def test_naive(self):
         assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None)
@@ -432,7 +438,7 @@ def test_timetz(self):
         assert result == self.arrow._datetime.timetz()
 
     def test_astimezone(self):
-        other_tz = tz.gettz("US/Pacific")
+        other_tz = ZoneInfo("US/Pacific")
 
         result = self.arrow.astimezone(other_tz)
 
@@ -538,20 +544,20 @@ class TestArrowFalsePositiveDst:
 
     def test_dst(self):
         self.before_1 = arrow.Arrow(
-            2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York")
+            2016, 11, 6, 3, 59, tzinfo=ZoneInfo("America/New_York")
         )
-        self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York"))
-        self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York"))
+        self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=ZoneInfo("America/New_York"))
+        self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=ZoneInfo("America/New_York"))
         self.after_2 = arrow.Arrow(
-            2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York")
+            2016, 11, 6, 23, 59, tzinfo=ZoneInfo("America/New_York")
         )
         self.before_3 = arrow.Arrow(
-            2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York")
+            2018, 11, 4, 3, 59, tzinfo=ZoneInfo("America/New_York")
         )
-        self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York"))
-        self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York"))
+        self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=ZoneInfo("America/New_York"))
+        self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=ZoneInfo("America/New_York"))
         self.after_4 = arrow.Arrow(
-            2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York")
+            2018, 11, 4, 23, 59, tzinfo=ZoneInfo("America/New_York")
         )
         assert self.before_1.day == self.before_2.day
         assert self.after_1.day == self.after_2.day
@@ -562,9 +568,9 @@ def test_dst(self):
 class TestArrowConversion:
     def test_to(self):
         dt_from = datetime.now()
-        arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific"))
+        arrow_from = arrow.Arrow.fromdatetime(dt_from, ZoneInfo("US/Pacific"))
 
-        self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone(
+        self.expected = dt_from.replace(tzinfo=ZoneInfo("US/Pacific")).astimezone(
             tz.tzutc()
         )
 
@@ -592,7 +598,7 @@ def test_to_israel_same_offset(self):
     # issue 315
     def test_anchorage_dst(self):
         before = arrow.Arrow(2016, 3, 13, 1, 59, tzinfo="America/Anchorage")
-        after = arrow.Arrow(2016, 3, 13, 2, 1, tzinfo="America/Anchorage")
+        after = arrow.Arrow(2016, 3, 13, 3, 1, tzinfo="America/Anchorage")
 
         assert before.utcoffset() != after.utcoffset()
 
@@ -652,9 +658,9 @@ def test_replace(self):
     def test_replace_tzinfo(self):
         arw = arrow.Arrow.utcnow().to("US/Eastern")
 
-        result = arw.replace(tzinfo=tz.gettz("US/Pacific"))
+        result = arw.replace(tzinfo=ZoneInfo("US/Pacific"))
 
-        assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific"))
+        assert result == arw.datetime.replace(tzinfo=ZoneInfo("US/Pacific"))
 
     def test_replace_fold(self):
         before = arrow.Arrow(2017, 11, 5, 1, tzinfo="America/New_York")
@@ -863,12 +869,12 @@ def test_shift_negative_imaginary(self):
         )
 
     def test_shift_with_imaginary_check(self):
-        dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern"))
+        dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern"))
         shifted = dt.shift(hours=1)
         assert shifted.datetime.hour == 3
 
     def test_shift_without_imaginary_check(self):
-        dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern"))
+        dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern"))
         shifted = dt.shift(hours=1, check_imaginary=False)
         assert shifted.datetime.hour == 3
 
@@ -1033,38 +1039,38 @@ def test_naive_tz(self):
         )
 
         for r in result:
-            assert r.tzinfo == tz.gettz("US/Pacific")
+            assert r.tzinfo == ZoneInfo("US/Pacific")
 
     def test_aware_same_tz(self):
         result = arrow.Arrow.range(
             "day",
-            arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")),
-            arrow.Arrow(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")),
+            arrow.Arrow(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")),
+            arrow.Arrow(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")),
         )
 
         for r in result:
-            assert r.tzinfo == tz.gettz("US/Pacific")
+            assert r.tzinfo == ZoneInfo("US/Pacific")
 
     def test_aware_different_tz(self):
         result = arrow.Arrow.range(
             "day",
-            datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")),
-            datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")),
+            datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")),
+            datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")),
         )
 
         for r in result:
-            assert r.tzinfo == tz.gettz("US/Eastern")
+            assert r.tzinfo == ZoneInfo("US/Eastern")
 
     def test_aware_tz(self):
         result = arrow.Arrow.range(
             "day",
-            datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")),
-            datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")),
-            tz=tz.gettz("US/Central"),
+            datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")),
+            datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")),
+            tz=ZoneInfo("US/Central"),
         )
 
         for r in result:
-            assert r.tzinfo == tz.gettz("US/Central")
+            assert r.tzinfo == ZoneInfo("US/Central")
 
     def test_imaginary(self):
         # issue #72, avoid duplication in utc column
@@ -1345,7 +1351,7 @@ def test_second(self):
         ]
 
     def test_naive_tz(self):
-        tzinfo = tz.gettz("US/Pacific")
+        tzinfo = ZoneInfo("US/Pacific")
 
         result = arrow.Arrow.span_range(
             "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific"
@@ -1356,7 +1362,7 @@ def test_naive_tz(self):
             assert c.tzinfo == tzinfo
 
     def test_aware_same_tz(self):
-        tzinfo = tz.gettz("US/Pacific")
+        tzinfo = ZoneInfo("US/Pacific")
 
         result = arrow.Arrow.span_range(
             "hour",
@@ -1369,8 +1375,8 @@ def test_aware_same_tz(self):
             assert c.tzinfo == tzinfo
 
     def test_aware_different_tz(self):
-        tzinfo1 = tz.gettz("US/Pacific")
-        tzinfo2 = tz.gettz("US/Eastern")
+        tzinfo1 = ZoneInfo("US/Pacific")
+        tzinfo2 = ZoneInfo("US/Eastern")
 
         result = arrow.Arrow.span_range(
             "hour",
@@ -1385,14 +1391,14 @@ def test_aware_different_tz(self):
     def test_aware_tz(self):
         result = arrow.Arrow.span_range(
             "hour",
-            datetime(2013, 1, 1, 0, tzinfo=tz.gettz("US/Eastern")),
-            datetime(2013, 1, 1, 2, 59, tzinfo=tz.gettz("US/Eastern")),
+            datetime(2013, 1, 1, 0, tzinfo=ZoneInfo("US/Eastern")),
+            datetime(2013, 1, 1, 2, 59, tzinfo=ZoneInfo("US/Eastern")),
             tz="US/Central",
         )
 
         for f, c in result:
-            assert f.tzinfo == tz.gettz("US/Central")
-            assert c.tzinfo == tz.gettz("US/Central")
+            assert f.tzinfo == ZoneInfo("US/Central")
+            assert c.tzinfo == ZoneInfo("US/Central")
 
     def test_bounds_param_is_passed(self):
         result = list(
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 0ee9c4e0..056cee41 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -5,6 +5,11 @@
 import pytest
 from dateutil import tz
 
+try:
+    from zoneinfo import ZoneInfo
+except ImportError:
+    from backports.zoneinfo import ZoneInfo
+
 from arrow import Arrow
 from arrow.parser import ParserError
 
@@ -15,7 +20,7 @@
 class TestGet:
     def test_no_args(self):
         assert_datetime_equality(
-            self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=tz.tzutc())
+            self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
         )
 
     def test_timestamp_one_arg_no_arg(self):
@@ -31,12 +36,12 @@ def test_one_arg_none(self):
     def test_struct_time(self):
         assert_datetime_equality(
             self.factory.get(time.gmtime()),
-            datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()),
+            datetime.now(timezone.utc).replace(tzinfo=timezone.utc),
         )
 
     def test_one_arg_timestamp(self):
         int_timestamp = int(time.time())
-        timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace(
+        timestamp_dt = datetime.fromtimestamp(int_timestamp, timezone.utc).replace(
             tzinfo=tz.tzutc()
         )
 
@@ -46,7 +51,7 @@ def test_one_arg_timestamp(self):
             self.factory.get(str(int_timestamp))
 
         float_timestamp = time.time()
-        timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace(
+        timestamp_dt = datetime.fromtimestamp(float_timestamp, timezone.utc).replace(
             tzinfo=tz.tzutc()
         )
 
@@ -66,19 +71,19 @@ def test_one_arg_expanded_timestamp(self):
         microsecond_timestamp = 1591328104308505
 
         # Regression test for issue #796
-        assert self.factory.get(millisecond_timestamp) == datetime.utcfromtimestamp(
-            1591328104.308
+        assert self.factory.get(millisecond_timestamp) == datetime.fromtimestamp(
+            1591328104.308, timezone.utc
         ).replace(tzinfo=tz.tzutc())
-        assert self.factory.get(microsecond_timestamp) == datetime.utcfromtimestamp(
-            1591328104.308505
+        assert self.factory.get(microsecond_timestamp) == datetime.fromtimestamp(
+            1591328104.308505, timezone.utc
         ).replace(tzinfo=tz.tzutc())
 
     def test_one_arg_timestamp_with_tzinfo(self):
         timestamp = time.time()
         timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone(
-            tz.gettz("US/Pacific")
+            ZoneInfo("US/Pacific")
         )
-        timezone = tz.gettz("US/Pacific")
+        timezone = ZoneInfo("US/Pacific")
 
         assert_datetime_equality(
             self.factory.get(timestamp, tzinfo=timezone), timestamp_dt
@@ -105,11 +110,11 @@ def test_one_arg_tzinfo(self):
         self.expected = (
             datetime.now(timezone.utc)
             .replace(tzinfo=tz.tzutc())
-            .astimezone(tz.gettz("US/Pacific"))
+            .astimezone(ZoneInfo("US/Pacific"))
         )
 
         assert_datetime_equality(
-            self.factory.get(tz.gettz("US/Pacific")), self.expected
+            self.factory.get(ZoneInfo("US/Pacific")), self.expected
         )
 
     # regression test for issue #658
@@ -125,18 +130,18 @@ def test_kwarg_tzinfo(self):
         self.expected = (
             datetime.now(timezone.utc)
             .replace(tzinfo=tz.tzutc())
-            .astimezone(tz.gettz("US/Pacific"))
+            .astimezone(ZoneInfo("US/Pacific"))
         )
 
         assert_datetime_equality(
-            self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected
+            self.factory.get(tzinfo=ZoneInfo("US/Pacific")), self.expected
         )
 
     def test_kwarg_tzinfo_string(self):
         self.expected = (
             datetime.now(timezone.utc)
             .replace(tzinfo=tz.tzutc())
-            .astimezone(tz.gettz("US/Pacific"))
+            .astimezone(ZoneInfo("US/Pacific"))
         )
 
         assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected)
@@ -168,7 +173,7 @@ def test_one_arg_datetime_tzinfo_kwarg(self):
 
         result = self.factory.get(dt, tzinfo="America/Chicago")
 
-        expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago"))
+        expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago"))
 
         assert_datetime_equality(result._datetime, expected)
 
@@ -177,7 +182,7 @@ def test_one_arg_arrow_tzinfo_kwarg(self):
 
         result = self.factory.get(arw, tzinfo="America/Chicago")
 
-        expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago"))
+        expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago"))
 
         assert_datetime_equality(result._datetime, expected)
 
@@ -186,7 +191,7 @@ def test_one_arg_date_tzinfo_kwarg(self):
 
         result = self.factory.get(da, tzinfo="America/Chicago")
 
-        expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago"))
+        expected = Arrow(2021, 4, 29, tzinfo=ZoneInfo("America/Chicago"))
 
         assert result.date() == expected.date()
         assert result.tzinfo == expected.tzinfo
@@ -201,9 +206,7 @@ def test_one_arg_iso_calendar_tzinfo_kwarg(self):
     def test_one_arg_iso_str(self):
         dt = datetime.now(timezone.utc)
 
-        assert_datetime_equality(
-            self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc())
-        )
+        assert_datetime_equality(self.factory.get(dt.isoformat()), dt)
 
     def test_one_arg_iso_calendar(self):
         pairs = [
@@ -252,24 +255,24 @@ def test_one_arg_decimal(self):
         )
 
     def test_two_args_datetime_tzinfo(self):
-        result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific"))
+        result = self.factory.get(datetime(2013, 1, 1), ZoneInfo("US/Pacific"))
 
-        assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific"))
 
     def test_two_args_datetime_tz_str(self):
         result = self.factory.get(datetime(2013, 1, 1), "US/Pacific")
 
-        assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific"))
 
     def test_two_args_date_tzinfo(self):
-        result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific"))
+        result = self.factory.get(date(2013, 1, 1), ZoneInfo("US/Pacific"))
 
-        assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific"))
 
     def test_two_args_date_tz_str(self):
         result = self.factory.get(date(2013, 1, 1), "US/Pacific")
 
-        assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific"))
+        assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific"))
 
     def test_two_args_datetime_other(self):
         with pytest.raises(TypeError):
@@ -285,10 +288,10 @@ def test_two_args_str_str(self):
         assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc())
 
     def test_two_args_str_tzinfo(self):
-        result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific"))
+        result = self.factory.get("2013-01-01", tzinfo=ZoneInfo("US/Pacific"))
 
         assert_datetime_equality(
-            result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific"))
+            result._datetime, datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific"))
         )
 
     def test_two_args_twitter_format(self):
@@ -362,11 +365,11 @@ def test_locale(self):
 
     def test_locale_kwarg_only(self):
         res = self.factory.get(locale="ja")
-        assert res.tzinfo == tz.tzutc()
+        assert res.tzinfo == timezone.utc
 
     def test_locale_with_tzinfo(self):
-        res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo"))
-        assert res.tzinfo == tz.gettz("Asia/Tokyo")
+        res = self.factory.get(locale="ja", tzinfo=ZoneInfo("Asia/Tokyo"))
+        assert res.tzinfo == ZoneInfo("Asia/Tokyo")
 
 
 @pytest.mark.usefixtures("arrow_factory")
@@ -374,19 +377,19 @@ class TestUtcNow:
     def test_utcnow(self):
         assert_datetime_equality(
             self.factory.utcnow()._datetime,
-            datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()),
+            datetime.now(timezone.utc),
         )
 
 
 @pytest.mark.usefixtures("arrow_factory")
 class TestNow:
     def test_no_tz(self):
-        assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal()))
+        assert_datetime_equality(self.factory.now(), datetime.now().astimezone())
 
     def test_tzinfo(self):
         assert_datetime_equality(
-            self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST"))
+            self.factory.now(ZoneInfo("EST")), datetime.now(ZoneInfo("EST"))
         )
 
     def test_tz_str(self):
-        assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST")))
+        assert_datetime_equality(self.factory.now("EST"), datetime.now(ZoneInfo("EST")))
diff --git a/tests/test_formatter.py b/tests/test_formatter.py
index 53868288..ff3fea28 100644
--- a/tests/test_formatter.py
+++ b/tests/test_formatter.py
@@ -1,7 +1,7 @@
 try:
-    import zoneinfo
+    from zoneinfo import ZoneInfo
 except ImportError:
-    from backports import zoneinfo
+    from backports.zoneinfo import ZoneInfo
 
 from datetime import datetime, timezone
 
@@ -118,7 +118,7 @@ def test_timestamp(self):
         assert self.formatter._format_token(dt, "x") == expected
 
     def test_timezone(self):
-        dt = datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.gettz("US/Pacific"))
+        dt = datetime.now(timezone.utc).replace(tzinfo=ZoneInfo("US/Pacific"))
 
         result = self.formatter._format_token(dt, "ZZ")
         assert result == "-07:00" or result == "-08:00"
@@ -129,8 +129,8 @@ def test_timezone(self):
     @pytest.mark.parametrize("full_tz_name", make_full_tz_list())
     def test_timezone_formatter(self, full_tz_name):
         # This test will fail if we use "now" as date as soon as we change from/to DST
-        dt = datetime(1986, 2, 14, tzinfo=zoneinfo.ZoneInfo("UTC")).replace(
-            tzinfo=dateutil_tz.gettz(full_tz_name)
+        dt = datetime(1986, 2, 14, tzinfo=timezone.utc).replace(
+            tzinfo=ZoneInfo(full_tz_name)
         )
         abbreviation = dt.tzname()
 
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 3eb44d16..7038d880 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,11 +1,16 @@
 import calendar
 import os
 import time
-from datetime import datetime, timezone
+from datetime import datetime, timedelta, timezone
 
 import pytest
 from dateutil import tz
 
+try:
+    from zoneinfo import ZoneInfo
+except ImportError:
+    from backports.zoneinfo import ZoneInfo
+
 import arrow
 from arrow import formatter, parser
 from arrow.constants import MAX_TIMESTAMP_US
@@ -319,7 +324,7 @@ def test_parse_tz_zz(self):
 
     @pytest.mark.parametrize("full_tz_name", make_full_tz_list())
     def test_parse_tz_name_zzz(self, full_tz_name):
-        self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name))
+        self.expected = datetime(2013, 1, 1, tzinfo=ZoneInfo(full_tz_name))
         assert (
             self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ")
             == self.expected
@@ -1309,36 +1314,40 @@ def test_midnight_end_day(self):
 @pytest.mark.usefixtures("tzinfo_parser")
 class TestTzinfoParser:
     def test_parse_local(self):
-        assert self.parser.parse("local") == tz.tzlocal()
+        from datetime import datetime
+
+        assert self.parser.parse("local") == datetime.now().astimezone().tzinfo
 
     def test_parse_utc(self):
-        assert self.parser.parse("utc") == tz.tzutc()
-        assert self.parser.parse("UTC") == tz.tzutc()
+        assert self.parser.parse("utc") == timezone.utc
+        assert self.parser.parse("UTC") == timezone.utc
 
     def test_parse_utc_withoffset(self):
-        assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("(UTC-01:00") == tz.tzoffset(None, -3600)
-        assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600)
+        assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("(UTC-01:00") == timezone(timedelta(seconds=-3600))
+        assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600))
         assert self.parser.parse(
             "(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien"
-        ) == tz.tzoffset(None, 3600)
+        ) == timezone(timedelta(seconds=3600))
 
     def test_parse_iso(self):
-        assert self.parser.parse("01:00") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("11:35") == tz.tzoffset(None, 11 * 3600 + 2100)
-        assert self.parser.parse("+01:00") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("-01:00") == tz.tzoffset(None, -3600)
+        assert self.parser.parse("01:00") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("11:35") == timezone(
+            timedelta(seconds=11 * 3600 + 2100)
+        )
+        assert self.parser.parse("+01:00") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("-01:00") == timezone(timedelta(seconds=-3600))
 
-        assert self.parser.parse("0100") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("+0100") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("-0100") == tz.tzoffset(None, -3600)
+        assert self.parser.parse("0100") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("+0100") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("-0100") == timezone(timedelta(seconds=-3600))
 
-        assert self.parser.parse("01") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("+01") == tz.tzoffset(None, 3600)
-        assert self.parser.parse("-01") == tz.tzoffset(None, -3600)
+        assert self.parser.parse("01") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("+01") == timezone(timedelta(seconds=3600))
+        assert self.parser.parse("-01") == timezone(timedelta(seconds=-3600))
 
     def test_parse_str(self):
-        assert self.parser.parse("US/Pacific") == tz.gettz("US/Pacific")
+        assert self.parser.parse("US/Pacific") == ZoneInfo("US/Pacific")
 
     def test_parse_fails(self):
         with pytest.raises(parser.ParserError):
diff --git a/tests/utils.py b/tests/utils.py
index 7a74b7e4..834aee05 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -8,9 +8,29 @@
 def make_full_tz_list():
     dateutil_zones = set(get_zonefile_instance().zones)
     zoneinfo_zones = set(zoneinfo.available_timezones())
-    return dateutil_zones.union(zoneinfo_zones)
+    # Since the tests create ZoneInfo objects, we can only use timezones
+    # that are available in zoneinfo. Filter out any dateutil-only timezones
+    # that are not available in zoneinfo (like Asia/Hanoi which was renamed to Asia/Ho_Chi_Minh)
+    all_zones = dateutil_zones.union(zoneinfo_zones)
+    return {tz for tz in all_zones if tz in zoneinfo_zones}
 
 
 def assert_datetime_equality(dt1, dt2, within=10):
-    assert dt1.tzinfo == dt2.tzinfo
+    # Compare timezone behavior instead of object identity for cross-platform compatibility
+    assert_timezone_equivalence(dt1.tzinfo, dt2.tzinfo, dt1)
     assert abs((dt1 - dt2).total_seconds()) < within
+
+
+def assert_timezone_equivalence(tz1, tz2, dt):
+    # Timezone objects are equivalent
+    if tz1 == tz2:
+        return
+
+    # Compare timezone names
+    assert tz1.tzname(dt) == tz2.tzname(dt)
+
+    # Compare UTC offset and DST behavior at the given datetime
+    assert tz1.utcoffset(dt) == tz2.utcoffset(
+        dt
+    ), f"UTC offset mismatch: {tz1.utcoffset(dt)} != {tz2.utcoffset(dt)}"
+    assert tz1.dst(dt) == tz2.dst(dt), f"DST mismatch: {tz1.dst(dt)} != {tz2.dst(dt)}"