Time zones#

A period’s timezone is a label on the clock it counts, not a conversion.

A period can carry a tzinfo, set with with_tzinfo():

The tzinfo does three things:

1. It stamps the datetimes the period returns#

datetime() attaches the period’s tzinfo to every datetime it produces, so interval boundaries come back aware:

pt1h_utc = Period.of_hours(1).with_tzinfo(UTC)

print(pt1h_utc.tzinfo)
print(pt1h_utc.floor(datetime(2024, 3, 1, 9, 30)))
UTC
2024-03-01 09:00:00+00:00

2. It labels the clock, and is not applied to inputs#

All of a period’s arithmetic runs on wall-clock fields. Any tzinfo on a datetime passed in is ignored - no conversion happens, so the same wall-clock time always lands in the same interval:

pt1h_utc = Period.of_hours(1).with_tzinfo(UTC)
d = datetime(2024, 3, 1, 9, 30)

# The same wall-clock time lands in the same interval, whatever tzinfo it claims.
print(pt1h_utc.ordinal(d))
print(pt1h_utc.ordinal(d.replace(tzinfo=UTC)))
print(pt1h_utc.ordinal(d.replace(tzinfo=IST)))
17734713
17734713
17734713

This is deliberate: an interval boundary at 09:00 stays at 09:00 on the clock the data was recorded against. It also means it is your responsibility to convert datetimes to the period’s timezone before passing them in, if they might be on another clock.

3. It makes periods on different clocks distinct#

A period at UTC is not equal to a naive one:

without_tz = Period.of_hours(1)
with_tz = Period.of_hours(1).with_tzinfo(UTC)

print(repr(without_tz))
print(repr(with_tz))

print(without_tz == with_tz)
PT1H
PT1H[Z]
False

That is what stops two series recorded on different clocks being silently treated as compatible.

Note

Sorting periods with mismatched tzinfo values raises TypeError, just as sorting naive against aware datetimes does. Equality and hashing are safe in every case.

Showing the timezone#

repr() shows the timezone in square brackets - a named zone by its name, a fixed offset in ISO 8601 form, and nothing at all when the period is naive:

print(repr(Period.of_hours(1).with_tzinfo(UTC)))
print(repr(Period.of_hours(1).with_tzinfo(IST)))
print(repr(Period.of_hours(1).with_tzinfo(ZoneInfo("Europe/London"))))
print(repr(Period.of_hours(1)))
PT1H[Z]
PT1H[+05:30]
PT1H[Europe/London]
PT1H

Daylight saving#

Warning

Because a period counts wall-clock time, a zone with daylight saving does not behave the way a fixed offset does. On a spring-forward day the local clock has 23 hours, but a period counting local time still sees 24 one-hour intervals - one of which never occurred - and on an autumn day one interval covers two real hours.

The reliable approach for data spanning a daylight saying time transition is to do period arithmetic in UTC or in a fixed offset, and convert to local time only for display:

p1d_utc = Period.of_days(1).with_tzinfo(UTC)

local_reading = datetime(2024, 3, 31, 2, 30, tzinfo=IST)
utc_reading = local_reading.astimezone(UTC)  # convert first ...

print(p1d_utc.floor(utc_reading))  # ... then ask the period
2024-03-30 00:00:00+00:00

If your data is recorded on local standard time with no DST (as environmental monitoring data often is), a fixed timezone offset is the right label and none of this applies.