From 47718d78139a435689b9d4fa086945fbff9cfec3 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Mon, 6 Jul 2026 15:30:56 +0530 Subject: [PATCH] Fix #807: handle pytz.FixedOffset in pendulum.instance() pendulum.instance() raised 'AttributeError: NoneType object has no attribute lower' for datetimes whose tzinfo is a pytz.FixedOffset. _safe_timezone() detects pytz zones via their localize() method and then uses their .zone name. Named pytz zones carry a zone string, but fixed-offset zones (pytz.FixedOffset) have zone = None, so that None was passed to timezone(), which crashed. Fall back to the tzinfo's UTC offset when there is no zone name, mirroring how other offset-only tzinfos are handled. Added a regression test (skipped when pytz is unavailable). --- CHANGELOG.md | 5 +++++ src/pendulum/__init__.py | 12 +++++++++++- tests/test_main.py | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58bb1312..5c0d68a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [Unreleased] + +### Changed +- Fixed `pendulum.instance()` raising `AttributeError` for datetimes with a `pytz.FixedOffset` timezone [#981](https://github.com/python-pendulum/pendulum/pull/981) + ## [3.2.0] - 2026-01-30 ### Added diff --git a/src/pendulum/__init__.py b/src/pendulum/__init__.py index 16ae0865..71425cd9 100644 --- a/src/pendulum/__init__.py +++ b/src/pendulum/__init__.py @@ -105,7 +105,17 @@ def _safe_timezone( obj = obj.key # pytz elif hasattr(obj, "localize"): - obj = obj.zone # type: ignore[attr-defined] + # Named pytz zones expose a ``zone`` name, but fixed-offset zones + # (``pytz.FixedOffset``) have ``zone = None``; use their offset. + if obj.zone is not None: # type: ignore[attr-defined] + obj = obj.zone # type: ignore[attr-defined] + else: + offset = obj.utcoffset(dt) + + if offset is None: + offset = _datetime.timedelta(0) + + obj = int(offset.total_seconds()) elif obj.tzname(None) == "UTC": return UTC else: diff --git a/tests/test_main.py b/tests/test_main.py index 76033450..e7a5ac29 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,6 +5,9 @@ from datetime import date from datetime import datetime from datetime import time +from datetime import timedelta + +import pytest from dateutil import tz @@ -36,6 +39,18 @@ def test_instance_with_aware_datetime_any_tzinfo() -> None: assert now.timezone_name == "+02:00" +def test_instance_with_pytz_fixed_offset() -> None: + # ``pytz.FixedOffset`` has a ``localize`` method but no ``zone`` name, + # which used to raise ``AttributeError`` in ``_safe_timezone`` (#807). + pytz = pytest.importorskip("pytz") + + dt = pendulum.instance(datetime(2021, 2, 3, tzinfo=pytz.FixedOffset(60))) + assert dt.utcoffset() == timedelta(minutes=60) + + dt = pendulum.instance(datetime(2021, 2, 3, tzinfo=pytz.FixedOffset(-330))) + assert dt.utcoffset() == timedelta(minutes=-330) + + def test_instance_with_date() -> None: dt = pendulum.instance(date(2022, 12, 23))