Repository: sdispater/pendulum
Pull request #640 (2022-08-07 17:14:19)

Improve and fix typing
## Pull Request Check List

<!--
This is just a reminder about the most common mistakes. Please make sure that you tick all *appropriate* boxes.  But please read our [contribution guide](https://python-poetry.org/docs/contributing/) at least once, it will save you unnecessary review cycles!
-->

- [ ] Added **tests** for changed code.
- [ ] Updated **documentation** for changed code.

<!--
**Note**: If your Pull Request introduces a new feature or changes the current behavior, it should be based
on the `develop` branch. If it's a bug fix or only a documentation update, it should be based on the `master` branch.

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!
-->



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dfb621b..eb80983 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -15,7 +15,7 @@ repos:
     rev: v2.1.1
     hooks:
       - id: pycln
-        args: [--all]
+        args: [ --all ]
 
   - repo: https://github.com/psf/black
     rev: 22.6.0
@@ -26,7 +26,7 @@ repos:
     rev: 5.10.1
     hooks:
       - id: isort
-        args: [--add-import, from __future__ import annotations]
+        args: [ --add-import, from __future__ import annotations, --lines-after-imports, "-1" ]
 
   - repo: https://github.com/pycqa/flake8
     rev: 5.0.4
@@ -67,3 +67,5 @@ repos:
         exclude: ^build\.py$
         additional_dependencies:
           - pytest>=7.1.2
+          - types-backports
+          - types-python-dateutil
diff --git a/pendulum/__init__.py b/pendulum/__init__.py
index bbe284b..33ff1f0 100644
--- a/pendulum/__init__.py
+++ b/pendulum/__init__.py
@@ -2,8 +2,8 @@ from __future__ import annotations
 
 import datetime as _datetime
 
-from typing import Optional
 from typing import Union
+from typing import cast
 
 from pendulum.__version__ import __version__
 from pendulum.constants import DAYS_PER_WEEK
@@ -40,9 +40,6 @@ from pendulum.helpers import week_starts_at
 from pendulum.parser import parse
 from pendulum.period import Period
 from pendulum.time import Time
-from pendulum.tz import POST_TRANSITION
-from pendulum.tz import PRE_TRANSITION
-from pendulum.tz import TRANSITION_ERROR
 from pendulum.tz import UTC
 from pendulum.tz import local_timezone
 from pendulum.tz import set_local_timezone
@@ -52,7 +49,6 @@ from pendulum.tz import timezones
 from pendulum.tz.timezone import FixedTimezone
 from pendulum.tz.timezone import Timezone
 
-
 _TEST_NOW: DateTime | None = None
 _LOCALE = "en"
 _WEEK_STARTS_AT = MONDAY
@@ -61,7 +57,10 @@ _WEEK_ENDS_AT = SUNDAY
 _formatter = Formatter()
 
 
-def _safe_timezone(obj: str | float | _datetime.tzinfo | Timezone | None) -> Timezone:
+def _safe_timezone(
+    obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None,
+    dt: _datetime.datetime | None = None,
+) -> Timezone | FixedTimezone:
     """
     Creates a timezone instance
     from a string, Timezone, TimezoneInfo or integer offset.
@@ -75,19 +74,24 @@ def _safe_timezone(obj: str | float | _datetime.tzinfo | Timezone | None) -> Tim
     if isinstance(obj, (int, float)):
         obj = int(obj * 60 * 60)
     elif isinstance(obj, _datetime.tzinfo):
+        # zoneinfo
+        if hasattr(obj, "key"):
+            obj = obj.key  # type: ignore
         # pytz
-        if hasattr(obj, "localize"):
-            obj = obj.zone
+        elif hasattr(obj, "localize"):
+            obj = obj.zone  # type: ignore
         elif obj.tzname(None) == "UTC":
             return UTC
         else:
-            offset = obj.utcoffset(None)
+            offset = obj.utcoffset(dt)
 
             if offset is None:
                 offset = _datetime.timedelta(0)
 
             obj = int(offset.total_seconds())
 
+    obj = cast(Union[str, int], obj)
+
     return timezone(obj)
 
 
@@ -100,8 +104,8 @@ def datetime(
     minute: int = 0,
     second: int = 0,
     microsecond: int = 0,
-    tz: str | float | Timezone | None = UTC,
-    fold: int | None = 1,
+    tz: str | float | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
+    fold: int = 1,
     raise_on_unknown_times: bool = False,
 ) -> DateTime:
     """
@@ -146,7 +150,7 @@ def naive(
     minute: int = 0,
     second: int = 0,
     microsecond: int = 0,
-    fold: int | None = 1,
+    fold: int = 1,
 ) -> DateTime:
     """
     Return a naive DateTime.
@@ -168,7 +172,10 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T
     return Time(hour, minute, second, microsecond)
 
 
-def instance(dt: _datetime.datetime, tz: str | Timezone | None = UTC) -> DateTime:
+def instance(
+    dt: _datetime.datetime,
+    tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
+) -> DateTime:
     """
     Create a DateTime instance from a datetime one.
     """
@@ -180,19 +187,18 @@ def instance(dt: _datetime.datetime, tz: str | Timezone | None = UTC) -> DateTim
 
     tz = dt.tzinfo or tz
 
-    # Checking for pytz/tzinfo
-    if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, Timezone):
-        # pytz
-        if hasattr(tz, "localize") and tz.zone:
-            tz = tz.zone
-        else:
-            # We have no sure way to figure out
-            # the timezone name, we fallback
-            # on a fixed offset
-            tz = tz.utcoffset(dt).total_seconds() / 3600
+    if tz is not None:
+        tz = _safe_timezone(tz, dt=dt)
 
     return datetime(
-        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz
+        dt.year,
+        dt.month,
+        dt.day,
+        dt.hour,
+        dt.minute,
+        dt.second,
+        dt.microsecond,
+        tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz),
     )
 
 
@@ -228,12 +234,13 @@ def from_format(
     string: str,
     fmt: str,
     tz: str | Timezone = UTC,
-    locale: str | None = None,  # noqa
+    locale: str | None = None,
 ) -> DateTime:
     """
     Creates a DateTime instance from a specific format.
     """
     parts = _formatter.parse(string, fmt, now(), locale=locale)
+
     if parts["tz"] is None:
         parts["tz"] = tz
 
@@ -288,3 +295,64 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
     Create a Period instance.
     """
     return Period(start, end, absolute=absolute)
+
+
+__all__ = [
+    "__version__",
+    "DAYS_PER_WEEK",
+    "FRIDAY",
+    "HOURS_PER_DAY",
+    "MINUTES_PER_HOUR",
+    "MONDAY",
+    "MONTHS_PER_YEAR",
+    "SATURDAY",
+    "SECONDS_PER_DAY",
+    "SECONDS_PER_HOUR",
+    "SECONDS_PER_MINUTE",
+    "SUNDAY",
+    "THURSDAY",
+    "TUESDAY",
+    "WEDNESDAY",
+    "WEEKS_PER_YEAR",
+    "YEARS_PER_CENTURY",
+    "YEARS_PER_DECADE",
+    "Date",
+    "DateTime",
+    "Duration",
+    "Formatter",
+    "date",
+    "datetime",
+    "duration",
+    "format_diff",
+    "from_format",
+    "from_timestamp",
+    "get_locale",
+    "get_test_now",
+    "has_test_now",
+    "instance",
+    "local",
+    "locale",
+    "naive",
+    "now",
+    "period",
+    "set_locale",
+    "set_test_now",
+    "test",
+    "week_ends_at",
+    "week_starts_at",
+    "parse",
+    "Period",
+    "Time",
+    "UTC",
+    "local_timezone",
+    "set_local_timezone",
+    "test_local_timezone",
+    "time",
+    "timezone",
+    "timezones",
+    "today",
+    "tomorrow",
+    "FixedTimezone",
+    "Timezone",
+    "yesterday",
+]
diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi
new file mode 100644
index 0000000..99a5397
--- /dev/null
+++ b/pendulum/_extensions/_helpers.pyi
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from collections import namedtuple
+from datetime import date
+from datetime import datetime
+
+def days_in_year(year: int) -> int: ...
+def is_leap(year: int) -> bool: ...
+def is_long_year(year: int) -> bool: ...
+def local_time(
+    unix_time: int, utc_offset: int, microseconds: int
+) -> tuple[int, int, int, int, int, int, int]: ...
+
+class PreciseDiff(
+    namedtuple(
+        "PreciseDiff",
+        "years months days " "hours minutes seconds microseconds " "total_days",
+    )
+):
+    years: int
+    months: int
+    days: int
+    hours: int
+    minutes: int
+    seconds: int
+    microseconds: int
+    total_days: int
+
+def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ...
+def timestamp(dt: datetime) -> int: ...
+def week_day(year: int, month: int, day: int) -> int: ...
diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py
index 992ea06..01066a3 100644
--- a/pendulum/_extensions/helpers.py
+++ b/pendulum/_extensions/helpers.py
@@ -4,6 +4,7 @@ import datetime
 import math
 
 from collections import namedtuple
+from typing import cast
 
 from pendulum.constants import DAY_OF_WEEK_TABLE
 from pendulum.constants import DAYS_PER_L_YEAR
@@ -20,6 +21,8 @@ from pendulum.constants import SECS_PER_MIN
 from pendulum.constants import SECS_PER_YEAR
 from pendulum.constants import TM_DECEMBER
 from pendulum.constants import TM_JANUARY
+from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import zoneinfo
 
 
 class PreciseDiff(
@@ -28,7 +31,16 @@ class PreciseDiff(
         "years months days " "hours minutes seconds microseconds " "total_days",
     )
 ):
-    def __repr__(self):
+    years: int
+    months: int
+    days: int
+    hours: int
+    minutes: int
+    seconds: int
+    microseconds: int
+    total_days: int
+
+    def __repr__(self) -> str:
         return (
             f"{self.years} years "
             f"{self.months} months "
@@ -45,7 +57,7 @@ def is_leap(year: int) -> bool:
 
 
 def is_long_year(year: int) -> bool:
-    def p(y):
+    def p(y: int) -> int:
         return y + y // 4 - y // 100 + y // 400
 
     return p(year) % 7 == 4 or p(year - 1) % 7 == 3
@@ -103,14 +115,8 @@ def local_time(
     unix_time: int, utc_offset: int, microseconds: int
 ) -> tuple[int, int, int, int, int, int, int]:
     """
-    Returns a UNIX time as a broken down time
+    Returns a UNIX time as a broken-down time
     for a particular transition type.
-
-    :type unix_time: int
-    :type utc_offset: int
-    :type microseconds: int
-
-    :rtype: tuple
     """
     year = EPOCH_YEAR
     seconds = int(math.floor(unix_time))
@@ -173,7 +179,7 @@ def local_time(
     minute = seconds // SECS_PER_MIN
     second = seconds % SECS_PER_MIN
 
-    return (year, month, day, hour, minute, second, microseconds)
+    return year, month, day, hour, minute, second, microseconds
 
 
 def precise_diff(
@@ -183,20 +189,19 @@ def precise_diff(
     Calculate a precise difference between two datetimes.
 
     :param d1: The first datetime
-    :type d1: datetime.datetime or datetime.date
-
     :param d2: The second datetime
-    :type d2: datetime.datetime or datetime.date
-
-    :rtype: PreciseDiff
     """
     sign = 1
 
     if d1 == d2:
         return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
 
-    tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None
-    tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None
+    tzinfo1: datetime.tzinfo | None = (
+        d1.tzinfo if isinstance(d1, datetime.datetime) else None
+    )
+    tzinfo2: datetime.tzinfo | None = (
+        d2.tzinfo if isinstance(d2, datetime.datetime) else None
+    )
 
     if (
         tzinfo1 is None
@@ -227,22 +232,8 @@ def precise_diff(
     # Trying to figure out the timezone names
     # If we can't find them, we assume different timezones
     if tzinfo1 and tzinfo2:
-        if hasattr(tzinfo1, "key"):
-            # zoneinfo timezone
-            tz1 = tzinfo1.key
-        elif hasattr(tzinfo1, "name"):
-            # Pendulum timezone
-            tz1 = tzinfo1.name
-        elif hasattr(tzinfo1, "zone"):
-            # pytz timezone
-            tz1 = tzinfo1.zone
-
-        if hasattr(tzinfo2, "key"):
-            tz2 = tzinfo2.key
-        elif hasattr(tzinfo2, "name"):
-            tz2 = tzinfo2.name
-        elif hasattr(tzinfo2, "zone"):
-            tz2 = tzinfo2.zone
+        tz1 = _get_tzinfo_name(tzinfo1)
+        tz2 = _get_tzinfo_name(tzinfo2)
 
         in_same_tz = tz1 == tz2 and tz1 is not None
 
@@ -354,3 +345,20 @@ def _day_number(year: int, month: int, day: int) -> int:
         + (month * 306 + 5) // 10
         + (day - 1)
     )
+
+
+def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None:
+    if tzinfo is None:
+        return None
+
+    if hasattr(tzinfo, "key"):
+        # zoneinfo timezone
+        return cast(str, cast(zoneinfo.ZoneInfo, tzinfo).key)
+    elif hasattr(tzinfo, "name"):
+        # Pendulum timezone
+        return cast(Timezone, tzinfo).name
+    elif hasattr(tzinfo, "zone"):
+        # pytz timezone
+        return tzinfo.zone  # type: ignore
+
+    return None
diff --git a/pendulum/constants.py b/pendulum/constants.py
index c7b3876..a3d2a18 100644
--- a/pendulum/constants.py
+++ b/pendulum/constants.py
@@ -1,7 +1,6 @@
 # The day constants
 from __future__ import annotations
 
-
 SUNDAY = 0
 MONDAY = 1
 TUESDAY = 2
diff --git a/pendulum/date.py b/pendulum/date.py
index 925f7a8..f130349 100644
--- a/pendulum/date.py
+++ b/pendulum/date.py
@@ -4,7 +4,11 @@ import calendar
 import math
 
 from datetime import date
+from datetime import datetime
 from datetime import timedelta
+from typing import NoReturn
+from typing import cast
+from typing import overload
 
 import pendulum
 
@@ -20,12 +24,13 @@ from pendulum.constants import YEARS_PER_CENTURY
 from pendulum.constants import YEARS_PER_DECADE
 from pendulum.exceptions import PendulumException
 from pendulum.helpers import add_duration
+from pendulum.helpers import get_test_now
+from pendulum.helpers import has_test_now
 from pendulum.mixins.default import FormattableMixin
 from pendulum.period import Period
 
 
 class Date(FormattableMixin, date):
-
     # Names of days of the week
     _days = {
         SUNDAY: "Sunday",
@@ -41,54 +46,52 @@ class Date(FormattableMixin, date):
 
     # Getters/Setters
 
-    def set(self, year=None, month=None, day=None):
+    def set(
+        self, year: int | None = None, month: int | None = None, day: int | None = None
+    ) -> Date:
         return self.replace(year=year, month=month, day=day)
 
     @property
-    def day_of_week(self):
+    def day_of_week(self) -> int:
         """
         Returns the day of the week (0-6).
-
-        :rtype: int
         """
         return self.isoweekday() % 7
 
     @property
-    def day_of_year(self):
+    def day_of_year(self) -> int:
         """
         Returns the day of the year (1-366).
-
-        :rtype: int
         """
         k = 1 if self.is_leap_year() else 2
 
         return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
 
     @property
-    def week_of_year(self):
+    def week_of_year(self) -> int:
         return self.isocalendar()[1]
 
     @property
-    def days_in_month(self):
+    def days_in_month(self) -> int:
         return calendar.monthrange(self.year, self.month)[1]
 
     @property
-    def week_of_month(self):
+    def week_of_month(self) -> int:
         first_day_of_month = self.replace(day=1)
 
         return self.week_of_year - first_day_of_month.week_of_year + 1
 
     @property
-    def age(self):
+    def age(self) -> int:
         return self.diff(abs=False).in_years()
 
     @property
-    def quarter(self):
+    def quarter(self) -> int:
         return int(math.ceil(self.month / 3))
 
     # String Formatting
 
-    def to_date_string(self):
+    def to_date_string(self) -> str:
         """
         Format the instance as date.
 
@@ -96,7 +99,7 @@ class Date(FormattableMixin, date):
         """
         return self.strftime("%Y-%m-%d")
 
-    def to_formatted_date_string(self):
+    def to_formatted_date_string(self) -> str:
         """
         Format the instance as a readable date.
 
@@ -104,19 +107,14 @@ class Date(FormattableMixin, date):
         """
         return self.strftime("%b %d, %Y")
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
 
     # COMPARISONS
 
-    def closest(self, dt1, dt2):
+    def closest(self, dt1: date, dt2: date) -> Date:
         """
         Get the closest date from the instance.
-
-        :type dt1: Date or date
-        :type dt2: Date or date
-
-        :rtype: Date
         """
         dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
         dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
@@ -126,14 +124,9 @@ class Date(FormattableMixin, date):
 
         return dt2
 
-    def farthest(self, dt1, dt2):
+    def farthest(self, dt1: date, dt2: date) -> Date:
         """
         Get the farthest date from the instance.
-
-        :type dt1: Date or date
-        :type dt2: Date or date
-
-        :rtype: Date
         """
         dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
         dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
@@ -143,57 +136,43 @@ class Date(FormattableMixin, date):
 
         return dt2
 
-    def is_future(self):
+    def is_future(self) -> bool:
         """
         Determines if the instance is in the future, ie. greater than now.
-
-        :rtype: bool
         """
         return self > self.today()
 
-    def is_past(self):
+    def is_past(self) -> bool:
         """
         Determines if the instance is in the past, ie. less than now.
-
-        :rtype: bool
         """
         return self < self.today()
 
-    def is_leap_year(self):
+    def is_leap_year(self) -> bool:
         """
         Determines if the instance is a leap year.
-
-        :rtype: bool
         """
         return calendar.isleap(self.year)
 
-    def is_long_year(self):
+    def is_long_year(self) -> bool:
         """
         Determines if the instance is a long year
 
         See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
-
-        :rtype: bool
         """
         return Date(self.year, 12, 28).isocalendar()[1] == 53
 
-    def is_same_day(self, dt):
+    def is_same_day(self, dt: date) -> bool:
         """
         Checks if the passed in date is the same day as the instance current day.
-
-        :type dt: Date or date
-
-        :rtype: bool
         """
         return self == dt
 
-    def is_anniversary(self, dt=None):
+    def is_anniversary(self, dt: date | None = None) -> bool:
         """
-        Check if its the anniversary.
+        Check if it's the anniversary.
 
         Compares the date/month values of the two dates.
-
-        :rtype: bool
         """
         if dt is None:
             dt = Date.today()
@@ -207,25 +186,18 @@ class Date(FormattableMixin, date):
     # the old name can be completely replaced with the new in one of the future versions
     is_birthday = is_anniversary
 
-    # ADDITIONS AND SUBSTRACTIONS
+    # ADDITIONS AND SUBTRACTIONS
 
-    def add(self, years=0, months=0, weeks=0, days=0):
+    def add(
+        self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+    ) -> Date:
         """
         Add duration to the instance.
 
         :param years: The number of years
-        :type years: int
-
         :param months: The number of months
-        :type months: int
-
         :param weeks: The number of weeks
-        :type weeks: int
-
         :param days: The number of days
-        :type days: int
-
-        :rtype: Date
         """
         dt = add_duration(
             date(self.year, self.month, self.day),
@@ -237,34 +209,24 @@ class Date(FormattableMixin, date):
 
         return self.__class__(dt.year, dt.month, dt.day)
 
-    def subtract(self, years=0, months=0, weeks=0, days=0):
+    def subtract(
+        self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+    ) -> Date:
         """
         Remove duration from the instance.
 
         :param years: The number of years
-        :type years: int
-
         :param months: The number of months
-        :type months: int
-
         :param weeks: The number of weeks
-        :type weeks: int
-
         :param days: The number of days
-        :type days: int
-
-        :rtype: Date
         """
         return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
 
-    def _add_timedelta(self, delta):
+    def _add_timedelta(self, delta: timedelta) -> Date:
         """
         Add timedelta duration to the instance.
 
         :param delta: The timedelta instance
-        :type delta: pendulum.Duration or datetime.timedelta
-
-        :rtype: Date
         """
         if isinstance(delta, pendulum.Duration):
             return self.add(
@@ -276,14 +238,11 @@ class Date(FormattableMixin, date):
 
         return self.add(days=delta.days)
 
-    def _subtract_timedelta(self, delta):
+    def _subtract_timedelta(self, delta: timedelta) -> Date:
         """
         Remove timedelta duration from the instance.
 
         :param delta: The timedelta instance
-        :type delta: pendulum.Duration or datetime.timedelta
-
-        :rtype: Date
         """
         if isinstance(delta, pendulum.Duration):
             return self.subtract(
@@ -295,13 +254,25 @@ class Date(FormattableMixin, date):
 
         return self.subtract(days=delta.days)
 
-    def __add__(self, other):
+    def __add__(self, other: timedelta) -> Date:
         if not isinstance(other, timedelta):
             return NotImplemented
 
         return self._add_timedelta(other)
 
-    def __sub__(self, other):
+    @overload
+    def __sub__(self, delta: timedelta) -> Date:
+        ...
+
+    @overload
+    def __sub__(self, dt: datetime) -> NoReturn:
+        ...
+
+    @overload
+    def __sub__(self, dt: Date) -> Period:
+        ...
+
+    def __sub__(self, other: timedelta | date) -> Date | Period:
         if isinstance(other, timedelta):
             return self._subtract_timedelta(other)
 
@@ -314,23 +285,24 @@ class Date(FormattableMixin, date):
 
     # DIFFERENCES
 
-    def diff(self, dt=None, abs=True):
+    def diff(self, dt: date | None = None, abs: bool = True) -> Period:
         """
         Returns the difference between two Date objects as a Period.
 
-        :type dt: Date or None
-
+        :param dt: The date to compare to (defaults to today)
         :param abs: Whether to return an absolute interval or not
-        :type abs: bool
-
-        :rtype: Period
         """
         if dt is None:
             dt = self.today()
 
         return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs)
 
-    def diff_for_humans(self, other=None, absolute=False, locale=None):
+    def diff_for_humans(
+        self,
+        other: date | None = None,
+        absolute: bool = False,
+        locale: str | None = None,
+    ) -> str:
         """
         Get the difference in a human readable format in the current locale.
 
@@ -350,15 +322,9 @@ class Date(FormattableMixin, date):
         1 day after
         5 months after
 
-        :type other: Date
-
+        :param other: The date to compare to (defaults to today)
         :param absolute: removes time difference modifiers ago, after, etc
-        :type absolute: bool
-
         :param locale: The locale to use for localization
-        :type locale: str
-
-        :rtype: str
         """
         is_now = other is None
 
@@ -371,7 +337,7 @@ class Date(FormattableMixin, date):
 
     # MODIFIERS
 
-    def start_of(self, unit):
+    def start_of(self, unit: str) -> Date:
         """
         Returns a copy of the instance with the time reset
         with the following rules:
@@ -384,16 +350,13 @@ class Date(FormattableMixin, date):
         * century: date to first day of century and time to 00:00:00
 
         :param unit: The unit to reset to
-        :type unit: str
-
-        :rtype: Date
         """
         if unit not in self._MODIFIERS_VALID_UNITS:
             raise ValueError(f'Invalid unit "{unit}" for start_of()')
 
-        return getattr(self, f"_start_of_{unit}")()
+        return cast(Date, getattr(self, f"_start_of_{unit}")())
 
-    def end_of(self, unit):
+    def end_of(self, unit: str) -> Date:
         """
         Returns a copy of the instance with the time reset
         with the following rules:
@@ -405,108 +368,83 @@ class Date(FormattableMixin, date):
         * century: date to last day of century
 
         :param unit: The unit to reset to
-        :type unit: str
-
-        :rtype: Date
         """
         if unit not in self._MODIFIERS_VALID_UNITS:
             raise ValueError(f'Invalid unit "{unit}" for end_of()')
 
-        return getattr(self, f"_end_of_{unit}")()
+        return cast(Date, getattr(self, f"_end_of_{unit}")())
 
-    def _start_of_day(self):
+    def _start_of_day(self) -> Date:
         """
         Compatibility method.
-
-        :rtype: Date
         """
         return self
 
-    def _end_of_day(self):
+    def _end_of_day(self) -> Date:
         """
         Compatibility method
-
-        :rtype: Date
         """
         return self
 
-    def _start_of_month(self):
+    def _start_of_month(self) -> Date:
         """
         Reset the date to the first day of the month.
-
-        :rtype: Date
         """
         return self.set(self.year, self.month, 1)
 
-    def _end_of_month(self):
+    def _end_of_month(self) -> Date:
         """
         Reset the date to the last day of the month.
-
-        :rtype: Date
         """
         return self.set(self.year, self.month, self.days_in_month)
 
-    def _start_of_year(self):
+    def _start_of_year(self) -> Date:
         """
         Reset the date to the first day of the year.
-
-        :rtype: Date
         """
         return self.set(self.year, 1, 1)
 
-    def _end_of_year(self):
+    def _end_of_year(self) -> Date:
         """
         Reset the date to the last day of the year.
-
-        :rtype: Date
         """
         return self.set(self.year, 12, 31)
 
-    def _start_of_decade(self):
+    def _start_of_decade(self) -> Date:
         """
         Reset the date to the first day of the decade.
-
-        :rtype: Date
         """
         year = self.year - self.year % YEARS_PER_DECADE
 
         return self.set(year, 1, 1)
 
-    def _end_of_decade(self):
+    def _end_of_decade(self) -> Date:
         """
         Reset the date to the last day of the decade.
-
-        :rtype: Date
         """
         year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
 
         return self.set(year, 12, 31)
 
-    def _start_of_century(self):
+    def _start_of_century(self) -> Date:
         """
         Reset the date to the first day of the century.
-
-        :rtype: Date
         """
         year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
 
         return self.set(year, 1, 1)
 
-    def _end_of_century(self):
+    def _end_of_century(self) -> Date:
         """
         Reset the date to the last day of the century.
-
-        :rtype: Date
         """
         year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
 
         return self.set(year, 12, 31)
 
-    def _start_of_week(self):
+    def _start_of_week(self) -> Date:
         """
         Reset the date to the first day of the week.
-
-        :rtype: Date
         """
         dt = self
 
@@ -515,11 +453,9 @@ class Date(FormattableMixin, date):
 
         return dt.start_of("day")
 
-    def _end_of_week(self):
+    def _end_of_week(self) -> Date:
         """
         Reset the date to the last day of the week.
-
-        :rtype: Date
         """
         dt = self
 
@@ -528,7 +464,7 @@ class Date(FormattableMixin, date):
 
         return dt.end_of("day")
 
-    def next(self, day_of_week=None):
+    def next(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the next occurrence of a given day of the week.
         If no day_of_week is provided, modify to the next occurrence
@@ -536,9 +472,6 @@ class Date(FormattableMixin, date):
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
 
         :param day_of_week: The next day of week to reset to.
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         if day_of_week is None:
             day_of_week = self.day_of_week
@@ -552,7 +485,7 @@ class Date(FormattableMixin, date):
 
         return dt
 
-    def previous(self, day_of_week=None):
+    def previous(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the previous occurrence of a given day of the week.
         If no day_of_week is provided, modify to the previous occurrence
@@ -560,9 +493,6 @@ class Date(FormattableMixin, date):
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
 
         :param day_of_week: The previous day of week to reset to.
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         if day_of_week is None:
             day_of_week = self.day_of_week
@@ -576,7 +506,7 @@ class Date(FormattableMixin, date):
 
         return dt
 
-    def first_of(self, unit, day_of_week=None):
+    def first_of(self, unit: str, day_of_week: int | None = None) -> Date:
         """
         Returns an instance set to the first occurrence
         of a given day of the week in the current unit.
@@ -586,18 +516,14 @@ class Date(FormattableMixin, date):
         Supported units are month, quarter and year.
 
         :param unit: The unit to use
-        :type unit: str
-
-        :type day_of_week: int or None
-
-        :rtype: Date
+        :param day_of_week: The day of week to reset to.
         """
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        return getattr(self, f"_first_of_{unit}")(day_of_week)
+        return cast(Date, getattr(self, f"_first_of_{unit}")(day_of_week))
 
-    def last_of(self, unit, day_of_week=None):
+    def last_of(self, unit: str, day_of_week: int | None = None) -> Date:
         """
         Returns an instance set to the last occurrence
         of a given day of the week in the current unit.
@@ -607,18 +533,14 @@ class Date(FormattableMixin, date):
         Supported units are month, quarter and year.
 
         :param unit: The unit to use
-        :type unit: str
-
-        :type day_of_week: int or None
-
-        :rtype: Date
+        :param day_of_week: The day of week to reset to.
         """
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        return getattr(self, f"_last_of_{unit}")(day_of_week)
+        return cast(Date, getattr(self, f"_last_of_{unit}")(day_of_week))
 
-    def nth_of(self, unit, nth, day_of_week):
+    def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date:
         """
         Returns a new instance set to the given occurrence
         of a given day of the week in the current unit.
@@ -629,19 +551,14 @@ class Date(FormattableMixin, date):
         Supported units are month, quarter and year.
 
         :param unit: The unit to use
-        :type unit: str
-
-        :type nth: int
-
-        :type day_of_week: int or None
-
-        :rtype: Date
+        :param nth: The occurrence to use
+        :param day_of_week: The day of week to set to.
         """
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        dt = getattr(self, f"_nth_of_{unit}")(nth, day_of_week)
-        if dt is False:
+        dt = cast(Date, getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
+        if not dt:
             raise PendulumException(
                 f"Unable to find occurence {nth}"
                 f" of {self._days[day_of_week]} in {unit}"
@@ -649,16 +566,14 @@ class Date(FormattableMixin, date):
 
         return dt
 
-    def _first_of_month(self, day_of_week):
+    def _first_of_month(self, day_of_week: int) -> Date:
         """
         Modify to the first occurrence of a given day of the week
         in the current month. If no day_of_week is provided,
         modify to the first day of the month. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
 
-        :type day_of_week: int
-
-        :rtype: Date
+        :param day_of_week: The day of week to set to.
         """
         dt = self
 
@@ -676,16 +591,14 @@ class Date(FormattableMixin, date):
 
         return dt.set(day=day_of_month)
 
-    def _last_of_month(self, day_of_week=None):
+    def _last_of_month(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the last occurrence of a given day of the week
         in the current month. If no day_of_week is provided,
         modify to the last day of the month. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
 
-        :type day_of_week: int or None
-
-        :rtype: Date
+        :param day_of_week: The day of week to set to.
         """
         dt = self
 
@@ -703,19 +616,13 @@ class Date(FormattableMixin, date):
 
         return dt.set(day=day_of_month)
 
-    def _nth_of_month(self, nth, day_of_week):
+    def _nth_of_month(self, nth: int, day_of_week: int) -> Date | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current month. If the calculated occurrence is outside,
         the scope of the current month, then return False and no
         modifications are made. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type nth: int
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         if nth == 1:
             return self.first_of("month", day_of_week)
@@ -728,49 +635,35 @@ class Date(FormattableMixin, date):
         if dt.format("YYYY-MM") == check:
             return self.set(day=dt.day)
 
-        return False
+        return None
 
-    def _first_of_quarter(self, day_of_week=None):
+    def _first_of_quarter(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the first occurrence of a given day of the week
         in the current quarter. If no day_of_week is provided,
         modify to the first day of the quarter. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
             "month", day_of_week
         )
 
-    def _last_of_quarter(self, day_of_week=None):
+    def _last_of_quarter(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the last occurrence of a given day of the week
         in the current quarter. If no day_of_week is provided,
         modify to the last day of the quarter. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
 
-    def _nth_of_quarter(self, nth, day_of_week):
+    def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current quarter. If the calculated occurrence is outside,
         the scope of the current quarter, then return False and no
         modifications are made. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type nth: int
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         if nth == 1:
             return self.first_of("quarter", day_of_week)
@@ -783,49 +676,35 @@ class Date(FormattableMixin, date):
             dt = dt.next(day_of_week)
 
         if last_month < dt.month or year != dt.year:
-            return False
+            return None
 
         return self.set(self.year, dt.month, dt.day)
 
-    def _first_of_year(self, day_of_week=None):
+    def _first_of_year(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the first occurrence of a given day of the week
         in the current year. If no day_of_week is provided,
         modify to the first day of the year. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         return self.set(month=1).first_of("month", day_of_week)
 
-    def _last_of_year(self, day_of_week=None):
+    def _last_of_year(self, day_of_week: int | None = None) -> Date:
         """
         Modify to the last occurrence of a given day of the week
         in the current year. If no day_of_week is provided,
         modify to the last day of the year. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
 
-    def _nth_of_year(self, nth, day_of_week):
+    def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current year. If the calculated occurrence is outside,
         the scope of the current year, then return False and no
         modifications are made. Use the supplied consts
         to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
-        :type nth: int
-
-        :type day_of_week: int or None
-
-        :rtype: Date
         """
         if nth == 1:
             return self.first_of("year", day_of_week)
@@ -836,18 +715,14 @@ class Date(FormattableMixin, date):
             dt = dt.next(day_of_week)
 
         if year != dt.year:
-            return False
+            return None
 
         return self.set(self.year, dt.month, dt.day)
 
-    def average(self, dt=None):
+    def average(self, dt: date | None = None) -> Date:
         """
         Modify the current instance to the average
         of a given instance (default now) and the current instance.
-
-        :type dt: Date or date
-
-        :rtype: Date
         """
         if dt is None:
             dt = Date.today()
@@ -857,22 +732,32 @@ class Date(FormattableMixin, date):
     # Native methods override
 
     @classmethod
-    def today(cls):
-        return pendulum.today().date()
+    def today(cls) -> Date:
+        if has_test_now():
+            return cast(pendulum.DateTime, get_test_now()).date()
+
+        dt = date.today()
+
+        return cls(dt.year, dt.month, dt.day)
 
     @classmethod
-    def fromtimestamp(cls, t):
+    def fromtimestamp(cls, t: float) -> Date:
         dt = super().fromtimestamp(t)
 
         return cls(dt.year, dt.month, dt.day)
 
     @classmethod
-    def fromordinal(cls, n):
+    def fromordinal(cls, n: int) -> Date:
         dt = super().fromordinal(n)
 
         return cls(dt.year, dt.month, dt.day)
 
-    def replace(self, year=None, month=None, day=None):
+    def replace(
+        self,
+        year: int | None = None,
+        month: int | None = None,
+        day: int | None = None,
+    ) -> Date:
         year = year if year is not None else self.year
         month = month if month is not None else self.month
         day = day if day is not None else self.day
diff --git a/pendulum/datetime.py b/pendulum/datetime.py
index 03fb28d..1c6872d 100644
--- a/pendulum/datetime.py
+++ b/pendulum/datetime.py
@@ -3,7 +3,12 @@ from __future__ import annotations
 import calendar
 import datetime
 
+from typing import TYPE_CHECKING
+from typing import Any
 from typing import Callable
+from typing import Optional
+from typing import cast
+from typing import overload
 
 import pendulum
 
@@ -37,14 +42,16 @@ from pendulum.tz.timezone import FixedTimezone
 from pendulum.tz.timezone import Timezone
 from pendulum.utils._compat import PY38
 
+if TYPE_CHECKING:
+    from typing import Literal
 
-class DateTime(datetime.datetime, Date):
 
-    EPOCH: DateTime | None = None
+class DateTime(datetime.datetime, Date):
+    EPOCH: DateTime
 
     # Formats
 
-    _FORMATS: dict[str, str | Callable] = {
+    _FORMATS: dict[str, str | Callable[[datetime.datetime], str]] = {
         "atom": ATOM,
         "cookie": COOKIE,
         "iso8601": lambda dt: dt.isoformat(),
@@ -58,8 +65,6 @@ class DateTime(datetime.datetime, Date):
         "w3c": W3C,
     }
 
-    _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC)
-
     _MODIFIERS_VALID_UNITS: list[str] = [
         "second",
         "minute",
@@ -72,6 +77,8 @@ class DateTime(datetime.datetime, Date):
         "century",
     ]
 
+    _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC)
+
     @classmethod
     def create(
         cls,
@@ -82,8 +89,8 @@ class DateTime(datetime.datetime, Date):
         minute: int = 0,
         second: int = 0,
         microsecond: int = 0,
-        tz: str | float | Timezone | None = UTC,
-        fold: int | None = 1,
+        tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC,
+        fold: int = 1,
         raise_on_unknown_times: bool = False,
     ) -> DateTime:
         """
@@ -111,13 +118,25 @@ class DateTime(datetime.datetime, Date):
             fold=dt.fold,
         )
 
+    @overload
     @classmethod
-    def now(cls, tz: str | Timezone | None = None) -> DateTime:
+    def now(cls, tz: datetime.tzinfo | None = None) -> DateTime:
+        ...
+
+    @overload
+    @classmethod
+    def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> DateTime:
+        ...
+
+    @classmethod
+    def now(
+        cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None
+    ) -> DateTime:
         """
         Get a DateTime instance for the current date and time.
         """
         if has_test_now():
-            test_instance = get_test_now()
+            test_instance: DateTime = cast(DateTime, get_test_now())
             _tz = pendulum._safe_timezone(tz)
 
             if tz is not None and _tz != test_instance.timezone:
@@ -151,11 +170,11 @@ class DateTime(datetime.datetime, Date):
         """
         Get a DateTime instance for the current date and time in UTC.
         """
-        return pendulum.now(UTC)
+        return cls.now(UTC)
 
     @classmethod
     def today(cls) -> DateTime:
-        return pendulum.now()
+        return cls.now()
 
     @classmethod
     def strptime(cls, time: str, fmt: str) -> DateTime:
@@ -165,15 +184,15 @@ class DateTime(datetime.datetime, Date):
 
     def set(
         self,
-        year=None,
-        month=None,
-        day=None,
-        hour=None,
-        minute=None,
-        second=None,
-        microsecond=None,
-        tz=None,
-    ):
+        year: int | None = None,
+        month: int | None = None,
+        day: int | None = None,
+        hour: int | None = None,
+        minute: int | None = None,
+        second: int | None = None,
+        microsecond: int | None = None,
+        tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None,
+    ) -> DateTime:
         if year is None:
             year = self.year
         if month is None:
@@ -220,22 +239,27 @@ class DateTime(datetime.datetime, Date):
         return delta.days * SECONDS_PER_DAY + delta.seconds
 
     @property
-    def offset(self) -> int:
+    def offset(self) -> int | None:
         return self.get_offset()
 
     @property
-    def offset_hours(self) -> int:
-        return self.get_offset() / SECONDS_PER_MINUTE / MINUTES_PER_HOUR
+    def offset_hours(self) -> float | None:
+        offset = self.get_offset()
+
+        if offset is None:
+            return None
+
+        return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR
 
     @property
-    def timezone(self) -> Timezone | None:
+    def timezone(self) -> Timezone | FixedTimezone | None:
         if not isinstance(self.tzinfo, (Timezone, FixedTimezone)):
-            return
+            return None
 
         return self.tzinfo
 
     @property
-    def tz(self) -> Timezone | None:
+    def tz(self) -> Timezone | FixedTimezone | None:
         return self.timezone
 
     @property
@@ -243,7 +267,7 @@ class DateTime(datetime.datetime, Date):
         tz = self.timezone
 
         if tz is None:
-            return
+            return None
 
         return tz.name
 
@@ -260,8 +284,12 @@ class DateTime(datetime.datetime, Date):
     def is_dst(self) -> bool:
         return self.dst() != datetime.timedelta()
 
-    def get_offset(self) -> int:
-        return int(self.utcoffset().total_seconds())
+    def get_offset(self) -> int | None:
+        utcoffset = self.utcoffset()
+        if utcoffset is None:
+            return None
+
+        return int(utcoffset.total_seconds())
 
     def date(self) -> Date:
         return Date(self.year, self.month, self.day)
@@ -299,7 +327,7 @@ class DateTime(datetime.datetime, Date):
             hour=hour, minute=minute, second=second, microsecond=microsecond
         )
 
-    def in_timezone(self, tz: str | Timezone) -> DateTime:
+    def in_timezone(self, tz: str | Timezone | FixedTimezone) -> DateTime:
         """
         Set the instance's timezone from a string or object.
         """
@@ -309,9 +337,9 @@ class DateTime(datetime.datetime, Date):
         if not self.timezone:
             dt = dt.replace(fold=1)
 
-        return tz.convert(dt)
+        return cast(DateTime, tz.convert(dt))
 
-    def in_tz(self, tz: str | Timezone) -> DateTime:
+    def in_tz(self, tz: str | Timezone | FixedTimezone) -> DateTime:
         """
         Set the instance's timezone from a string or object.
         """
@@ -415,11 +443,11 @@ class DateTime(datetime.datetime, Date):
         if fmt not in self._FORMATS:
             raise ValueError(f"Format [{fmt}] is not supported")
 
-        fmt = self._FORMATS[fmt]
-        if callable(fmt):
-            return fmt(self)
+        fmt_value = self._FORMATS[fmt]
+        if callable(fmt_value):
+            return fmt_value(self)
 
-        return self.format(fmt, locale=locale)
+        return self.format(fmt_value, locale=locale)
 
     def __str__(self) -> str:
         return self.isoformat("T")
@@ -449,32 +477,21 @@ class DateTime(datetime.datetime, Date):
         )
 
     # Comparisons
-    def closest(
-        self, dt1: datetime.datetime, dt2: datetime.datetime, *dts: datetime.datetime
-    ) -> DateTime:
+    def closest(self, *dts: datetime.datetime) -> DateTime:  # type: ignore[override]
         """
         Get the farthest date from the instance.
         """
-        dt1 = pendulum.instance(dt1)
-        dt2 = pendulum.instance(dt2)
-        dts = [dt1, dt2] + [pendulum.instance(x) for x in dts]
-        dts = [(abs(self - dt), dt) for dt in dts]
+        pdts = [pendulum.instance(x) for x in dts]
 
-        return min(dts)[1]
+        return min((abs(self - dt), dt) for dt in pdts)[1]
 
-    def farthest(
-        self, dt1: datetime.datetime, dt2: datetime.datetime, *dts: datetime.datetime
-    ) -> DateTime:
+    def farthest(self, *dts: datetime.datetime) -> DateTime:  # type: ignore[override]
         """
         Get the farthest date from the instance.
         """
-        dt1 = pendulum.instance(dt1)
-        dt2 = pendulum.instance(dt2)
+        pdts = [pendulum.instance(x) for x in dts]
 
-        dts = [dt1, dt2] + [pendulum.instance(x) for x in dts]
-        dts = [(abs(self - dt), dt) for dt in dts]
-
-        return max(dts)[1]
+        return max((abs(self - dt), dt) for dt in pdts)[1]
 
     def is_future(self) -> bool:
         """
@@ -499,7 +516,7 @@ class DateTime(datetime.datetime, Date):
             == 53
         )
 
-    def is_same_day(self, dt: datetime.datetime) -> bool:
+    def is_same_day(self, dt: datetime.datetime) -> bool:  # type: ignore[override]
         """
         Checks if the passed in date is the same day
         as the instance current day.
@@ -508,7 +525,9 @@ class DateTime(datetime.datetime, Date):
 
         return self.to_date_string() == dt.to_date_string()
 
-    def is_anniversary(self, dt: datetime.datetime | None = None) -> bool:
+    def is_anniversary(  # type: ignore[override]
+        self, dt: datetime.datetime | None = None
+    ) -> bool:
         """
         Check if its the anniversary.
         Compares the date/month values of the two dates.
@@ -530,7 +549,7 @@ class DateTime(datetime.datetime, Date):
         days: int = 0,
         hours: int = 0,
         minutes: int = 0,
-        seconds: int = 0,
+        seconds: float = 0,
         microseconds: int = 0,
     ) -> DateTime:
         """
@@ -568,7 +587,7 @@ class DateTime(datetime.datetime, Date):
             microseconds=microseconds,
         )
 
-        if units_of_variable_length or self.tzinfo is None:
+        if units_of_variable_length or self.tz is None:
             return DateTime.create(
                 dt.year,
                 dt.month,
@@ -613,7 +632,7 @@ class DateTime(datetime.datetime, Date):
         days: int = 0,
         hours: int = 0,
         minutes: int = 0,
-        seconds: int = 0,
+        seconds: float = 0,
         microseconds: int = 0,
     ) -> DateTime:
         """
@@ -668,7 +687,9 @@ class DateTime(datetime.datetime, Date):
 
     # DIFFERENCES
 
-    def diff(self, dt: DateTime | None = None, abs: bool = True) -> Period:
+    def diff(  # type: ignore[override]
+        self, dt: datetime.datetime | None = None, abs: bool = True
+    ) -> Period:
         """
         Returns the difference between two DateTime objects represented as a Period.
         """
@@ -677,7 +698,7 @@ class DateTime(datetime.datetime, Date):
 
         return Period(self, dt, absolute=abs)
 
-    def diff_for_humans(
+    def diff_for_humans(  # type: ignore[override]
         self,
         other: DateTime | None = None,
         absolute: bool = False,
@@ -730,7 +751,7 @@ class DateTime(datetime.datetime, Date):
         if unit not in self._MODIFIERS_VALID_UNITS:
             raise ValueError(f'Invalid unit "{unit}" for start_of()')
 
-        return getattr(self, f"_start_of_{unit}")()
+        return cast(DateTime, getattr(self, f"_start_of_{unit}")())
 
     def end_of(self, unit: str) -> DateTime:
         """
@@ -750,7 +771,7 @@ class DateTime(datetime.datetime, Date):
         if unit not in self._MODIFIERS_VALID_UNITS:
             raise ValueError(f'Invalid unit "{unit}" for end_of()')
 
-        return getattr(self, f"_end_of_{unit}")()
+        return cast(DateTime, getattr(self, f"_end_of_{unit}")())
 
     def _start_of_second(self) -> DateTime:
         """
@@ -947,7 +968,7 @@ class DateTime(datetime.datetime, Date):
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        return getattr(self, f"_first_of_{unit}")(day_of_week)
+        return cast(DateTime, getattr(self, f"_first_of_{unit}")(day_of_week))
 
     def last_of(self, unit: str, day_of_week: int | None = None) -> DateTime:
         """
@@ -961,7 +982,7 @@ class DateTime(datetime.datetime, Date):
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        return getattr(self, f"_last_of_{unit}")(day_of_week)
+        return cast(DateTime, getattr(self, f"_last_of_{unit}")(day_of_week))
 
     def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime:
         """
@@ -976,8 +997,10 @@ class DateTime(datetime.datetime, Date):
         if unit not in ["month", "quarter", "year"]:
             raise ValueError(f'Invalid unit "{unit}" for first_of()')
 
-        dt = getattr(self, f"_nth_of_{unit}")(nth, day_of_week)
-        if dt is False:
+        dt = cast(
+            Optional[DateTime], getattr(self, f"_nth_of_{unit}")(nth, day_of_week)
+        )
+        if not dt:
             raise PendulumException(
                 f"Unable to find occurence {nth}"
                 f" of {self._days[day_of_week]} in {unit}"
@@ -1031,7 +1054,9 @@ class DateTime(datetime.datetime, Date):
 
         return dt.set(day=day_of_month)
 
-    def _nth_of_month(self, nth: int, day_of_week: int | None = None) -> DateTime:
+    def _nth_of_month(
+        self, nth: int, day_of_week: int | None = None
+    ) -> DateTime | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current month. If the calculated occurrence is outside,
@@ -1050,7 +1075,7 @@ class DateTime(datetime.datetime, Date):
         if dt.format("%Y-%M") == check:
             return self.set(day=dt.day).start_of("day")
 
-        return False
+        return None
 
     def _first_of_quarter(self, day_of_week: int | None = None) -> DateTime:
         """
@@ -1072,19 +1097,15 @@ class DateTime(datetime.datetime, Date):
         """
         return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
 
-    def _nth_of_quarter(self, nth: int, day_of_week: int | None = None) -> DateTime:
+    def _nth_of_quarter(
+        self, nth: int, day_of_week: int | None = None
+    ) -> DateTime | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current quarter. If the calculated occurrence is outside,
         the scope of the current quarter, then return False and no
         modifications are made. Use the supplied consts
         to indicate the desired day_of_week, ex. DateTime.MONDAY.
-
-        :type nth: int
-
-        :type day_of_week: int or None
-
-        :rtype: DateTime
         """
         if nth == 1:
             return self.first_of("quarter", day_of_week)
@@ -1097,7 +1118,7 @@ class DateTime(datetime.datetime, Date):
             dt = dt.next(day_of_week)
 
         if last_month < dt.month or year != dt.year:
-            return False
+            return None
 
         return self.on(self.year, dt.month, dt.day).start_of("day")
 
@@ -1119,7 +1140,7 @@ class DateTime(datetime.datetime, Date):
         """
         return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
 
-    def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime:
+    def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime | None:
         """
         Modify to the given occurrence of a given day of the week
         in the current year. If the calculated occurrence is outside,
@@ -1136,18 +1157,16 @@ class DateTime(datetime.datetime, Date):
             dt = dt.next(day_of_week)
 
         if year != dt.year:
-            return False
+            return None
 
         return self.on(self.year, dt.month, dt.day).start_of("day")
 
-    def average(self, dt: datetime.datetime | None = None) -> DateTime:
+    def average(  # type: ignore[override]
+        self, dt: datetime.datetime | None = None
+    ) -> DateTime:
         """
         Modify the current instance to the average
         of a given instance (default now) and the current instance.
-
-        :type dt: DateTime or datetime
-
-        :rtype: DateTime
         """
         if dt is None:
             dt = self.now(self.tz)
@@ -1157,6 +1176,14 @@ class DateTime(datetime.datetime, Date):
             microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2
         )
 
+    @overload  # type: ignore[override]
+    def __sub__(self, other: datetime.timedelta) -> DateTime:
+        ...
+
+    @overload
+    def __sub__(self, other: DateTime) -> Period:
+        ...
+
     def __sub__(
         self, other: datetime.datetime | datetime.timedelta
     ) -> DateTime | Period:
@@ -1214,7 +1241,7 @@ class DateTime(datetime.datetime, Date):
 
             caller = inspect.stack()[1][3]
             if caller == "astimezone":
-                return super().__add__(other)
+                return cast(DateTime, super().__add__(other))
 
         return self._add_timedelta_(other)
 
@@ -1225,19 +1252,28 @@ class DateTime(datetime.datetime, Date):
 
     @classmethod
     def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime:
-        return pendulum.instance(datetime.datetime.fromtimestamp(t, tz=tz), tz=tz)
+        tzinfo = pendulum._safe_timezone(tz)
+
+        return pendulum.instance(
+            datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo
+        )
 
     @classmethod
     def utcfromtimestamp(cls, t: float) -> DateTime:
         return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
 
     @classmethod
-    def fromordinal(cls, n) -> DateTime:
+    def fromordinal(cls, n: int) -> DateTime:
         return pendulum.instance(datetime.datetime.fromordinal(n), tz=None)
 
     @classmethod
-    def combine(cls, date: datetime.date, time: datetime.time) -> DateTime:
-        return pendulum.instance(datetime.datetime.combine(date, time), tz=None)
+    def combine(
+        cls,
+        date: datetime.date,
+        time: datetime.time,
+        tzinfo: datetime.tzinfo | None = None,
+    ) -> DateTime:
+        return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo)
 
     def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime:
         dt = super().astimezone(tz)
@@ -1263,9 +1299,9 @@ class DateTime(datetime.datetime, Date):
         minute: int | None = None,
         second: int | None = None,
         microsecond: int | None = None,
-        tzinfo: bool | datetime.tzinfo | None = True,
+        tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
         fold: int | None = None,
-    ):
+    ) -> DateTime:
         if year is None:
             year = self.year
         if month is None:
@@ -1285,14 +1321,27 @@ class DateTime(datetime.datetime, Date):
         if fold is None:
             fold = self.fold
 
+        if tzinfo is not None:
+            tzinfo = pendulum._safe_timezone(tzinfo)
+
         return DateTime.create(
-            year, month, day, hour, minute, second, microsecond, tz=tzinfo, fold=fold
+            year,
+            month,
+            day,
+            hour,
+            minute,
+            second,
+            microsecond,
+            tz=tzinfo,
+            fold=fold,
         )
 
-    def __getnewargs__(self) -> tuple:
+    def __getnewargs__(self) -> tuple[DateTime]:
         return (self,)
 
-    def _getstate(self, protocol: int = 3) -> tuple:
+    def _getstate(
+        self, protocol: int = 3
+    ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]:
         return (
             self.year,
             self.month,
@@ -1304,13 +1353,21 @@ class DateTime(datetime.datetime, Date):
             self.tzinfo,
         )
 
-    def __reduce__(self) -> tuple:
+    def __reduce__(
+        self,
+    ) -> tuple[
+        type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
+    ]:
         return self.__reduce_ex__(2)
 
-    def __reduce_ex__(self, protocol: int) -> tuple:
+    def __reduce_ex__(  # type: ignore[override]
+        self, protocol: int
+    ) -> tuple[
+        type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
+    ]:
         return self.__class__, self._getstate(protocol)
 
-    def _cmp(self, other: datetime.datetime, **kwargs) -> int:
+    def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int:
         # Fix for pypy which compares using this method
         # which would lead to infinite recursion if we didn't override
         dt = datetime.datetime(
@@ -1328,6 +1385,8 @@ class DateTime(datetime.datetime, Date):
         return 0 if dt == other else 1 if dt > other else -1
 
 
-DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC)
-DateTime.max: DateTime = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC)
-DateTime.EPOCH: DateTime = DateTime(1970, 1, 1)
+DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC)  # type: ignore[misc]
+DateTime.max: DateTime = DateTime(  # type: ignore[misc]
+    9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC
+)
+DateTime.EPOCH: DateTime = DateTime(1970, 1, 1, tzinfo=UTC)  # type: ignore[misc]
diff --git a/pendulum/duration.py b/pendulum/duration.py
index 88414ff..a3a68b1 100644
--- a/pendulum/duration.py
+++ b/pendulum/duration.py
@@ -1,6 +1,8 @@
 from __future__ import annotations
 
 from datetime import timedelta
+from typing import cast
+from typing import overload
 
 import pendulum
 
@@ -11,7 +13,7 @@ from pendulum.constants import US_PER_SECOND
 from pendulum.utils._compat import PYPY
 
 
-def _divide_and_round(a, b):
+def _divide_and_round(a: float, b: float) -> int:
     """divide a by b and round result to the nearest integer
 
     When the ratio is exactly half-way between two integers,
@@ -43,6 +45,15 @@ class Duration(timedelta):
     Provides several improvements over the base class.
     """
 
+    _total: float = 0
+    _years: int = 0
+    _months: int = 0
+    _weeks: int = 0
+    _days: int = 0
+    _remaining_days: int = 0
+    _seconds: int = 0
+    _microseconds: int = 0
+
     _y = None
     _m = None
     _w = None
@@ -54,16 +65,16 @@ class Duration(timedelta):
 
     def __new__(
         cls,
-        days=0,
-        seconds=0,
-        microseconds=0,
-        milliseconds=0,
-        minutes=0,
-        hours=0,
-        weeks=0,
-        years=0,
-        months=0,
-    ):
+        days: float = 0,
+        seconds: float = 0,
+        microseconds: float = 0,
+        milliseconds: float = 0,
+        minutes: float = 0,
+        hours: float = 0,
+        weeks: float = 0,
+        years: float = 0,
+        months: float = 0,
+    ) -> Duration:
         if not isinstance(years, int) or not isinstance(months, int):
             raise ValueError("Float year and months are not supported")
 
@@ -98,21 +109,21 @@ class Duration(timedelta):
 
         return self
 
-    def total_minutes(self):
+    def total_minutes(self) -> float:
         return self.total_seconds() / SECONDS_PER_MINUTE
 
-    def total_hours(self):
+    def total_hours(self) -> float:
         return self.total_seconds() / SECONDS_PER_HOUR
 
-    def total_days(self):
+    def total_days(self) -> float:
         return self.total_seconds() / SECONDS_PER_DAY
 
-    def total_weeks(self):
+    def total_weeks(self) -> float:
         return self.total_days() / 7
 
     if PYPY:
 
-        def total_seconds(self):
+        def total_seconds(self) -> float:
             days = 0
 
             if hasattr(self, "_years"):
@@ -132,29 +143,29 @@ class Duration(timedelta):
             ) / US_PER_SECOND
 
     @property
-    def years(self):
+    def years(self) -> int:
         return self._years
 
     @property
-    def months(self):
+    def months(self) -> int:
         return self._months
 
     @property
-    def weeks(self):
+    def weeks(self) -> int:
         return self._weeks
 
     if PYPY:
 
         @property
-        def days(self):
+        def days(self) -> int:
             return self._years * 365 + self._months * 30 + self._days
 
     @property
-    def remaining_days(self):
+    def remaining_days(self) -> int:
         return self._remaining_days
 
     @property
-    def hours(self):
+    def hours(self) -> int:
         if self._h is None:
             seconds = self._seconds
             self._h = 0
@@ -164,7 +175,7 @@ class Duration(timedelta):
         return self._h
 
     @property
-    def minutes(self):
+    def minutes(self) -> int:
         if self._i is None:
             seconds = self._seconds
             self._i = 0
@@ -174,11 +185,11 @@ class Duration(timedelta):
         return self._i
 
     @property
-    def seconds(self):
+    def seconds(self) -> int:
         return self._seconds
 
     @property
-    def remaining_seconds(self):
+    def remaining_seconds(self) -> int:
         if self._s is None:
             self._s = self._seconds
             self._s = abs(self._s) % 60 * self._sign(self._s)
@@ -186,44 +197,39 @@ class Duration(timedelta):
         return self._s
 
     @property
-    def microseconds(self):
+    def microseconds(self) -> int:
         return self._microseconds
 
     @property
-    def invert(self):
+    def invert(self) -> bool:
         if self._invert is None:
             self._invert = self.total_seconds() < 0
 
         return self._invert
 
-    def in_weeks(self):
+    def in_weeks(self) -> int:
         return int(self.total_weeks())
 
-    def in_days(self):
+    def in_days(self) -> int:
         return int(self.total_days())
 
-    def in_hours(self):
+    def in_hours(self) -> int:
         return int(self.total_hours())
 
-    def in_minutes(self):
+    def in_minutes(self) -> int:
         return int(self.total_minutes())
 
-    def in_seconds(self):
+    def in_seconds(self) -> int:
         return int(self.total_seconds())
 
-    def in_words(self, locale=None, separator=" "):
+    def in_words(self, locale: str | None = None, separator: str = " ") -> str:
         """
         Get the current interval in words in the current locale.
 
         Ex: 6 jours 23 heures 58 minutes
 
         :param locale: The locale to use. Defaults to current locale.
-        :type locale: str
-
         :param separator: The separator to use between each unit
-        :type separator: str
-
-        :rtype: str
         """
         periods = [
             ("year", self.years),
@@ -238,46 +244,45 @@ class Duration(timedelta):
         if locale is None:
             locale = pendulum.get_locale()
 
-        locale = pendulum.locale(locale)
+        loaded_locale = pendulum.locale(locale)
+
         parts = []
         for period in periods:
-            unit, count = period
-            if abs(count) > 0:
-                translation = locale.translation(
-                    f"units.{unit}.{locale.plural(abs(count))}"
+            unit, period_count = period
+            if abs(period_count) > 0:
+                translation = loaded_locale.translation(
+                    f"units.{unit}.{loaded_locale.plural(abs(period_count))}"
                 )
-                parts.append(translation.format(count))
+                parts.append(translation.format(period_count))
 
         if not parts:
+            count: int | str = 0
             if abs(self.microseconds) > 0:
-                unit = f"units.second.{locale.plural(1)}"
+                unit = f"units.second.{loaded_locale.plural(1)}"
                 count = f"{abs(self.microseconds) / 1e6:.2f}"
             else:
-                unit = f"units.microsecond.{locale.plural(0)}"
-                count = 0
-            translation = locale.translation(unit)
+                unit = f"units.microsecond.{loaded_locale.plural(0)}"
+            translation = loaded_locale.translation(unit)
             parts.append(translation.format(count))
 
         return separator.join(parts)
 
-    def _sign(self, value):
+    def _sign(self, value: float) -> int:
         if value < 0:
             return -1
 
         return 1
 
-    def as_timedelta(self):
+    def as_timedelta(self) -> timedelta:
         """
         Return the interval as a native timedelta.
-
-        :rtype: timedelta
         """
         return timedelta(seconds=self.total_seconds())
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.in_words()
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         rep = f"{self.__class__.__name__}("
 
         if self._years:
@@ -308,7 +313,7 @@ class Duration(timedelta):
 
         return rep.replace(", )", ")")
 
-    def __add__(self, other):
+    def __add__(self, other: timedelta) -> Duration:
         if isinstance(other, timedelta):
             return self.__class__(seconds=self.total_seconds() + other.total_seconds())
 
@@ -316,13 +321,13 @@ class Duration(timedelta):
 
     __radd__ = __add__
 
-    def __sub__(self, other):
+    def __sub__(self, other: timedelta) -> Duration:
         if isinstance(other, timedelta):
             return self.__class__(seconds=self.total_seconds() - other.total_seconds())
 
         return NotImplemented
 
-    def __neg__(self):
+    def __neg__(self) -> Duration:
         return self.__class__(
             years=-self._years,
             months=-self._months,
@@ -332,10 +337,10 @@ class Duration(timedelta):
             microseconds=-self._microseconds,
         )
 
-    def _to_microseconds(self):
+    def _to_microseconds(self) -> int:
         return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
 
-    def __mul__(self, other):
+    def __mul__(self, other: int | float) -> Duration:
         if isinstance(other, int):
             return self.__class__(
                 years=self._years * other,
@@ -353,13 +358,21 @@ class Duration(timedelta):
 
     __rmul__ = __mul__
 
-    def __floordiv__(self, other):
+    @overload
+    def __floordiv__(self, other: timedelta) -> int:
+        ...
+
+    @overload
+    def __floordiv__(self, other: int) -> Duration:
+        ...
+
+    def __floordiv__(self, other: int | timedelta) -> int | Duration:
         if not isinstance(other, (int, timedelta)):
             return NotImplemented
 
         usec = self._to_microseconds()
         if isinstance(other, timedelta):
-            return usec // other._to_microseconds()
+            return cast(int, usec // other._to_microseconds())  # type: ignore[attr-defined]
 
         if isinstance(other, int):
             return self.__class__(
@@ -370,13 +383,21 @@ class Duration(timedelta):
                 months=self._months // other,
             )
 
-    def __truediv__(self, other):
+    @overload
+    def __truediv__(self, other: timedelta) -> float:
+        ...
+
+    @overload
+    def __truediv__(self, other: float) -> Duration:
+        ...
+
+    def __truediv__(self, other: int | float | timedelta) -> Duration | float:
         if not isinstance(other, (int, float, timedelta)):
             return NotImplemented
 
         usec = self._to_microseconds()
         if isinstance(other, timedelta):
-            return usec / other._to_microseconds()
+            return cast(float, usec / other._to_microseconds())  # type: ignore[attr-defined]
 
         if isinstance(other, int):
             return self.__class__(
@@ -400,17 +421,17 @@ class Duration(timedelta):
 
     __div__ = __floordiv__
 
-    def __mod__(self, other):
+    def __mod__(self, other: timedelta) -> Duration:
         if isinstance(other, timedelta):
-            r = self._to_microseconds() % other._to_microseconds()
+            r = self._to_microseconds() % other._to_microseconds()  # type: ignore[attr-defined]
 
             return self.__class__(0, 0, r)
 
         return NotImplemented
 
-    def __divmod__(self, other):
+    def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
         if isinstance(other, timedelta):
-            q, r = divmod(self._to_microseconds(), other._to_microseconds())
+            q, r = divmod(self._to_microseconds(), other._to_microseconds())  # type: ignore[attr-defined]
 
             return q, self.__class__(0, 0, r)
 
@@ -431,16 +452,16 @@ class AbsoluteDuration(Duration):
 
     def __new__(
         cls,
-        days=0,
-        seconds=0,
-        microseconds=0,
-        milliseconds=0,
-        minutes=0,
-        hours=0,
-        weeks=0,
-        years=0,
-        months=0,
-    ):
+        days: float = 0,
+        seconds: float = 0,
+        microseconds: float = 0,
+        milliseconds: float = 0,
+        minutes: float = 0,
+        hours: float = 0,
+        weeks: float = 0,
+        years: float = 0,
+        months: float = 0,
+    ) -> AbsoluteDuration:
         if not isinstance(years, int) or not isinstance(months, int):
             raise ValueError("Float year and months are not supported")
 
@@ -470,11 +491,11 @@ class AbsoluteDuration(Duration):
 
         return self
 
-    def total_seconds(self):
+    def total_seconds(self) -> float:
         return abs(self._total)
 
     @property
-    def invert(self):
+    def invert(self) -> bool:
         if self._invert is None:
             self._invert = self._total < 0
 
diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py
index 0c6e725..975c409 100644
--- a/pendulum/formatting/__init__.py
+++ b/pendulum/formatting/__init__.py
@@ -2,5 +2,4 @@ from __future__ import annotations
 
 from pendulum.formatting.formatter import Formatter
 
-
 __all__ = ["Formatter"]
diff --git a/pendulum/formatting/difference_formatter.py b/pendulum/formatting/difference_formatter.py
index 4d738fa..dad219d 100644
--- a/pendulum/formatting/difference_formatter.py
+++ b/pendulum/formatting/difference_formatter.py
@@ -4,9 +4,8 @@ import typing as t
 
 from pendulum.locales.locale import Locale
 
-
 if t.TYPE_CHECKING:
-    from pendulum import Period
+    from pendulum import Duration
 
 
 class DifferenceFormatter:
@@ -14,40 +13,29 @@ class DifferenceFormatter:
     Handles formatting differences in text.
     """
 
-    def __init__(self, locale="en"):
+    def __init__(self, locale: str = "en") -> None:
         self._locale = Locale.load(locale)
 
     def format(
         self,
-        diff: Period,
+        diff: Duration,
         is_now: bool = True,
         absolute: bool = False,
-        locale: str | None = None,
+        locale: str | Locale | None = None,
     ) -> str:
         """
         Formats a difference.
 
         :param diff: The difference to format
-        :type diff: pendulum.period.Period
-
         :param is_now: Whether the difference includes now
-        :type is_now: bool
-
         :param absolute: Whether it's an absolute difference or not
-        :type absolute: bool
-
         :param locale: The locale to use
-        :type locale: str or None
-
-        :rtype: str
         """
         if locale is None:
             locale = self._locale
         else:
             locale = Locale.load(locale)
 
-        count = diff.remaining_seconds
-
         if diff.years > 0:
             unit = "year"
             count = diff.years
@@ -89,7 +77,7 @@ class DifferenceFormatter:
             time = locale.get("custom.units.few_second")
             if time is not None:
                 if absolute:
-                    return time
+                    return t.cast(str, time)
 
                 key = "custom"
                 is_future = diff.invert
@@ -104,7 +92,7 @@ class DifferenceFormatter:
                     else:
                         key += ".before"
 
-                return locale.get(key).format(time)
+                return t.cast(str, locale.get(key).format(time))
             else:
                 unit = "second"
                 count = diff.remaining_seconds
@@ -151,8 +139,8 @@ class DifferenceFormatter:
                 else:
                     key += ".before"
 
-                return locale.get(key).format(time)
+                return t.cast(str, locale.get(key).format(time))
 
         key += f".{locale.plural(count)}"
 
-        return locale.get(key).format(count)
+        return t.cast(str, locale.get(key).format(count))
diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py
index 29d1acf..f91d5d9 100644
--- a/pendulum/formatting/formatter.py
+++ b/pendulum/formatting/formatter.py
@@ -2,12 +2,20 @@ from __future__ import annotations
 
 import datetime
 import re
-import typing
+
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import Callable
+from typing import Match
+from typing import Sequence
+from typing import cast
 
 import pendulum
 
 from pendulum.locales.locale import Locale
 
+if TYPE_CHECKING:
+    from pendulum import Timezone
 
 _MATCH_1 = r"\d"
 _MATCH_2 = r"\d\d"
@@ -35,8 +43,7 @@ _MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?"
 
 
 class Formatter:
-
-    _TOKENS = (
+    _TOKENS: str = (
         r"\[([^\[]*)\]|\\(.)|"
         "("
         "Mo|MM?M?M?"
@@ -54,11 +61,11 @@ class Formatter:
         ")"
     )
 
-    _FORMAT_RE = re.compile(_TOKENS)
+    _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS)
 
-    _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
+    _FROM_FORMAT_RE: re.Pattern[str] = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
 
-    _LOCALIZABLE_TOKENS = {
+    _LOCALIZABLE_TOKENS: dict[str, str | Callable[[Locale], Sequence[str]] | None] = {
         "Qo": None,
         "MMMM": "months.wide",
         "MMM": "months.abbreviated",
@@ -83,7 +90,7 @@ class Formatter:
         ),
     }
 
-    _TOKENS_RULES = {
+    _TOKENS_RULES: dict[str, Callable[[pendulum.DateTime], str]] = {
         # Year
         "YYYY": lambda dt: f"{dt.year:d}",
         "YY": lambda dt: f"{dt.year:d}"[2:],
@@ -147,7 +154,7 @@ class Formatter:
         "LLLL": "dddd, MMMM D, YYYY h:mm A",
     }
 
-    _REGEX_TOKENS = {
+    _REGEX_TOKENS: dict[str, str | Sequence[str] | None] = {
         "Y": _MATCH_SIGNED,
         "YY": (_MATCH_1_TO_2, _MATCH_2),
         "YYYY": (_MATCH_1_TO_4, _MATCH_4),
@@ -188,7 +195,7 @@ class Formatter:
         "z": _MATCH_TIMEZONE,
     }
 
-    _PARSE_TOKENS = {
+    _PARSE_TOKENS: dict[str, Callable[[str], Any]] = {
         "YYYY": lambda year: int(year),
         "YY": lambda year: int(year),
         "Q": lambda quarter: int(quarter),
@@ -234,27 +241,17 @@ class Formatter:
         Formats a DateTime instance with a given format and locale.
 
         :param dt: The instance to format
-        :type dt: pendulum.DateTime
-
         :param fmt: The format to use
-        :type fmt: str
-
         :param locale: The locale to use
-        :type locale: str or Locale or None
-
-        :rtype: str
         """
-        if not locale:
-            locale = pendulum.get_locale()
-
-        locale = Locale.load(locale)
+        loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
 
         result = self._FORMAT_RE.sub(
             lambda m: m.group(1)
             if m.group(1)
             else m.group(2)
             if m.group(2)
-            else self._format_token(dt, m.group(3), locale),
+            else self._format_token(dt, m.group(3), loaded_locale),
             fmt,
         )
 
@@ -265,15 +262,8 @@ class Formatter:
         Formats a DateTime instance with a given token and locale.
 
         :param dt: The instance to format
-        :type dt: pendulum.DateTime
-
         :param token: The token to use
-        :type token: str
-
         :param locale: The locale to use
-        :type locale: Locale
-
-        :rtype: str
         """
         if token in self._DATE_FORMATS:
             fmt = locale.get(f"custom.date_formats.{token}")
@@ -306,6 +296,8 @@ class Formatter:
 
             return f"{sign}{hour:02d}{separator}{minute:02d}"
 
+        return token
+
     def _format_localizable_token(
         self, dt: pendulum.DateTime, token: str, locale: Locale
     ) -> str:
@@ -314,26 +306,21 @@ class Formatter:
         with a given localizable token and locale.
 
         :param dt: The instance to format
-        :type dt: pendulum.DateTime
-
         :param token: The token to use
-        :type token: str
-
         :param locale: The locale to use
-        :type locale: Locale
-
-        :rtype: str
         """
         if token == "MMM":
-            return locale.get("translations.months.abbreviated")[dt.month]
+            return cast(str, locale.get("translations.months.abbreviated")[dt.month])
         elif token == "MMMM":
-            return locale.get("translations.months.wide")[dt.month]
+            return cast(str, locale.get("translations.months.wide")[dt.month])
         elif token == "dd":
-            return locale.get("translations.days.short")[dt.day_of_week]
+            return cast(str, locale.get("translations.days.short")[dt.day_of_week])
         elif token == "ddd":
-            return locale.get("translations.days.abbreviated")[dt.day_of_week]
+            return cast(
+                str, locale.get("translations.days.abbreviated")[dt.day_of_week]
+            )
         elif token == "dddd":
-            return locale.get("translations.days.wide")[dt.day_of_week]
+            return cast(str, locale.get("translations.days.wide")[dt.day_of_week])
         elif token == "Do":
             return locale.ordinalize(dt.day)
         elif token == "do":
@@ -353,7 +340,7 @@ class Formatter:
             else:
                 key += ".am"
 
-            return locale.get(key)
+            return cast(str, locale.get(key))
         else:
             return token
 
@@ -363,7 +350,7 @@ class Formatter:
         fmt: str,
         now: pendulum.DateTime,
         locale: str | None = None,
-    ) -> dict[str, typing.Any]:
+    ) -> dict[str, Any]:
         """
         Parses a time string matching a given format as a tuple.
 
@@ -378,12 +365,12 @@ class Formatter:
 
         tokens = self._FROM_FORMAT_RE.findall(escaped_fmt)
         if not tokens:
-            return time
+            raise ValueError("The given time string does not match the given format")
 
         if not locale:
             locale = pendulum.get_locale()
 
-        locale = Locale.load(locale)
+        loaded_locale: Locale = Locale.load(locale)
 
         parsed = {
             "year": None,
@@ -402,19 +389,23 @@ class Formatter:
         }
 
         pattern = self._FROM_FORMAT_RE.sub(
-            lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt
+            lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt
         )
 
         if not re.search("^" + pattern + "$", time):
             raise ValueError(f"String does not match format {fmt}")
 
-        re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time)
+        _get_parsed_values: Callable[
+            [Match[str]], Any
+        ] = lambda m: self._get_parsed_values(m, parsed, loaded_locale, now)
+
+        re.sub(pattern, _get_parsed_values, time)
 
         return self._check_parsed(parsed, now)
 
     def _check_parsed(
-        self, parsed: dict[str, typing.Any], now: pendulum.DateTime
-    ) -> dict[str, typing.Any]:
+        self, parsed: dict[str, Any], now: pendulum.DateTime
+    ) -> dict[str, Any]:
         """
         Checks validity of parsed elements.
 
@@ -422,7 +413,7 @@ class Formatter:
 
         :return: The validated elements.
         """
-        validated = {
+        validated: dict[str, int | Timezone | None] = {
             "year": parsed["year"],
             "month": parsed["month"],
             "day": parsed["day"],
@@ -474,14 +465,17 @@ class Formatter:
             validated["year"] = now.year
 
         if parsed["day_of_year"] is not None:
-            dt = pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}')
+            dt = cast(
+                pendulum.DateTime,
+                pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}'),
+            )
 
             validated["month"] = dt.month
             validated["day"] = dt.day
 
         if parsed["day_of_week"] is not None:
             dt = pendulum.datetime(
-                validated["year"],
+                cast(int, validated["year"]),
                 validated["month"] or now.month,
                 validated["day"] or now.day,
             )
@@ -534,8 +528,8 @@ class Formatter:
 
     def _get_parsed_values(
         self,
-        m: typing.Match[str],
-        parsed: dict[str, typing.Any],
+        m: Match[str],
+        parsed: dict[str, Any],
         locale: Locale,
         now: pendulum.DateTime,
     ) -> None:
@@ -549,7 +543,7 @@ class Formatter:
         self,
         token: str,
         value: str,
-        parsed: dict[str, typing.Any],
+        parsed: dict[str, Any],
         now: pendulum.DateTime,
     ) -> None:
         parsed_token = self._PARSE_TOKENS[token](value)
@@ -610,7 +604,7 @@ class Formatter:
             parsed["tz"] = pendulum.timezone(value)
 
     def _get_parsed_locale_value(
-        self, token: str, value: str, parsed: dict[str, typing.Any], locale: Locale
+        self, token: str, value: str, parsed: dict[str, Any], locale: Locale
     ) -> None:
         if token == "MMMM":
             unit = "month"
@@ -619,7 +613,7 @@ class Formatter:
             unit = "month"
             match = "months.abbreviated"
         elif token == "Do":
-            parsed["day"] = int(re.match(r"(\d+)", value).group(1))
+            parsed["day"] = int(cast(Match[str], re.match(r"(\d+)", value)).group(1))
 
             return
         elif token == "dddd":
@@ -671,16 +665,18 @@ class Formatter:
                 candidates = values(locale)
             else:
                 candidates = tuple(
-                    locale.translation(self._LOCALIZABLE_TOKENS[token]).values()
+                    locale.translation(
+                        cast(str, self._LOCALIZABLE_TOKENS[token])
+                    ).values()
                 )
         else:
-            candidates = self._REGEX_TOKENS[token]
+            candidates = cast(Sequence[str], self._REGEX_TOKENS[token])
 
         if not candidates:
             raise ValueError(f"Unsupported token: {token}")
 
         if not isinstance(candidates, tuple):
-            candidates = (candidates,)
+            candidates = (cast(str, candidates),)
 
         pattern = f'(?P<{token}>{"|".join(candidates)})'
 
diff --git a/pendulum/helpers.py b/pendulum/helpers.py
index 6052e17..f5b68f8 100644
--- a/pendulum/helpers.py
+++ b/pendulum/helpers.py
@@ -19,10 +19,9 @@ from pendulum.constants import DAYS_PER_MONTHS
 from pendulum.formatting.difference_formatter import DifferenceFormatter
 from pendulum.locales.locale import Locale
 
-
 if TYPE_CHECKING:
     # Prevent import cycles
-    from pendulum.period import Period
+    from pendulum.duration import Duration
 
 with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
 
@@ -34,6 +33,7 @@ try:
     if not with_extensions or struct.calcsize("P") == 4:
         raise ImportError()
 
+    from pendulum._extensions._helpers import PreciseDiff
     from pendulum._extensions._helpers import days_in_year
     from pendulum._extensions._helpers import is_leap
     from pendulum._extensions._helpers import is_long_year
@@ -42,55 +42,55 @@ try:
     from pendulum._extensions._helpers import timestamp
     from pendulum._extensions._helpers import week_day
 except ImportError:
-    from pendulum._extensions.helpers import days_in_year  # noqa: F401
+    from pendulum._extensions.helpers import PreciseDiff  # type: ignore[misc]
+    from pendulum._extensions.helpers import days_in_year
     from pendulum._extensions.helpers import is_leap
-    from pendulum._extensions.helpers import is_long_year  # noqa: F401
-    from pendulum._extensions.helpers import local_time  # noqa: F401
-    from pendulum._extensions.helpers import precise_diff  # noqa: F401
-    from pendulum._extensions.helpers import timestamp  # noqa: F401
-    from pendulum._extensions.helpers import week_day  # noqa: F401
-
+    from pendulum._extensions.helpers import is_long_year
+    from pendulum._extensions.helpers import local_time
+    from pendulum._extensions.helpers import precise_diff  # type: ignore[misc]
+    from pendulum._extensions.helpers import timestamp
+    from pendulum._extensions.helpers import week_day
 
 difference_formatter = DifferenceFormatter()
 
 
 @overload
 def add_duration(
-    dt: _DT,
+    dt: datetime,
     years: int = 0,
     months: int = 0,
     weeks: int = 0,
     days: int = 0,
     hours: int = 0,
     minutes: int = 0,
-    seconds: int = 0,
+    seconds: float = 0,
     microseconds: int = 0,
-) -> _DT:
-    pass
+) -> datetime:
+    ...
 
 
 @overload
 def add_duration(
-    dt: _D,
+    dt: date,
     years: int = 0,
     months: int = 0,
     weeks: int = 0,
     days: int = 0,
-) -> _D:
+) -> date:
     pass
 
 
 def add_duration(
-    dt,
-    years=0,
-    months=0,
-    weeks=0,
-    days=0,
-    hours=0,
-    minutes=0,
-    seconds=0,
-    microseconds=0,
-):
+    dt: date | datetime,
+    years: int = 0,
+    months: int = 0,
+    weeks: int = 0,
+    days: int = 0,
+    hours: int = 0,
+    minutes: int = 0,
+    seconds: float = 0,
+    microseconds: int = 0,
+) -> date | datetime:
     """
     Adds a duration to a date/datetime instance.
     """
@@ -112,7 +112,7 @@ def add_duration(
 
     if abs(seconds) > 59:
         s = _sign(seconds)
-        div, mod = divmod(seconds * s, 60)
+        div, mod = divmod(seconds * s, 60)  # type: ignore[assignment]
         seconds = mod * s
         minutes += div * s
 
@@ -160,7 +160,10 @@ def add_duration(
 
 
 def format_diff(
-    diff: Period, is_now: bool = True, absolute: bool = False, locale: str | None = None
+    diff: Duration,
+    is_now: bool = True,
+    absolute: bool = False,
+    locale: str | None = None,
 ) -> str:
     if locale is None:
         locale = get_locale()
@@ -168,7 +171,7 @@ def format_diff(
     return difference_formatter.format(diff, is_now, absolute, locale)
 
 
-def _sign(x):
+def _sign(x: float) -> int:
     return int(copysign(1, x))
 
 
@@ -222,3 +225,26 @@ def week_ends_at(wday: int) -> None:
         raise ValueError("Invalid week day as start of week.")
 
     pendulum._WEEK_ENDS_AT = wday
+
+
+__all__ = [
+    "PreciseDiff",
+    "days_in_year",
+    "is_leap",
+    "is_long_year",
+    "local_time",
+    "precise_diff",
+    "timestamp",
+    "week_day",
+    "add_duration",
+    "format_diff",
+    "test",
+    "set_test_now",
+    "get_test_now",
+    "has_test_now",
+    "locale",
+    "set_locale",
+    "get_locale",
+    "week_starts_at",
+    "week_ends_at",
+]
diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py
index 7f24caf..637509a 100644
--- a/pendulum/locales/locale.py
+++ b/pendulum/locales/locale.py
@@ -1,12 +1,12 @@
-import re
-import sys
+from __future__ import annotations
 
 from importlib import import_module
-from typing import Any
-from typing import Dict
-from typing import Optional
-from typing import Union
+from pathlib import Path
 
+import re
+import sys
+from typing import Any, cast
+from typing import Dict
 
 if sys.version_info >= (3, 9):
     from importlib import resources
@@ -19,15 +19,15 @@ class Locale:
     Represent a specific locale.
     """
 
-    _cache = {}
+    _cache: dict[str, Locale] = {}
 
     def __init__(self, locale: str, data: Any) -> None:
-        self._locale = locale
-        self._data = data
-        self._key_cache = {}
+        self._locale: str = locale
+        self._data: Any = data
+        self._key_cache: dict[str, str] = {}
 
     @classmethod
-    def load(cls, locale: Union[str, "Locale"]) -> "Locale":
+    def load(cls, locale: str | Locale) -> Locale:
         if isinstance(locale, Locale):
             return locale
 
@@ -37,7 +37,7 @@ class Locale:
 
         # Checking locale existence
         actual_locale = locale
-        locale_path = resources.files(__package__).joinpath(actual_locale)
+        locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale))
         while not locale_path.exists():
             if actual_locale == locale:
                 raise ValueError(f"Locale [{locale}] does not exist.")
@@ -58,7 +58,7 @@ class Locale:
         else:
             return locale.lower()
 
-    def get(self, key: str, default: Optional[Any] = None) -> Any:
+    def get(self, key: str, default: Any | None = None) -> Any:
         if key in self._key_cache:
             return self._key_cache[key]
 
@@ -78,10 +78,10 @@ class Locale:
         return self.get(f"translations.{key}")
 
     def plural(self, number: int) -> str:
-        return self._data["plural"](number)
+        return cast(str, self._data["plural"](number))
 
     def ordinal(self, number: int) -> str:
-        return self._data["ordinal"](number)
+        return cast(str, self._data["ordinal"](number))
 
     def ordinalize(self, number: int) -> str:
         ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}")
@@ -91,12 +91,12 @@ class Locale:
 
         return f"{number}{ordinal}"
 
-    def match_translation(self, key: str, value: Any) -> Optional[Dict]:
+    def match_translation(self, key: str, value: Any) -> dict[str, str] | None:
         translations = self.translation(key)
         if value not in translations.values():
             return None
 
-        return {v: k for k, v in translations.items()}[value]
+        return cast(Dict[str, str], {v: k for k, v in translations.items()}[value])
 
     def __repr__(self) -> str:
         return f"{self.__class__.__name__}('{self._locale}')"
diff --git a/pendulum/mixins/default.py b/pendulum/mixins/default.py
index f6f0315..59f985e 100644
--- a/pendulum/mixins/default.py
+++ b/pendulum/mixins/default.py
@@ -2,37 +2,28 @@ from __future__ import annotations
 
 from pendulum.formatting import Formatter
 
-
 _formatter = Formatter()
 
 
 class FormattableMixin:
+    _formatter: Formatter = _formatter
 
-    _formatter = _formatter
-
-    def format(self, fmt, locale=None):
+    def format(self, fmt: str, locale: str | None = None) -> str:
         """
         Formats the instance using the given format.
 
         :param fmt: The format to use
-        :type fmt: str
-
         :param locale: The locale to use
-        :type locale: str or None
-
-        :rtype: str
         """
         return self._formatter.format(self, fmt, locale)
 
-    def for_json(self):
+    def for_json(self) -> str:
         """
-        Methods for automatic json serialization by simplejson
-
-        :rtype: str
+        Methods for automatic json serialization by simplejson.
         """
         return str(self)
 
-    def __format__(self, format_spec):
+    def __format__(self, format_spec: str) -> str:
         if len(format_spec) > 0:
             if "%" in format_spec:
                 return self.strftime(format_spec)
@@ -41,5 +32,5 @@ class FormattableMixin:
 
         return str(self)
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.isoformat()
diff --git a/pendulum/parser.py b/pendulum/parser.py
index dc3ab48..52d6dbd 100644
--- a/pendulum/parser.py
+++ b/pendulum/parser.py
@@ -7,20 +7,19 @@ import pendulum
 
 from pendulum.parsing import _Interval
 from pendulum.parsing import parse as base_parse
-from pendulum.tz import UTC
-
+from pendulum.tz.timezone import UTC
 
 if t.TYPE_CHECKING:
     from pendulum.date import Date
     from pendulum.datetime import DateTime
-    from pendulum.time import Duration
+    from pendulum.duration import Duration
+    from pendulum.period import Period
     from pendulum.time import Time
 
-
 try:
     from pendulum.parsing._iso8601 import Duration as CDuration
 except ImportError:
-    CDuration = None
+    CDuration = None  # type: ignore[misc, assignment]
 
 
 def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
@@ -30,14 +29,11 @@ def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
     return _parse(text, **options)
 
 
-def _parse(text, **options):
+def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | Period:
     """
     Parses a string with the given options.
 
     :param text: The string to parse.
-    :type text: str
-
-    :rtype: mixed
     """
     # Handling special cases
     if text == "now":
@@ -86,7 +82,9 @@ def _parse(text, **options):
                     ),
                 )
 
-            dt = pendulum.instance(parsed.end, tz=options.get("tz", UTC))
+            dt = pendulum.instance(
+                t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC)
+            )
 
             return pendulum.period(
                 dt.subtract(
@@ -103,8 +101,12 @@ def _parse(text, **options):
             )
 
         return pendulum.period(
-            pendulum.instance(parsed.start, tz=options.get("tz", UTC)),
-            pendulum.instance(parsed.end, tz=options.get("tz", UTC)),
+            pendulum.instance(
+                t.cast(datetime.datetime, parsed.start), tz=options.get("tz", UTC)
+            ),
+            pendulum.instance(
+                t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC)
+            ),
         )
 
     if CDuration and isinstance(parsed, CDuration):
diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py
index 5855463..0e64065 100644
--- a/pendulum/parsing/__init__.py
+++ b/pendulum/parsing/__init__.py
@@ -9,22 +9,25 @@ import struct
 from datetime import date
 from datetime import datetime
 from datetime import time
+from typing import Any
+from typing import Optional
+from typing import cast
 
 from dateutil import parser
 
 from pendulum.parsing.exceptions import ParserError
 
-
 with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
 
 try:
     if not with_extensions or struct.calcsize("P") == 4:
         raise ImportError()
 
+    from pendulum.parsing._iso8601 import Duration
     from pendulum.parsing._iso8601 import parse_iso8601
 except ImportError:
-    from pendulum.parsing.iso8601 import parse_iso8601
-
+    from pendulum.duration import Duration  # type: ignore[misc]
+    from pendulum.parsing.iso8601 import parse_iso8601  # type: ignore[misc]
 
 COMMON = re.compile(
     # Date (optional)  # noqa: E800
@@ -52,7 +55,6 @@ COMMON = re.compile(
     re.VERBOSE,
 )
 
-
 DEFAULT_OPTIONS = {
     "day_first": False,
     "year_first": True,
@@ -62,35 +64,31 @@ DEFAULT_OPTIONS = {
 }
 
 
-def parse(text, **options):
+def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
     """
     Parses a string with the given options.
 
     :param text: The string to parse.
-    :type text: str
-
-    :rtype: Parsed
     """
-    _options = copy.copy(DEFAULT_OPTIONS)
+    _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS)
     _options.update(options)
 
     return _normalize(_parse(text, **_options), **_options)
 
 
-def _normalize(parsed, **options):
+def _normalize(
+    parsed: datetime | date | time | _Interval | Duration, **options: Any
+) -> datetime | date | time | _Interval | Duration:
     """
     Normalizes the parsed element.
 
     :param parsed: The parsed elements.
-    :type parsed: Parsed
-
-    :rtype: Parsed
     """
     if options.get("exact"):
         return parsed
 
     if isinstance(parsed, time):
-        now = options["now"] or datetime.now()
+        now = cast(Optional[datetime], options["now"]) or datetime.now()
 
         return datetime(
             now.year,
@@ -107,7 +105,7 @@ def _normalize(parsed, **options):
     return parsed
 
 
-def _parse(text, **options):
+def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
     # Trying to parse ISO8601
     with contextlib.suppress(ValueError):
         return parse_iso8601(text)
@@ -134,14 +132,11 @@ def _parse(text, **options):
     return dt
 
 
-def _parse_common(text, **options):
+def _parse_common(text: str, **options: Any) -> datetime | date | time:
     """
     Tries to parse the string as a common datetime format.
 
     :param text: The string to parse.
-    :type text: str
-
-    :rtype: dict or None
     """
     m = COMMON.match(text)
     has_date = False
@@ -202,13 +197,18 @@ class _Interval:
     Special class to handle ISO 8601 intervals
     """
 
-    def __init__(self, start=None, end=None, duration=None):
+    def __init__(
+        self,
+        start: datetime | None = None,
+        end: datetime | None = None,
+        duration: Duration | None = None,
+    ) -> None:
         self.start = start
         self.end = end
         self.duration = duration
 
 
-def _parse_iso8601_interval(text):
+def _parse_iso8601_interval(text: str) -> _Interval:
     if "/" not in text:
         raise ParserError("Invalid interval")
 
@@ -228,4 +228,6 @@ def _parse_iso8601_interval(text):
         start = parse_iso8601(first)
         end = parse_iso8601(last)
 
-    return _Interval(start, end, duration)
+    return _Interval(
+        cast(datetime, start), cast(datetime, end), cast(Duration, duration)
+    )
diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi
new file mode 100644
index 0000000..b9ce5d4
--- /dev/null
+++ b/pendulum/parsing/_iso8601.pyi
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+from datetime import date
+from datetime import datetime
+from datetime import time
+
+class Duration:
+
+    years: int = 0
+    months: int = 0
+    weeks: int = 0
+    days: int = 0
+    remaining_days: int = 0
+    hours: int = 0
+    minutes: int = 0
+    seconds: int = 0
+    remaining_seconds: int = 0
+    microseconds: int = 0
+
+def parse_iso8601(
+    text: str,
+) -> datetime | date | time | Duration: ...
diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py
index a792ba7..907cf13 100644
--- a/pendulum/parsing/iso8601.py
+++ b/pendulum/parsing/iso8601.py
@@ -3,6 +3,8 @@ from __future__ import annotations
 import datetime
 import re
 
+from typing import cast
+
 from pendulum.constants import HOURS_PER_DAY
 from pendulum.constants import MINUTES_PER_HOUR
 from pendulum.constants import MONTHS_OFFSETS
@@ -16,7 +18,6 @@ from pendulum.parsing.exceptions import ParserError
 from pendulum.tz.timezone import UTC
 from pendulum.tz.timezone import FixedTimezone
 
-
 ISO8601_DT = re.compile(
     # Date (optional)  # noqa: E800
     "^"
@@ -56,7 +57,6 @@ ISO8601_DT = re.compile(
     re.VERBOSE,
 )
 
-
 ISO8601_DURATION = re.compile(
     "^P"  # Duration P indicator
     # Years, months and days (optional)  # noqa: E800
@@ -79,7 +79,9 @@ ISO8601_DURATION = re.compile(
 )
 
 
-def parse_iso8601(text):
+def parse_iso8601(
+    text: str,
+) -> datetime.datetime | datetime.date | datetime.time | Duration:
     """
     ISO 8601 compliant parser.
 
@@ -105,175 +107,178 @@ def parse_iso8601(text):
     minute = 0
     second = 0
     microsecond = 0
-    tzinfo = None
-
-    if m:
-        if m.group("date"):
-            # A date has been specified
-            is_date = True
-
-            if m.group("isocalendar"):
-                # We have a ISO 8601 string defined
-                # by week number
-                if (
-                    m.group("weeksep")
-                    and not m.group("weekdaysep")
-                    and m.group("isoweekday")
-                ):
-                    raise ParserError(f"Invalid date string: {text}")
-
-                if not m.group("weeksep") and m.group("weekdaysep"):
-                    raise ParserError(f"Invalid date string: {text}")
-
-                try:
-                    date = _get_iso_8601_week(
-                        m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
-                    )
-                except ParserError:
-                    raise
-                except ValueError:
-                    raise ParserError(f"Invalid date string: {text}")
-
-                year = date["year"]
-                month = date["month"]
-                day = date["day"]
-            else:
-                # We have a classic date representation
-                year = int(m.group("year"))
+    tzinfo: FixedTimezone | None = None
+
+    if m.group("date"):
+        # A date has been specified
+        is_date = True
+
+        if m.group("isocalendar"):
+            # We have a ISO 8601 string defined
+            # by week number
+            if (
+                m.group("weeksep")
+                and not m.group("weekdaysep")
+                and m.group("isoweekday")
+            ):
+                raise ParserError(f"Invalid date string: {text}")
+
+            if not m.group("weeksep") and m.group("weekdaysep"):
+                raise ParserError(f"Invalid date string: {text}")
+
+            try:
+                date = _get_iso_8601_week(
+                    m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
+                )
+            except ParserError:
+                raise
+            except ValueError:
+                raise ParserError(f"Invalid date string: {text}")
+
+            year = date["year"]
+            month = date["month"]
+            day = date["day"]
+        else:
+            # We have a classic date representation
+            year = int(m.group("year"))
 
-                if not m.group("monthday"):
-                    # No month and day
-                    month = 1
-                    day = 1
-                else:
-                    if m.group("month") and m.group("day"):
-                        # Month and day
-                        if not m.group("daysep") and len(m.group("day")) == 1:
-                            # Ordinal day
-                            ordinal = int(m.group("month") + m.group("day"))
-                            leap = is_leap(year)
-                            months_offsets = MONTHS_OFFSETS[leap]
-
-                            if ordinal > months_offsets[13]:
-                                raise ParserError("Ordinal day is out of range")
-
-                            for i in range(1, 14):
-                                if ordinal <= months_offsets[i]:
-                                    day = ordinal - months_offsets[i - 1]
-                                    month = i - 1
-
-                                    break
-                        else:
-                            month = int(m.group("month"))
-                            day = int(m.group("day"))
+            if not m.group("monthday"):
+                # No month and day
+                month = 1
+                day = 1
+            else:
+                if m.group("month") and m.group("day"):
+                    # Month and day
+                    if not m.group("daysep") and len(m.group("day")) == 1:
+                        # Ordinal day
+                        ordinal = int(m.group("month") + m.group("day"))
+                        leap = is_leap(year)
+                        months_offsets = MONTHS_OFFSETS[leap]
+
+                        if ordinal > months_offsets[13]:
+                            raise ParserError("Ordinal day is out of range")
+
+                        for i in range(1, 14):
+                            if ordinal <= months_offsets[i]:
+                                day = ordinal - months_offsets[i - 1]
+                                month = i - 1
+
+                                break
                     else:
-                        # Only month
-                        if not m.group("monthsep"):
-                            # The date looks like 201207
-                            # which is invalid for a date
-                            # But it might be a time in the form hhmmss
-                            ambiguous_date = True
-
                         month = int(m.group("month"))
-                        day = 1
-
-        if not m.group("time"):
-            # No time has been specified
-            if ambiguous_date:
-                # We can "safely" assume that the ambiguous date
-                # was actually a time in the form hhmmss
-                hhmmss = f"{str(year)}{str(month):0>2}"
+                        day = int(m.group("day"))
+                else:
+                    # Only month
+                    if not m.group("monthsep"):
+                        # The date looks like 201207
+                        # which is invalid for a date
+                        # But it might be a time in the form hhmmss
+                        ambiguous_date = True
+
+                    month = int(m.group("month"))
+                    day = 1
 
-                return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
+    if not m.group("time"):
+        # No time has been specified
+        if ambiguous_date:
+            # We can "safely" assume that the ambiguous date
+            # was actually a time in the form hhmmss
+            hhmmss = f"{str(year)}{str(month):0>2}"
 
-            return datetime.date(year, month, day)
+            return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
 
-        if ambiguous_date:
-            raise ParserError(f"Invalid date string: {text}")
+        return datetime.date(year, month, day)
 
-        if is_date and not m.group("timesep"):
-            raise ParserError(f"Invalid date string: {text}")
+    if ambiguous_date:
+        raise ParserError(f"Invalid date string: {text}")
 
-        if not is_date:
-            is_time = True
+    if is_date and not m.group("timesep"):
+        raise ParserError(f"Invalid date string: {text}")
 
-        # Grabbing hh:mm:ss
-        hour = int(m.group("hour"))
-        minsep = m.group("minsep")
+    if not is_date:
+        is_time = True
 
-        if m.group("minute"):
-            minute = int(m.group("minute"))
-        elif minsep:
-            raise ParserError("Invalid ISO 8601 time part")
+    # Grabbing hh:mm:ss
+    hour = int(m.group("hour"))
+    minsep = m.group("minsep")
 
-        secsep = m.group("secsep")
-        if secsep and not minsep and m.group("minute"):
-            # minute/second separator but no hour/minute separator
-            raise ParserError("Invalid ISO 8601 time part")
+    if m.group("minute"):
+        minute = int(m.group("minute"))
+    elif minsep:
+        raise ParserError("Invalid ISO 8601 time part")
 
-        if m.group("second"):
-            if not secsep and minsep:
-                # No minute/second separator but hour/minute separator
-                raise ParserError("Invalid ISO 8601 time part")
+    secsep = m.group("secsep")
+    if secsep and not minsep and m.group("minute"):
+        # minute/second separator but no hour/minute separator
+        raise ParserError("Invalid ISO 8601 time part")
 
-            second = int(m.group("second"))
-        elif secsep:
+    if m.group("second"):
+        if not secsep and minsep:
+            # No minute/second separator but hour/minute separator
             raise ParserError("Invalid ISO 8601 time part")
 
-        # Grabbing subseconds, if any
-        if m.group("subsecondsection"):
-            # Limiting to 6 chars
-            subsecond = m.group("subsecond")[:6]
+        second = int(m.group("second"))
+    elif secsep:
+        raise ParserError("Invalid ISO 8601 time part")
+
+    # Grabbing subseconds, if any
+    if m.group("subsecondsection"):
+        # Limiting to 6 chars
+        subsecond = m.group("subsecond")[:6]
 
-            microsecond = int(f"{subsecond:0<6}")
+        microsecond = int(f"{subsecond:0<6}")
 
-        # Grabbing timezone, if any
-        tz = m.group("tz")
-        if tz:
-            if tz == "Z":
-                tzinfo = UTC
+    # Grabbing timezone, if any
+    tz = m.group("tz")
+    if tz:
+        if tz == "Z":
+            tzinfo = UTC
+        else:
+            negative = bool(tz.startswith("-"))
+            tz = tz[1:]
+            if ":" not in tz:
+                if len(tz) == 2:
+                    tz = f"{tz}00"
+
+                off_hour = tz[0:2]
+                off_minute = tz[2:4]
             else:
-                negative = bool(tz.startswith("-"))
-                tz = tz[1:]
-                if ":" not in tz:
-                    if len(tz) == 2:
-                        tz = f"{tz}00"
-
-                    off_hour = tz[0:2]
-                    off_minute = tz[2:4]
-                else:
-                    off_hour, off_minute = tz.split(":")
+                off_hour, off_minute = tz.split(":")
 
-                offset = ((int(off_hour) * 60) + int(off_minute)) * 60
+            offset = ((int(off_hour) * 60) + int(off_minute)) * 60
 
-                if negative:
-                    offset = -1 * offset
+            if negative:
+                offset = -1 * offset
 
-                tzinfo = FixedTimezone(offset)
+            tzinfo = FixedTimezone(offset)
 
-        if is_time:
-            return datetime.time(hour, minute, second, microsecond)
+    if is_time:
+        return datetime.time(hour, minute, second, microsecond)
 
-        return datetime.datetime(
-            year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
-        )
+    return datetime.datetime(
+        year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
+    )
 
 
-def _parse_iso8601_duration(text, **options):
+def _parse_iso8601_duration(text: str, **options: str) -> Duration | None:
     m = ISO8601_DURATION.match(text)
     if not m:
-        return
+        return None
 
     years = 0
     months = 0
     weeks = 0
-    days = 0
-    hours = 0
-    minutes = 0
-    seconds = 0
-    microseconds = 0
+    days: int | float = 0
+    hours: int | float = 0
+    minutes: int | float = 0
+    seconds: int | float = 0
+    microseconds: int | float = 0
     fractional = False
 
+    _days: str | float
+    _hour: str | int | None
+    _minutes: str | int | None
+    _seconds: str | int | None
     if m.group("w"):
         # Weeks
         if m.group("ymd") or m.group("hms"):
@@ -289,7 +294,7 @@ def _parse_iso8601_duration(text, **options):
             _weeks, portion = _weeks.split(".")
             weeks = int(_weeks)
             _days = int(portion) / 10 * 7
-            days, hours = int(_days // 1), _days % 1 * HOURS_PER_DAY
+            days, hours = int(_days // 1), int(_days % 1 * HOURS_PER_DAY)
         else:
             weeks = int(_weeks)
 
@@ -359,7 +364,7 @@ def _parse_iso8601_duration(text, **options):
             if fractional:
                 raise ParserError("Invalid duration")
 
-            _hours = _hours.replace(",", ".").replace("H", "")
+            _hours = cast(str, _hours).replace(",", ".").replace("H", "")
 
             if "." in _hours:
                 fractional = True
@@ -374,7 +379,7 @@ def _parse_iso8601_duration(text, **options):
             if fractional:
                 raise ParserError("Invalid duration")
 
-            _minutes = _minutes.replace(",", ".").replace("M", "")
+            _minutes = cast(str, _minutes).replace(",", ".").replace("M", "")
 
             if "." in _minutes:
                 fractional = True
@@ -389,7 +394,7 @@ def _parse_iso8601_duration(text, **options):
             if fractional:
                 raise ParserError("Invalid duration")
 
-            _seconds = _seconds.replace(",", ".").replace("S", "")
+            _seconds = cast(str, _seconds).replace(",", ".").replace("S", "")
 
             if "." in _seconds:
                 _seconds, _microseconds = _seconds.split(".")
@@ -410,7 +415,9 @@ def _parse_iso8601_duration(text, **options):
     )
 
 
-def _get_iso_8601_week(year, week, weekday):
+def _get_iso_8601_week(
+    year: int | str, week: int | str, weekday: int | str
+) -> dict[str, int]:
     if not weekday:
         weekday = 1
     else:
diff --git a/pendulum/period.py b/pendulum/period.py
index 589dafd..eb1b232 100644
--- a/pendulum/period.py
+++ b/pendulum/period.py
@@ -5,6 +5,11 @@ import operator
 from datetime import date
 from datetime import datetime
 from datetime import timedelta
+from typing import TYPE_CHECKING
+from typing import Iterator
+from typing import Union
+from typing import cast
+from typing import overload
 
 import pendulum
 
@@ -12,6 +17,12 @@ from pendulum.constants import MONTHS_PER_YEAR
 from pendulum.duration import Duration
 from pendulum.helpers import precise_diff
 
+if TYPE_CHECKING:
+    from typing import SupportsIndex
+
+    from pendulum.helpers import PreciseDiff
+    from pendulum.locales.locale import Locale  # noqa
+
 
 class Period(Duration):
     """
@@ -19,7 +30,38 @@ class Period(Duration):
     time difference.
     """
 
-    def __new__(cls, start, end, absolute=False):
+    @overload
+    def __new__(
+        cls,
+        start: pendulum.DateTime | datetime,
+        end: pendulum.DateTime | datetime,
+        absolute: bool = False,
+    ) -> Period:
+        ...
+
+    @overload
+    def __new__(
+        cls,
+        start: pendulum.Date | date,
+        end: pendulum.Date | date,
+        absolute: bool = False,
+    ) -> Period:
+        ...
+
+    def __new__(
+        cls,
+        start: pendulum.DateTime | pendulum.Date | datetime | date,
+        end: pendulum.DateTime | pendulum.Date | datetime | date,
+        absolute: bool = False,
+    ) -> Period:
+        if (
+            isinstance(start, datetime)
+            and not isinstance(end, datetime)
+            or not isinstance(start, datetime)
+            and isinstance(end, datetime)
+        ):
+            raise ValueError("Both start and end of a Period must have the same type")
+
         if (
             isinstance(start, datetime)
             and isinstance(end, datetime)
@@ -75,18 +117,26 @@ class Period(Duration):
             and _start.tzinfo is _end.tzinfo
         ):
             if _start.tzinfo is not None:
-                _start = (_start - start.utcoffset()).replace(tzinfo=None)
+                offset = cast(timedelta, cast(datetime, start).utcoffset())
+                _start = (_start - offset).replace(tzinfo=None)
 
             if isinstance(end, datetime) and _end.tzinfo is not None:
-                _end = (_end - end.utcoffset()).replace(tzinfo=None)
+                offset = cast(timedelta, end.utcoffset())
+                _end = (_end - offset).replace(tzinfo=None)
 
-        delta = _end - _start
+        delta: timedelta = _end - _start  # type: ignore[operator]
 
-        return super().__new__(cls, seconds=delta.total_seconds())
+        return cast(Period, super().__new__(cls, seconds=delta.total_seconds()))
 
-    def __init__(self, start, end, absolute=False):
+    def __init__(
+        self,
+        start: pendulum.DateTime | pendulum.Date | datetime | date,
+        end: pendulum.DateTime | pendulum.Date | datetime | date,
+        absolute: bool = False,
+    ) -> None:
         super().__init__()
 
+        _start: pendulum.DateTime | pendulum.Date | datetime | date
         if not isinstance(start, pendulum.Date):
             if isinstance(start, datetime):
                 start = pendulum.instance(start)
@@ -109,6 +159,7 @@ class Period(Duration):
             else:
                 _start = date(start.year, start.month, start.day)
 
+        _end: pendulum.DateTime | pendulum.Date | datetime | date
         if not isinstance(end, pendulum.Date):
             if isinstance(end, datetime):
                 end = pendulum.instance(end)
@@ -140,63 +191,59 @@ class Period(Duration):
                 _end, _start = _start, _end
 
         self._absolute = absolute
-        self._start = start
-        self._end = end
-        self._delta = precise_diff(_start, _end)
+        self._start: pendulum.DateTime | pendulum.Date = start
+        self._end: pendulum.DateTime | pendulum.Date = end
+        self._delta: PreciseDiff = precise_diff(_start, _end)
 
     @property
-    def years(self):
+    def years(self) -> int:
         return self._delta.years
 
     @property
-    def months(self):
+    def months(self) -> int:
         return self._delta.months
 
     @property
-    def weeks(self):
+    def weeks(self) -> int:
         return abs(self._delta.days) // 7 * self._sign(self._delta.days)
 
     @property
-    def days(self):
+    def days(self) -> int:
         return self._days
 
     @property
-    def remaining_days(self):
+    def remaining_days(self) -> int:
         return abs(self._delta.days) % 7 * self._sign(self._days)
 
     @property
-    def hours(self):
+    def hours(self) -> int:
         return self._delta.hours
 
     @property
-    def minutes(self):
+    def minutes(self) -> int:
         return self._delta.minutes
 
     @property
-    def start(self):
+    def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
         return self._start
 
     @property
-    def end(self):
+    def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
         return self._end
 
-    def in_years(self):
+    def in_years(self) -> int:
         """
         Gives the duration of the Period in full years.
-
-        :rtype: int
         """
         return self.years
 
-    def in_months(self):
+    def in_months(self) -> int:
         """
         Gives the duration of the Period in full months.
-
-        :rtype: int
         """
         return self.years * MONTHS_PER_YEAR + self.months
 
-    def in_weeks(self):
+    def in_weeks(self) -> int:
         days = self.in_days()
         sign = 1
 
@@ -205,23 +252,20 @@ class Period(Duration):
 
         return sign * (abs(days) // 7)
 
-    def in_days(self):
+    def in_days(self) -> int:
         return self._delta.total_days
 
-    def in_words(self, locale=None, separator=" "):
+    def in_words(self, locale: str | None = None, separator: str = " ") -> str:
         """
         Get the current interval in words in the current locale.
 
         Ex: 6 jours 23 heures 58 minutes
 
         :param locale: The locale to use. Defaults to current locale.
-        :type locale: str
-
         :param separator: The separator to use between each unit
-        :type separator: str
-
-        :rtype: str
         """
+        from pendulum.locales.locale import Locale  # noqa
+
         periods = [
             ("year", self.years),
             ("month", self.months),
@@ -231,33 +275,32 @@ class Period(Duration):
             ("minute", self.minutes),
             ("second", self.remaining_seconds),
         ]
-
-        if locale is None:
-            locale = pendulum.get_locale()
-
-        locale = pendulum.locale(locale)
+        loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
         parts = []
         for period in periods:
-            unit, count = period
-            if abs(count) > 0:
-                translation = locale.translation(
-                    f"units.{unit}.{locale.plural(abs(count))}"
+            unit, period_count = period
+            if abs(period_count) > 0:
+                translation = loaded_locale.translation(
+                    f"units.{unit}.{loaded_locale.plural(abs(period_count))}"
                 )
-                parts.append(translation.format(count))
+                parts.append(translation.format(period_count))
 
         if not parts:
+            count: str | int = 0
             if abs(self.microseconds) > 0:
-                unit = f"units.second.{locale.plural(1)}"
+                unit = f"units.second.{loaded_locale.plural(1)}"
                 count = f"{abs(self.microseconds) / 1e6:.2f}"
             else:
-                unit = f"units.microsecond.{locale.plural(0)}"
-                count = 0
-            translation = locale.translation(unit)
+                unit = f"units.microsecond.{loaded_locale.plural(0)}"
+
+            translation = loaded_locale.translation(unit)
             parts.append(translation.format(count))
 
         return separator.join(parts)
 
-    def range(self, unit, amount=1):
+    def range(
+        self, unit: str, amount: int = 1
+    ) -> Iterator[pendulum.DateTime | pendulum.Date]:
         method = "add"
         op = operator.le
         if not self._absolute and self.invert:
@@ -268,66 +311,82 @@ class Period(Duration):
 
         i = amount
         while op(start, end):
-            yield start
+            yield cast(Union[pendulum.DateTime, pendulum.Date], start)
 
             start = getattr(self.start, method)(**{unit: i})
 
             i += amount
 
-    def as_interval(self):
+    def as_interval(self) -> Duration:
         """
-        Return the Period as an Duration.
-
-        :rtype: Duration
+        Return the Period as a Duration.
         """
         return Duration(seconds=self.total_seconds())
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]:
         return self.range("days")
 
-    def __contains__(self, item):
+    def __contains__(
+        self, item: datetime | date | pendulum.DateTime | pendulum.Date
+    ) -> bool:
         return self.start <= item <= self.end
 
-    def __add__(self, other):
+    def __add__(self, other: timedelta) -> Duration:
         return self.as_interval().__add__(other)
 
     __radd__ = __add__
 
-    def __sub__(self, other):
+    def __sub__(self, other: timedelta) -> Duration:
         return self.as_interval().__sub__(other)
 
-    def __neg__(self):
+    def __neg__(self) -> Period:
         return self.__class__(self.end, self.start, self._absolute)
 
-    def __mul__(self, other):
+    def __mul__(self, other: int | float) -> Duration:
         return self.as_interval().__mul__(other)
 
     __rmul__ = __mul__
 
-    def __floordiv__(self, other):
+    @overload
+    def __floordiv__(self, other: timedelta) -> int:
+        ...
+
+    @overload
+    def __floordiv__(self, other: int) -> Duration:
+        ...
+
+    def __floordiv__(self, other: int | timedelta) -> int | Duration:
         return self.as_interval().__floordiv__(other)
 
-    def __truediv__(self, other):
-        return self.as_interval().__truediv__(other)
+    __div__ = __floordiv__  # type: ignore[assignment]
+
+    @overload
+    def __truediv__(self, other: timedelta) -> float:
+        ...
 
-    __div__ = __floordiv__
+    @overload
+    def __truediv__(self, other: float) -> Duration:
+        ...
 
-    def __mod__(self, other):
+    def __truediv__(self, other: float | timedelta) -> Duration | float:
+        return self.as_interval().__truediv__(other)
+
+    def __mod__(self, other: timedelta) -> Duration:
         return self.as_interval().__mod__(other)
 
-    def __divmod__(self, other):
+    def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
         return self.as_interval().__divmod__(other)
 
-    def __abs__(self):
-        return self.__class__(self.start, self.end, True)
+    def __abs__(self) -> Period:
+        return self.__class__(self.start, self.end, absolute=True)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f"<Period [{self._start} -> {self._end}]>"
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.__repr__()
 
-    def _cmp(self, other):
+    def _cmp(self, other: timedelta) -> int:
         # Only needed for PyPy
         assert isinstance(other, timedelta)
 
@@ -338,24 +397,48 @@ class Period(Duration):
 
         return 0 if td == other else 1 if td > other else -1
 
-    def _getstate(self, protocol=3):
+    def _getstate(
+        self, protocol: SupportsIndex = 3
+    ) -> tuple[
+        pendulum.DateTime | pendulum.Date | datetime | date,
+        pendulum.DateTime | pendulum.Date | datetime | date,
+        bool,
+    ]:
         start, end = self.start, self.end
 
         if self._invert and self._absolute:
             end, start = start, end
 
-        return (start, end, self._absolute)
-
-    def __reduce__(self):
+        return start, end, self._absolute
+
+    def __reduce__(
+        self,
+    ) -> tuple[
+        type[Period],
+        tuple[
+            pendulum.DateTime | pendulum.Date | datetime | date,
+            pendulum.DateTime | pendulum.Date | datetime | date,
+            bool,
+        ],
+    ]:
         return self.__reduce_ex__(2)
 
-    def __reduce_ex__(self, protocol):
+    def __reduce_ex__(
+        self, protocol: SupportsIndex
+    ) -> tuple[
+        type[Period],
+        tuple[
+            pendulum.DateTime | pendulum.Date | datetime | date,
+            pendulum.DateTime | pendulum.Date | datetime | date,
+            bool,
+        ],
+    ]:
         return self.__class__, self._getstate(protocol)
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash((self.start, self.end, self._absolute))
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         if isinstance(other, Period):
             return (self.start, self.end, self._absolute) == (
                 other.start,
diff --git a/pendulum/time.py b/pendulum/time.py
index 779f70a..f979e25 100644
--- a/pendulum/time.py
+++ b/pendulum/time.py
@@ -1,7 +1,13 @@
 from __future__ import annotations
 
+import datetime
+
 from datetime import time
 from datetime import timedelta
+from typing import TYPE_CHECKING
+from typing import Optional
+from typing import cast
+from typing import overload
 
 import pendulum
 
@@ -12,6 +18,9 @@ from pendulum.duration import AbsoluteDuration
 from pendulum.duration import Duration
 from pendulum.mixins.default import FormattableMixin
 
+if TYPE_CHECKING:
+    from typing import Literal
+
 
 class Time(FormattableMixin, time):
     """
@@ -19,7 +28,7 @@ class Time(FormattableMixin, time):
     """
 
     # String formatting
-    def __repr__(self):
+    def __repr__(self) -> str:
         us = ""
         if self.microsecond:
             us = f", {self.microsecond}"
@@ -35,14 +44,9 @@ class Time(FormattableMixin, time):
 
     # Comparisons
 
-    def closest(self, dt1, dt2):
+    def closest(self, dt1: Time | time, dt2: Time | time) -> Time:
         """
         Get the closest time from the instance.
-
-        :type dt1: Time or time
-        :type dt2: Time or time
-
-        :rtype: Time
         """
         dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
         dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
@@ -52,14 +56,9 @@ class Time(FormattableMixin, time):
 
         return dt2
 
-    def farthest(self, dt1, dt2):
+    def farthest(self, dt1: Time | time, dt2: Time | time) -> Time:
         """
         Get the farthest time from the instance.
-
-        :type dt1: Time or time
-        :type dt2: Time or time
-
-        :rtype: Time
         """
         dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
         dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
@@ -71,23 +70,16 @@ class Time(FormattableMixin, time):
 
     # ADDITIONS AND SUBSTRACTIONS
 
-    def add(self, hours=0, minutes=0, seconds=0, microseconds=0):
+    def add(
+        self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
+    ) -> Time:
         """
         Add duration to the instance.
 
         :param hours: The number of hours
-        :type hours: int
-
         :param minutes: The number of minutes
-        :type minutes: int
-
         :param seconds: The number of seconds
-        :type seconds: int
-
         :param microseconds: The number of microseconds
-        :type microseconds: int
-
-        :rtype: Time
         """
         from pendulum.datetime import DateTime
 
@@ -99,7 +91,9 @@ class Time(FormattableMixin, time):
             .time()
         )
 
-    def subtract(self, hours=0, minutes=0, seconds=0, microseconds=0):
+    def subtract(
+        self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
+    ) -> Time:
         """
         Add duration to the instance.
 
@@ -127,41 +121,43 @@ class Time(FormattableMixin, time):
             .time()
         )
 
-    def add_timedelta(self, delta):
+    def add_timedelta(self, delta: datetime.timedelta) -> Time:
         """
         Add timedelta duration to the instance.
 
         :param delta: The timedelta instance
-        :type delta: datetime.timedelta
-
-        :rtype: Time
         """
         if delta.days:
             raise TypeError("Cannot add timedelta with days to Time.")
 
         return self.add(seconds=delta.seconds, microseconds=delta.microseconds)
 
-    def subtract_timedelta(self, delta):
+    def subtract_timedelta(self, delta: datetime.timedelta) -> Time:
         """
         Remove timedelta duration from the instance.
 
         :param delta: The timedelta instance
-        :type delta: datetime.timedelta
-
-        :rtype: Time
         """
         if delta.days:
             raise TypeError("Cannot subtract timedelta with days to Time.")
 
         return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds)
 
-    def __add__(self, other):
+    def __add__(self, other: datetime.timedelta) -> Time:
         if not isinstance(other, timedelta):
             return NotImplemented
 
         return self.add_timedelta(other)
 
-    def __sub__(self, other):
+    @overload
+    def __sub__(self, other: time) -> pendulum.Duration:
+        ...
+
+    @overload
+    def __sub__(self, other: datetime.timedelta) -> Time:
+        ...
+
+    def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
         if not isinstance(other, (Time, time, timedelta)):
             return NotImplemented
 
@@ -178,7 +174,15 @@ class Time(FormattableMixin, time):
 
         return other.diff(self, False)
 
-    def __rsub__(self, other):
+    @overload
+    def __rsub__(self, other: time) -> pendulum.Duration:
+        ...
+
+    @overload
+    def __rsub__(self, other: datetime.timedelta) -> Time:
+        ...
+
+    def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
         if not isinstance(other, (Time, time)):
             return NotImplemented
 
@@ -194,16 +198,12 @@ class Time(FormattableMixin, time):
 
     # DIFFERENCES
 
-    def diff(self, dt=None, abs=True):
+    def diff(self, dt: time | None = None, abs: bool = True) -> Duration:
         """
         Returns the difference between two Time objects as an Duration.
 
-        :type dt: Time or None
-
-        :param abs: Whether to return an absolute interval or not
-        :type abs: bool
-
-        :rtype: Duration
+        :param dt: The time to subtract from
+        :param abs: Whether to return an absolute duration or not
         """
         if dt is None:
             dt = pendulum.now().time()
@@ -224,19 +224,18 @@ class Time(FormattableMixin, time):
 
         return klass(microseconds=us2 - us1)
 
-    def diff_for_humans(self, other=None, absolute=False, locale=None):
+    def diff_for_humans(
+        self,
+        other: time | None = None,
+        absolute: bool = False,
+        locale: str | None = None,
+    ) -> str:
         """
         Get the difference in a human readable format in the current locale.
 
-        :type other: Time or time
-
+        :param dt: The time to subtract from
         :param absolute: removes time difference modifiers ago, after, etc
-        :type absolute: bool
-
         :param locale: The locale to use for localization
-        :type locale: str
-
-        :rtype: str
         """
         is_now = other is None
 
@@ -250,8 +249,14 @@ class Time(FormattableMixin, time):
     # Compatibility methods
 
     def replace(
-        self, hour=None, minute=None, second=None, microsecond=None, tzinfo=True
-    ):
+        self,
+        hour: int | None = None,
+        minute: int | None = None,
+        second: int | None = None,
+        microsecond: int | None = None,
+        tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
+        fold: int = 0,
+    ) -> Time:
         if tzinfo is True:
             tzinfo = self.tzinfo
 
@@ -260,23 +265,36 @@ class Time(FormattableMixin, time):
         second = second if second is not None else self.second
         microsecond = microsecond if microsecond is not None else self.microsecond
 
-        t = super().replace(hour, minute, second, microsecond, tzinfo=tzinfo)
+        t = super().replace(
+            hour,
+            minute,
+            second,
+            microsecond,
+            tzinfo=cast(Optional[datetime.tzinfo], tzinfo),
+            fold=fold,
+        )
         return self.__class__(
             t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
         )
 
-    def __getnewargs__(self):
+    def __getnewargs__(self) -> tuple[Time]:
         return (self,)
 
-    def _get_state(self, protocol=3):
+    def _get_state(
+        self, protocol: int = 3
+    ) -> tuple[int, int, int, int, datetime.tzinfo | None]:
         tz = self.tzinfo
 
-        return (self.hour, self.minute, self.second, self.microsecond, tz)
+        return self.hour, self.minute, self.second, self.microsecond, tz
 
-    def __reduce__(self):
+    def __reduce__(
+        self,
+    ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
         return self.__reduce_ex__(2)
 
-    def __reduce_ex__(self, protocol):
+    def __reduce_ex__(  # type: ignore[override]
+        self, protocol: int
+    ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
         return self.__class__, self._get_state(protocol)
 
 
diff --git a/pendulum/tz/__init__.py b/pendulum/tz/__init__.py
index fd0a3b7..45c9855 100644
--- a/pendulum/tz/__init__.py
+++ b/pendulum/tz/__init__.py
@@ -2,10 +2,6 @@ from __future__ import annotations
 
 import sys
 
-from typing import Union
-
-import tzdata
-
 from pendulum.tz.local_timezone import get_local_timezone
 from pendulum.tz.local_timezone import set_local_timezone
 from pendulum.tz.local_timezone import test_local_timezone
@@ -13,28 +9,25 @@ from pendulum.tz.timezone import UTC
 from pendulum.tz.timezone import FixedTimezone
 from pendulum.tz.timezone import Timezone
 
-
 if sys.version_info >= (3, 9):
     from importlib import resources
 else:
     import importlib_resources as resources
 
-
 PRE_TRANSITION = "pre"
 POST_TRANSITION = "post"
 TRANSITION_ERROR = "error"
 
 _timezones = None
 
+_tz_cache: dict[int, FixedTimezone] = {}
 
-_tz_cache = {}
 
-
-def timezones():
+def timezones() -> tuple[str, ...]:
     global _timezones
 
     if _timezones is None:
-        with open(resources.files(tzdata).joinpath("zones")) as f:
+        with resources.files("tzdata").joinpath("zones").open() as f:
             _timezones = tuple(tz.strip() for tz in f.readlines())
 
     return _timezones
@@ -66,8 +59,22 @@ def fixed_timezone(offset: int) -> FixedTimezone:
     return tz
 
 
-def local_timezone() -> Timezone:
+def local_timezone() -> Timezone | FixedTimezone:
     """
     Return the local timezone.
     """
     return get_local_timezone()
+
+
+__all__ = [
+    "UTC",
+    "Timezone",
+    "FixedTimezone",
+    "set_local_timezone",
+    "get_local_timezone",
+    "test_local_timezone",
+    "timezone",
+    "fixed_timezone",
+    "local_timezone",
+    "timezones",
+]
diff --git a/pendulum/tz/data/windows.py b/pendulum/tz/data/windows.py
index e76e6bb..65aa6c3 100644
--- a/pendulum/tz/data/windows.py
+++ b/pendulum/tz/data/windows.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-
 windows_timezones = {
     "AUS Central Standard Time": "Australia/Darwin",
     "AUS Eastern Standard Time": "Australia/Sydney",
diff --git a/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py
index 5d5a393..b8833ac 100644
--- a/pendulum/tz/exceptions.py
+++ b/pendulum/tz/exceptions.py
@@ -1,31 +1,32 @@
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from datetime import datetime
 
-class TimezoneError(ValueError):
 
+class TimezoneError(ValueError):
     pass
 
 
 class InvalidTimezone(TimezoneError):
-
     pass
 
 
 class NonExistingTime(TimezoneError):
-
     message = "The datetime {} does not exist."
 
-    def __init__(self, dt):
+    def __init__(self, dt: datetime) -> None:
         message = self.message.format(dt)
 
         super().__init__(message)
 
 
 class AmbiguousTime(TimezoneError):
-
     message = "The datetime {} is ambiguous."
 
-    def __init__(self, dt):
+    def __init__(self, dt: datetime) -> None:
         message = self.message.format(dt)
 
         super().__init__(message)
diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py
index 6ddb9bf..41cf81b 100644
--- a/pendulum/tz/local_timezone.py
+++ b/pendulum/tz/local_timezone.py
@@ -7,20 +7,17 @@ import sys
 
 from contextlib import contextmanager
 from typing import Iterator
+from typing import cast
 
 from pendulum.tz.exceptions import InvalidTimezone
 from pendulum.tz.timezone import FixedTimezone
 from pendulum.tz.timezone import Timezone
 
-
-try:
-    import _winreg as winreg
-except ImportError:
+if sys.platform == "win32":
     try:
+        import _winreg as winreg
+    except (ImportError, AttributeError):
         import winreg
-    except ImportError:
-        winreg = None
-
 
 _mock_local_timezone = None
 _local_timezone = None
@@ -64,81 +61,88 @@ def _get_system_timezone() -> Timezone:
     return _get_unix_timezone()
 
 
-def _get_windows_timezone() -> Timezone:
-    from pendulum.tz.data.windows import windows_timezones
+if sys.platform == "win32":
+
+    def _get_windows_timezone() -> Timezone:
+        from pendulum.tz.data.windows import windows_timezones
+
+        # Windows is special. It has unique time zone names (in several
+        # meanings of the word) available, but unfortunately, they can be
+        # translated to the language of the operating system, so we need to
+        # do a backwards lookup, by going through all time zones and see which
+        # one matches.
+        handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+
+        tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+        localtz = winreg.OpenKey(handle, tz_local_key_name)
 
-    # Windows is special. It has unique time zone names (in several
-    # meanings of the word) available, but unfortunately, they can be
-    # translated to the language of the operating system, so we need to
-    # do a backwards lookup, by going through all time zones and see which
-    # one matches.
-    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+        timezone_info = {}
+        size = winreg.QueryInfoKey(localtz)[1]
+        for i in range(size):
+            data = winreg.EnumValue(localtz, i)
+            timezone_info[data[0]] = data[1]
 
-    tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
-    localtz = winreg.OpenKey(handle, tz_local_key_name)
+        localtz.Close()
 
-    timezone_info = {}
-    size = winreg.QueryInfoKey(localtz)[1]
-    for i in range(size):
-        data = winreg.EnumValue(localtz, i)
-        timezone_info[data[0]] = data[1]
+        if "TimeZoneKeyName" in timezone_info:
+            # Windows 7 (and Vista?)
 
-    localtz.Close()
+            # For some reason this returns a string with loads of NUL bytes at
+            # least on some systems. I don't know if this is a bug somewhere, I
+            # just work around it.
+            tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
+        else:
+            # Windows 2000 or XP
 
-    if "TimeZoneKeyName" in timezone_info:
-        # Windows 7 (and Vista?)
+            # This is the localized name:
+            tzwin = timezone_info["StandardName"]
 
-        # For some reason this returns a string with loads of NUL bytes at
-        # least on some systems. I don't know if this is a bug somewhere, I
-        # just work around it.
-        tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
-    else:
-        # Windows 2000 or XP
+            # Open the list of timezones to look up the real name:
+            tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+            tzkey = winreg.OpenKey(handle, tz_key_name)
 
-        # This is the localized name:
-        tzwin = timezone_info["StandardName"]
+            # Now, match this value to Time Zone information
+            tzkeyname = None
+            for i in range(winreg.QueryInfoKey(tzkey)[0]):
+                subkey = winreg.EnumKey(tzkey, i)
+                sub = winreg.OpenKey(tzkey, subkey)
 
-        # Open the list of timezones to look up the real name:
-        tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
-        tzkey = winreg.OpenKey(handle, tz_key_name)
+                info = {}
+                size = winreg.QueryInfoKey(sub)[1]
+                for i in range(size):
+                    data = winreg.EnumValue(sub, i)
+                    info[data[0]] = data[1]
 
-        # Now, match this value to Time Zone information
-        tzkeyname = None
-        for i in range(winreg.QueryInfoKey(tzkey)[0]):
-            subkey = winreg.EnumKey(tzkey, i)
-            sub = winreg.OpenKey(tzkey, subkey)
+                sub.Close()
+                with contextlib.suppress(KeyError):
+                    # This timezone didn't have proper configuration.
+                    # Ignore it.
+                    if info["Std"] == tzwin:
+                        tzkeyname = subkey
+                        break
 
-            info = {}
-            size = winreg.QueryInfoKey(sub)[1]
-            for i in range(size):
-                data = winreg.EnumValue(sub, i)
-                info[data[0]] = data[1]
+            tzkey.Close()
+            handle.Close()
 
-            sub.Close()
-            with contextlib.suppress(KeyError):
-                # This timezone didn't have proper configuration.
-                # Ignore it.
-                if info["Std"] == tzwin:
-                    tzkeyname = subkey
-                    break
+        if tzkeyname is None:
+            raise LookupError("Can not find Windows timezone configuration")
 
-        tzkey.Close()
-        handle.Close()
+        timezone = windows_timezones.get(tzkeyname)
+        if timezone is None:
+            # Nope, that didn't work. Try adding "Standard Time",
+            # it seems to work a lot of times:
+            timezone = windows_timezones.get(tzkeyname + " Standard Time")
 
-    if tzkeyname is None:
-        raise LookupError("Can not find Windows timezone configuration")
+        # Return what we have.
+        if timezone is None:
+            raise LookupError("Unable to find timezone " + tzkeyname)
 
-    timezone = windows_timezones.get(tzkeyname)
-    if timezone is None:
-        # Nope, that didn't work. Try adding "Standard Time",
-        # it seems to work a lot of times:
-        timezone = windows_timezones.get(tzkeyname + " Standard Time")
+        return Timezone(timezone)
 
-    # Return what we have.
-    if timezone is None:
-        raise LookupError("Unable to find timezone " + tzkeyname)
+else:
 
-    return Timezone(timezone)
+    def _get_windows_timezone() -> Timezone:
+        ...
 
 
 def _get_darwin_timezone() -> Timezone:
@@ -160,12 +164,12 @@ def _get_unix_timezone(_root: str = "/") -> Timezone:
     tzpath = os.path.join(_root, "etc/timezone")
     if os.path.isfile(tzpath):
         with open(tzpath, "rb") as tzfile:
-            data = tzfile.read()
+            tzfile_data = tzfile.read()
 
             # Issue #3 was that /etc/timezone was a zoneinfo file.
             # That's a misconfiguration, but we need to handle it gracefully:
-            if data[:5] != "TZif2":
-                etctz = data.strip().decode()
+            if tzfile_data[:5] != b"TZif2":
+                etctz = tzfile_data.strip().decode()
                 # Get rid of host definitions and comments:
                 if " " in etctz:
                     etctz, dummy = etctz.split(" ", 1)
@@ -200,15 +204,19 @@ def _get_unix_timezone(_root: str = "/") -> Timezone:
             if match is not None:
                 # Some setting existed
                 line = line[match.end() :]
-                etctz = line[: end_re.search(line).start()]
+                etctz = line[
+                    : cast(
+                        re.Match, end_re.search(line)  # type: ignore[type-arg]
+                    ).start()
+                ]
 
                 parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
-                tzpath = []
+                tzpath_parts: list[str] = []
                 while parts:
-                    tzpath.insert(0, parts.pop(0))
+                    tzpath_parts.insert(0, parts.pop(0))
 
                     with contextlib.suppress(InvalidTimezone):
-                        return Timezone(os.path.join(*tzpath))
+                        return Timezone(os.path.join(*tzpath_parts))
 
     # systemd distributions use symlinks that include the zone name,
     # see manpage of localtime(5) and timedatectl(1)
@@ -217,11 +225,11 @@ def _get_unix_timezone(_root: str = "/") -> Timezone:
         parts = list(
             reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
         )
-        tzpath = []
+        tzpath_parts: list[str] = []  # type: ignore[no-redef]
         while parts:
-            tzpath.insert(0, parts.pop(0))
+            tzpath_parts.insert(0, parts.pop(0))
             with contextlib.suppress(InvalidTimezone):
-                return Timezone(os.path.join(*tzpath))
+                return Timezone(os.path.join(*tzpath_parts))
 
     # No explicit setting existed. Use localtime
     for filename in ("etc/localtime", "usr/local/etc/localtime"):
@@ -231,7 +239,7 @@ def _get_unix_timezone(_root: str = "/") -> Timezone:
             continue
 
         with open(tzpath, "rb") as f:
-            return Timezone.from_file(f)
+            return cast(Timezone, Timezone.from_file(f))
 
     raise RuntimeError("Unable to find any timezone configuration")
 
@@ -243,7 +251,7 @@ def _tz_from_env(tzenv: str) -> Timezone:
     # TZ specifies a file
     if os.path.isfile(tzenv):
         with open(tzenv, "rb") as f:
-            return Timezone.from_file(f)
+            return cast(Timezone, Timezone.from_file(f))
 
     # TZ specifies a zoneinfo zone.
     try:
diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py
index 1ddf074..f689004 100644
--- a/pendulum/tz/timezone.py
+++ b/pendulum/tz/timezone.py
@@ -1,25 +1,20 @@
 from __future__ import annotations
 
+import datetime as datetime_
+
 from abc import ABC
 from abc import abstractmethod
-from datetime import datetime
-from datetime import timedelta
-from datetime import tzinfo
-from typing import TypeVar
+from typing import cast
 
 from pendulum.tz.exceptions import AmbiguousTime
 from pendulum.tz.exceptions import InvalidTimezone
 from pendulum.tz.exceptions import NonExistingTime
 from pendulum.utils._compat import zoneinfo
 
-
 POST_TRANSITION = "post"
 PRE_TRANSITION = "pre"
 TRANSITION_ERROR = "error"
 
-_datetime = datetime
-_D = TypeVar("_D", bound=datetime)
-
 
 class PendulumTimezone(ABC):
     @property
@@ -28,7 +23,9 @@ class PendulumTimezone(ABC):
         raise NotImplementedError
 
     @abstractmethod
-    def convert(self, dt: datetime, dst_rule: str | None = None) -> datetime:
+    def convert(
+        self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+    ) -> datetime_.datetime:
         raise NotImplementedError
 
     @abstractmethod
@@ -41,11 +38,11 @@ class PendulumTimezone(ABC):
         minute: int = 0,
         second: int = 0,
         microsecond: int = 0,
-    ) -> datetime:
+    ) -> datetime_.datetime:
         raise NotImplementedError
 
 
-class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
+class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):  # type: ignore[misc]
     """
     Represents a named timezone.
 
@@ -57,15 +54,17 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
 
     def __new__(cls, key: str) -> Timezone:
         try:
-            return super().__new__(cls, key)
+            return cast(Timezone, super().__new__(cls, key))
         except zoneinfo.ZoneInfoNotFoundError:
             raise InvalidTimezone(key)
 
     @property
     def name(self) -> str:
-        return self.key
+        return cast(str, self.key)
 
-    def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime:
+    def convert(
+        self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+    ) -> datetime_.datetime:
         """
         Converts a datetime in the current timezone.
 
@@ -121,19 +120,21 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
         minute: int = 0,
         second: int = 0,
         microsecond: int = 0,
-    ) -> _datetime:
+    ) -> datetime_.datetime:
         """
         Return a normalized datetime for the current timezone.
         """
         return self.convert(
-            datetime(year, month, day, hour, minute, second, microsecond, fold=1)
+            datetime_.datetime(
+                year, month, day, hour, minute, second, microsecond, fold=1
+            )
         )
 
     def __repr__(self) -> str:
         return f"{self.__class__.__name__}('{self.name}')"
 
 
-class FixedTimezone(tzinfo, PendulumTimezone):
+class FixedTimezone(datetime_.tzinfo, PendulumTimezone):
     def __init__(self, offset: int, name: str | None = None) -> None:
         sign = "-" if offset < 0 else "+"
 
@@ -145,13 +146,15 @@ class FixedTimezone(tzinfo, PendulumTimezone):
 
         self._name = name
         self._offset = offset
-        self._utcoffset = timedelta(seconds=offset)
+        self._utcoffset = datetime_.timedelta(seconds=offset)
 
     @property
     def name(self) -> str:
         return self._name
 
-    def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime:
+    def convert(
+        self, dt: datetime_.datetime, raise_on_unknown_times: bool = False
+    ) -> datetime_.datetime:
         if dt.tzinfo is None:
             return dt.__class__(
                 dt.year,
@@ -176,29 +179,31 @@ class FixedTimezone(tzinfo, PendulumTimezone):
         minute: int = 0,
         second: int = 0,
         microsecond: int = 0,
-    ) -> datetime:
+    ) -> datetime_.datetime:
         return self.convert(
-            datetime(year, month, day, hour, minute, second, microsecond, fold=1)
+            datetime_.datetime(
+                year, month, day, hour, minute, second, microsecond, fold=1
+            )
         )
 
     @property
     def offset(self) -> int:
         return self._offset
 
-    def utcoffset(self, dt: datetime | None) -> timedelta:
+    def utcoffset(self, dt: datetime_.datetime | None) -> datetime_.timedelta:
         return self._utcoffset
 
-    def dst(self, dt: _datetime | None):
-        return timedelta()
+    def dst(self, dt: datetime_.datetime | None) -> datetime_.timedelta:
+        return datetime_.timedelta()
 
-    def fromutc(self, dt: datetime) -> datetime:
+    def fromutc(self, dt: datetime_.datetime) -> datetime_.datetime:
         # Use the stdlib datetime's add method to avoid infinite recursion
-        return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
+        return (datetime_.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
 
-    def tzname(self, dt: datetime | None) -> str | None:
+    def tzname(self, dt: datetime_.datetime | None) -> str | None:
         return self._name
 
-    def __getinitargs__(self) -> tuple:
+    def __getinitargs__(self) -> tuple[int, str]:
         return self._offset, self._name
 
     def __repr__(self) -> str:
diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py
index 60ad52c..8f32f9e 100644
--- a/pendulum/utils/_compat.py
+++ b/pendulum/utils/_compat.py
@@ -2,15 +2,12 @@ from __future__ import annotations
 
 import sys
 
-
 PYPY = hasattr(sys, "pypy_version_info")
 PY38 = sys.version_info[:2] >= (3, 8)
 
-
 try:
     from backports import zoneinfo
 except ImportError:
-    import zoneinfo
-
+    import zoneinfo  # type: ignore[no-redef]
 
 __all__ = ["zoneinfo"]
diff --git a/poetry.lock b/poetry.lock
index 6ad6918..cddeae3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -15,10 +15,10 @@ optional = false
 python-versions = ">=3.5"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope-interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
-docs = ["furo", "sphinx", "zope-interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope-interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope-interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope-interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope-interface"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
 
 [[package]]
 name = "babel"
@@ -159,7 +159,7 @@ python-versions = "*"
 python-dateutil = ">=2.8.1"
 
 [package.extras]
-dev = ["wheel", "flake8", "markdown", "twine"]
+dev = ["flake8", "markdown", "twine", "wheel"]
 
 [[package]]
 name = "identify"
@@ -185,9 +185,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
+docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
 perf = ["ipython"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl-flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+testing = ["flufl-flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
 
 [[package]]
 name = "importlib-resources"
@@ -201,8 +201,8 @@ python-versions = ">=3.7"
 zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [[package]]
 name = "iniconfig"
@@ -221,10 +221,10 @@ optional = false
 python-versions = ">=3.6.1,<4.0"
 
 [package.extras]
-pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
-requirements_deprecated_finder = ["pipreqs", "pip-api"]
 colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
 plugins = ["setuptools"]
+requirements_deprecated_finder = ["pip-api", "pipreqs"]
 
 [[package]]
 name = "jinja2"
@@ -351,8 +351,8 @@ optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
-test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
+docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
 
 [[package]]
 name = "pluggy"
@@ -430,7 +430,7 @@ optional = false
 python-versions = ">=3.6.8"
 
 [package.extras]
-diagrams = ["railroad-diagrams", "jinja2"]
+diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
@@ -467,7 +467,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
 pytest = ">=4.6"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
 
 [[package]]
 name = "python-dateutil"
@@ -516,9 +516,9 @@ optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)", "sphinx-notfound-page (==0.8.3)", "sphinx-hoverxref (<2)", "pygments-github-lexers (==0.0.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-reredirects", "sphinxcontrib-towncrier", "furo"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-enabler (>=1.3)", "pytest-perf", "mock", "flake8-2020", "virtualenv (>=13.0.0)", "wheel", "pip (>=19.1)", "jaraco.envs (>=2.2)", "pytest-xdist", "jaraco.path (>=3.2.0)", "build", "filelock (>=3.4.0)", "pip-run (>=8.8)", "ini2toml[lite] (>=0.9)", "tomli-w (>=1.0.0)", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy (>=0.9.1)"]
-testing-integration = ["pytest", "pytest-xdist", "pytest-enabler", "virtualenv (>=13.0.0)", "tomli", "wheel", "jaraco.path (>=3.2.0)", "jaraco.envs (>=2.2)", "build", "filelock (>=3.4.0)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "six"
@@ -576,7 +576,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,
 
 [package.extras]
 docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
-testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"]
+testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"]
 
 [[package]]
 name = "typed-ast"
@@ -586,6 +586,22 @@ category = "dev"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "types-backports"
+version = "0.1.3"
+description = "Typing stubs for backports"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.8.19"
+description = "Typing stubs for python-dateutil"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "typing-extensions"
 version = "4.3.0"
@@ -640,16 +656,18 @@ optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco-itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["func-timeout", "jaraco-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "8b2dbefd9d864bdc0e95e198e80296a9a314055cf0afaf3afb26e08261f7720a"
+content-hash = "f2bb781400f4422396c7b1e6f5698bd1c91097f0cdc6f9fa56f334fd550de5f7"
 
 [metadata.files]
-atomicwrites = []
+atomicwrites = [
+    {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
+]
 attrs = [
     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
     {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
@@ -717,7 +735,49 @@ colorama = [
     {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
     {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
 ]
-coverage = []
+coverage = [
+    {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"},
+    {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"},
+    {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"},
+    {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"},
+    {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"},
+    {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"},
+    {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"},
+    {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"},
+    {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"},
+    {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"},
+    {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"},
+    {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"},
+    {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"},
+    {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"},
+    {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"},
+    {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"},
+    {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"},
+    {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"},
+    {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"},
+    {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"},
+    {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"},
+    {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"},
+    {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"},
+    {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"},
+    {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"},
+    {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"},
+    {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"},
+    {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"},
+    {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"},
+    {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"},
+    {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"},
+    {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"},
+    {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"},
+    {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"},
+    {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"},
+    {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"},
+    {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"},
+    {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"},
+    {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"},
+    {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"},
+    {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"},
+]
 crashtest = [
     {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"},
     {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"},
@@ -738,7 +798,10 @@ identify = [
     {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"},
     {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"},
 ]
-importlib-metadata = []
+importlib-metadata = [
+    {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
+    {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
+]
 importlib-resources = [
     {file = "importlib_resources-5.9.0-py3-none-any.whl", hash = "sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7"},
     {file = "importlib_resources-5.9.0.tar.gz", hash = "sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681"},
@@ -816,7 +879,10 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
-nodeenv = []
+nodeenv = [
+    {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
+    {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
+]
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ -833,7 +899,10 @@ pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
-pre-commit = []
+pre-commit = [
+    {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
+    {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
+]
 py = [
     {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
     {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@@ -967,7 +1036,10 @@ tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
-tox = []
+tox = [
+    {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"},
+    {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"},
+]
 typed-ast = [
     {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
     {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
@@ -994,6 +1066,14 @@ typed-ast = [
     {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
     {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
 ]
+types-backports = [
+    {file = "types-backports-0.1.3.tar.gz", hash = "sha256:f4b7206c073df88d6200891e3d27506185fd60cda66fb289737b2fa92c0010cf"},
+    {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"},
+]
+types-python-dateutil = [
+    {file = "types-python-dateutil-2.8.19.tar.gz", hash = "sha256:bfd3eb39c7253aea4ba23b10f69b017d30b013662bb4be4ab48b20bbd763f309"},
+    {file = "types_python_dateutil-2.8.19-py3-none-any.whl", hash = "sha256:6284df1e4783d8fc6e587f0317a81333856b872a6669a282f8a325342bce7fa8"},
+]
 typing-extensions = [
     {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
     {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
@@ -1033,4 +1113,7 @@ watchdog = [
     {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
     {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
 ]
-zipp = []
+zipp = [
+    {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
+    {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index ac8ea9a..f5bd900 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,25 +11,25 @@ documentation = "https://pendulum.eustace.io/docs"
 keywords = ['datetime', 'date', 'time']
 
 packages = [
-    {include = "pendulum"},
+    { include = "pendulum" },
     #{include = "tests", format = "sdist"},
 ]
 include = [
-    {path = "pendulum/py.typed"},
+    { path = "pendulum/py.typed" },
     # C extensions must be included in the wheel distributions
-    {path = "pendulum/_extensions/*.so", format = "wheel"},
-    {path = "pendulum/_extensions/*.pyd", format = "wheel"},
-    {path = "pendulum/parsing/*.so", format = "wheel"},
-    {path = "pendulum/parsing/*.pyd", format = "wheel"},
+    { path = "pendulum/_extensions/*.so", format = "wheel" },
+    { path = "pendulum/_extensions/*.pyd", format = "wheel" },
+    { path = "pendulum/parsing/*.so", format = "wheel" },
+    { path = "pendulum/parsing/*.pyd", format = "wheel" },
 ]
 
 
 [tool.poetry.dependencies]
 python = "^3.7"
 python-dateutil = "^2.6"
-"backports.zoneinfo" = {version = "^0.2.1", python = ">=3.7,<3.9"}
+"backports.zoneinfo" = { version = "^0.2.1", python = ">=3.7,<3.9" }
 tzdata = ">=2020.1"
-importlib-resources = {version = "^5.9.0", python = ">=3.7,<3.9"}
+importlib-resources = { version = "^5.9.0", python = ">=3.7,<3.9" }
 
 [tool.poetry.group.test.dependencies]
 pytest = "^7.1.2"
@@ -44,9 +44,11 @@ pygments = "^2.2"
 markdown-include = "^0.5.1"
 
 [tool.poetry.group.lint.dependencies]
-black = {version = "^22.6.0", markers = "implementation_name != 'pypy'"}
+black = { version = "^22.6.0", markers = "implementation_name != 'pypy'" }
 isort = "^5.10.1"
 pre-commit = "^2.20.0"
+types-backports = "^0.1.3"
+types-python-dateutil = "^2.8.19"
 
 [tool.poetry.group.dev.dependencies]
 babel = "^2.10.3"
@@ -61,7 +63,7 @@ script = "build.py"
 profile = "black"
 force_single_line = true
 atomic = true
-lines_after_imports = 2
+lines_after_imports = -1
 lines_between_types = 1
 skip_glob = [
     "pendulum/locales/**",
@@ -91,26 +93,7 @@ pretty = true
 
 [[tool.mypy.overrides]]
 module = [
-    "pendulum",
-    "pendulum._extensions.helpers",
-    "pendulum.date",
-    "pendulum.datetime",
-    "pendulum.duration",
-    "pendulum.formatting.formatter",
-    "pendulum.formatting.difference_formatter",
-    "pendulum.helpers",
-    "pendulum.locales.locale",
     "pendulum.mixins.default",
-    "pendulum.parser",
-    "pendulum.parsing",
-    "pendulum.parsing.iso8601",
-    "pendulum.period",
-    "pendulum.time",
-    "pendulum.tz",
-    "pendulum.tz.exceptions",
-    "pendulum.tz.local_timezone",
-    "pendulum.tz.timezone",
-    "pendulum.utils._compat",
     "tests.conftest",
     "tests.test_helpers",
     "tests.test_main",


Test patch:
diff --git a/tests/date/test_diff.py b/tests/date/test_diff.py
index 6373a9f..59a87ec 100644
--- a/tests/date/test_diff.py
+++ b/tests/date/test_diff.py
@@ -129,13 +129,13 @@ def test_diff_for_humans_now_and_nearly_month(today):
 
 
 def test_diff_for_humans_now_and_month():
-    with pendulum.test(pendulum.datetime(2016, 3, 1)):
+    with pendulum.test(pendulum.datetime(2016, 4, 1)):
         today = pendulum.today().date()
 
         assert today.subtract(weeks=4).diff_for_humans() == "4 weeks ago"
         assert today.subtract(months=1).diff_for_humans() == "1 month ago"
 
-    with pendulum.test(pendulum.datetime(2017, 2, 28)):
+    with pendulum.test(pendulum.datetime(2017, 3, 1)):
         today = pendulum.today().date()
 
         assert today.subtract(weeks=4).diff_for_humans() == "1 month ago"
@@ -183,23 +183,23 @@ def test_diff_for_humans_now_and_nearly_future_month(today):
 
 def test_diff_for_humans_now_and_future_month():
     with pendulum.test(pendulum.datetime(2016, 3, 1)):
-        today = pendulum.today().date()
+        today = pendulum.today("UTC").date()
 
         assert today.add(weeks=4).diff_for_humans() == "in 4 weeks"
         assert today.add(months=1).diff_for_humans() == "in 1 month"
 
     with pendulum.test(pendulum.datetime(2017, 3, 31)):
-        today = pendulum.today().date()
+        today = pendulum.today("UTC").date()
 
         assert today.add(months=1).diff_for_humans() == "in 1 month"
 
     with pendulum.test(pendulum.datetime(2017, 4, 30)):
-        today = pendulum.today().date()
+        today = pendulum.today("UTC").date()
 
         assert today.add(months=1).diff_for_humans() == "in 1 month"
 
     with pendulum.test(pendulum.datetime(2017, 1, 31)):
-        today = pendulum.today().date()
+        today = pendulum.today("UTC").date()
 
         assert today.add(weeks=4).diff_for_humans() == "in 1 month"
 
diff --git a/tests/datetime/test_behavior.py b/tests/datetime/test_behavior.py
index 712a4e5..e02323a 100644
--- a/tests/datetime/test_behavior.py
+++ b/tests/datetime/test_behavior.py
@@ -6,7 +6,6 @@ from copy import deepcopy
 from datetime import date
 from datetime import datetime
 from datetime import time
-from datetime import timedelta
 
 import pytest
 
@@ -14,6 +13,7 @@ import pendulum
 
 from pendulum import timezone
 from pendulum.tz.timezone import Timezone
+from pendulum.utils._compat import zoneinfo
 
 
 @pytest.fixture
@@ -147,8 +147,9 @@ def test_pickle_with_integer_tzinfo():
 
 def test_proper_dst():
     dt = pendulum.datetime(1941, 7, 1, tz="Europe/Amsterdam")
+    native_dt = datetime(1941, 7, 1, tzinfo=zoneinfo.ZoneInfo("Europe/Amsterdam"))
 
-    assert dt.dst() == timedelta(0, 6000)
+    assert dt.dst() == native_dt.dst()
 
 
 def test_deepcopy():
diff --git a/tests/localization/test_cs.py b/tests/localization/test_cs.py
index 8551b61..b938e63 100644
--- a/tests/localization/test_cs.py
+++ b/tests/localization/test_cs.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "cs"
 
 
diff --git a/tests/localization/test_da.py b/tests/localization/test_da.py
index 7b455fa..ef46a08 100644
--- a/tests/localization/test_da.py
+++ b/tests/localization/test_da.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "da"
 
 
diff --git a/tests/localization/test_de.py b/tests/localization/test_de.py
index 0b051ee..45f544a 100644
--- a/tests/localization/test_de.py
+++ b/tests/localization/test_de.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "de"
 
 
diff --git a/tests/localization/test_es.py b/tests/localization/test_es.py
index 4e8af45..7fb8d63 100644
--- a/tests/localization/test_es.py
+++ b/tests/localization/test_es.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "es"
 
 
diff --git a/tests/localization/test_fa.py b/tests/localization/test_fa.py
index 2c52b0e..3fb9124 100644
--- a/tests/localization/test_fa.py
+++ b/tests/localization/test_fa.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "fa"
 
 
diff --git a/tests/localization/test_fo.py b/tests/localization/test_fo.py
index d3c82a1..af0bd35 100644
--- a/tests/localization/test_fo.py
+++ b/tests/localization/test_fo.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "fo"
 
 
diff --git a/tests/localization/test_fr.py b/tests/localization/test_fr.py
index 5080061..6df0d65 100644
--- a/tests/localization/test_fr.py
+++ b/tests/localization/test_fr.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "fr"
 
 
diff --git a/tests/localization/test_he.py b/tests/localization/test_he.py
index 53adfd2..f6b96c4 100644
--- a/tests/localization/test_he.py
+++ b/tests/localization/test_he.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "he"
 
 
diff --git a/tests/localization/test_id.py b/tests/localization/test_id.py
index 9885c45..a34c10a 100644
--- a/tests/localization/test_id.py
+++ b/tests/localization/test_id.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "id"
 
 
diff --git a/tests/localization/test_it.py b/tests/localization/test_it.py
index b7fca2a..1c5c419 100644
--- a/tests/localization/test_it.py
+++ b/tests/localization/test_it.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "it"
 
 
diff --git a/tests/localization/test_ja.py b/tests/localization/test_ja.py
index 9dba609..920d3c5 100644
--- a/tests/localization/test_ja.py
+++ b/tests/localization/test_ja.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "ja"
 
 
diff --git a/tests/localization/test_ko.py b/tests/localization/test_ko.py
index 24a392f..79add87 100644
--- a/tests/localization/test_ko.py
+++ b/tests/localization/test_ko.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "ko"
 
 
diff --git a/tests/localization/test_lt.py b/tests/localization/test_lt.py
index edeb9aa..5dcdc38 100644
--- a/tests/localization/test_lt.py
+++ b/tests/localization/test_lt.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "lt"
 
 
diff --git a/tests/localization/test_nb.py b/tests/localization/test_nb.py
index 8e548d2..be12df2 100644
--- a/tests/localization/test_nb.py
+++ b/tests/localization/test_nb.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "nb"
 
 
diff --git a/tests/localization/test_nl.py b/tests/localization/test_nl.py
index e6f8ee9..b485731 100644
--- a/tests/localization/test_nl.py
+++ b/tests/localization/test_nl.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "nl"
 
 
diff --git a/tests/localization/test_nn.py b/tests/localization/test_nn.py
index bd89671..4c72644 100644
--- a/tests/localization/test_nn.py
+++ b/tests/localization/test_nn.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "nn"
 
 
diff --git a/tests/localization/test_pl.py b/tests/localization/test_pl.py
index e525d71..81c8ff2 100644
--- a/tests/localization/test_pl.py
+++ b/tests/localization/test_pl.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "pl"
 
 
diff --git a/tests/localization/test_ru.py b/tests/localization/test_ru.py
index 5b7dd35..c4fbcd7 100644
--- a/tests/localization/test_ru.py
+++ b/tests/localization/test_ru.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "ru"
 
 
diff --git a/tests/localization/test_sk.py b/tests/localization/test_sk.py
index df44a3a..1b47a37 100644
--- a/tests/localization/test_sk.py
+++ b/tests/localization/test_sk.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "sk"
 
 
diff --git a/tests/localization/test_sv.py b/tests/localization/test_sv.py
index b73ec50..7d93a06 100644
--- a/tests/localization/test_sv.py
+++ b/tests/localization/test_sv.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import pendulum
 
-
 locale = "sv"
 
 
diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py
index 83f2881..0047791 100644
--- a/tests/parsing/test_parse_iso8601.py
+++ b/tests/parsing/test_parse_iso8601.py
@@ -8,7 +8,6 @@ import pytest
 
 from pendulum.parsing import parse_iso8601
 
-
 try:
     from pendulum.parsing._extension import TZFixedOffset as FixedTimezone
 except ImportError:
diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py
index 315deb0..655267d 100644
--- a/tests/tz/test_timezone.py
+++ b/tests/tz/test_timezone.py
@@ -11,6 +11,7 @@ from pendulum import timezone
 from pendulum.tz import fixed_timezone
 from pendulum.tz.exceptions import AmbiguousTime
 from pendulum.tz.exceptions import NonExistingTime
+from pendulum.utils._compat import zoneinfo
 from tests.conftest import assert_datetime
 
 
@@ -232,8 +233,9 @@ def test_utcoffset_pre_transition():
 def test_dst():
     tz = pendulum.timezone("Europe/Amsterdam")
     dst = tz.dst(datetime(1940, 7, 1))
+    native_tz = zoneinfo.ZoneInfo("Europe/Amsterdam")
 
-    assert dst == timedelta(0, 6000)
+    assert dst == native_tz.dst(datetime(1940, 7, 1))
 
 
 def test_short_timezones_should_not_modify_time():