Skip to content

Commit 47718d7

Browse files
committed
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).
1 parent cbab60c commit 47718d7

3 files changed

Lines changed: 31 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## [Unreleased]
4+
5+
### Changed
6+
- Fixed `pendulum.instance()` raising `AttributeError` for datetimes with a `pytz.FixedOffset` timezone [#981](https://github.com/python-pendulum/pendulum/pull/981)
7+
38
## [3.2.0] - 2026-01-30
49

510
### Added

src/pendulum/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,17 @@ def _safe_timezone(
105105
obj = obj.key
106106
# pytz
107107
elif hasattr(obj, "localize"):
108-
obj = obj.zone # type: ignore[attr-defined]
108+
# Named pytz zones expose a ``zone`` name, but fixed-offset zones
109+
# (``pytz.FixedOffset``) have ``zone = None``; use their offset.
110+
if obj.zone is not None: # type: ignore[attr-defined]
111+
obj = obj.zone # type: ignore[attr-defined]
112+
else:
113+
offset = obj.utcoffset(dt)
114+
115+
if offset is None:
116+
offset = _datetime.timedelta(0)
117+
118+
obj = int(offset.total_seconds())
109119
elif obj.tzname(None) == "UTC":
110120
return UTC
111121
else:

tests/test_main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from datetime import date
66
from datetime import datetime
77
from datetime import time
8+
from datetime import timedelta
9+
10+
import pytest
811

912
from dateutil import tz
1013

@@ -36,6 +39,18 @@ def test_instance_with_aware_datetime_any_tzinfo() -> None:
3639
assert now.timezone_name == "+02:00"
3740

3841

42+
def test_instance_with_pytz_fixed_offset() -> None:
43+
# ``pytz.FixedOffset`` has a ``localize`` method but no ``zone`` name,
44+
# which used to raise ``AttributeError`` in ``_safe_timezone`` (#807).
45+
pytz = pytest.importorskip("pytz")
46+
47+
dt = pendulum.instance(datetime(2021, 2, 3, tzinfo=pytz.FixedOffset(60)))
48+
assert dt.utcoffset() == timedelta(minutes=60)
49+
50+
dt = pendulum.instance(datetime(2021, 2, 3, tzinfo=pytz.FixedOffset(-330)))
51+
assert dt.utcoffset() == timedelta(minutes=-330)
52+
53+
3954
def test_instance_with_date() -> None:
4055
dt = pendulum.instance(date(2022, 12, 23))
4156

0 commit comments

Comments
 (0)