Creating periods#

Every way to build a Period, and what each one means.

Period is abstract - you never instantiate it directly. Instead, a set of static factory methods are available to build a period in various ways. All of them return a fully-formed period.

From a named unit#

The most direct route. Each factory takes a count of that unit:

Factory

Example

Intervals start at

of_years()

Period.of_years(1)

00:00 on 1 January

of_months()

Period.of_months(3)

00:00 on the 1st

of_days()

Period.of_days(1)

midnight

of_hours()

Period.of_hours(6)

the top of the hour

of_minutes()

Period.of_minutes(15)

the top of the minute

of_seconds()

Period.of_seconds(30)

a whole second

of_microseconds()

Period.of_microseconds(40_000)

a whole microsecond

The count must be greater than zero; anything else raises PeriodValidationError.

From a string#

of() accepts three supported string formats and tries each in turn:

1. Plain ISO 8601 duration#

The ISO 8601 duration form: P, then a date part, then T and a time part. Parsing is case-insensitive.

p1y = Period.of("P1Y")  # 1 year
p3m = Period.of("P3M")  # 3 months (a quarter)
p1d = Period.of("P1D")  # 1 day
pt15m = Period.of("PT15M")  # 15 minutes
pt25hz = Period.of("PT0.04S")  # 40 ms - 25 Hz sampling

Components combine, and the period renders back as the string that built it:

p1y6m = Period.of("P1Y6M")
p1dt12h = Period.of("P1DT12H")
pt1h30m = Period.of("PT1H30M")

Warning

A period cannot mix calendar units (years, months) with clock units (days and below), because months have no fixed length in seconds. "P1M1D" raises PeriodValidationError.

Note

of_iso_duration() parses this form and only this form - useful when you want to reject the extended syntaxes explicitly.

2. Duration with an offset#

An extension of the ISO 8601 format, written <duration>+<offset>. The offset is itself a duration, and shifts every interval boundary forwards by that amount:

Period.of("P1D+T9H")  # Hydrological day: 09:00am - 09:00am
Period.of("PT15M+T5M")  # Records measured at :05, :20, :35, :50
Period.of("P1Y+9MT9H")  # UK Hydrological Year

There are also with_*_offset methods that apply an offset to an existing period (returning a new independent Period object):

Period.of("P1D").with_hour_offset(9)  # Hydrological day: 09:00am - 09:00am
Period.of("PT15M").with_minute_offset(5)  # Records measured at :05, :20, :35, :50
Period.of("P1Y").with_month_offset(9).with_hour_offset(9)  # UK Hydrological Year

Note

of_duration() accepts both this offset form and a plain duration (with no offset).

3. Duration with an origin#

Written <start>/<duration>. The start datetime becomes ordinal 0 and a boundary of the period, which fixes the grid for periods that have no meaningful natural boundary:

p7d = Period.of("2024-01-01/P7D")

print(p7d.ordinal(datetime(2024, 1, 1)))
print(p7d.is_aligned(datetime(2024, 1, 1)))
print(p7d.datetime(1))
0
True
2024-01-08 00:00:00

The start may be a full datetime, or a reduced-precision date - a bare year or year-month is padded to its first instant, following ISO 8601:

print(Period.of("2024/P1Y"))
print(Period.of("2024-10/P1M"))
print(Period.of("1883-01-01T09:00/P1D"))
2024-01-01/P1Y
2024-10-01/P1M
1883-01-01T09:00:00/P1D

Note

of_date_and_duration() parses this form only.

From a timedelta#

of_timedelta() builds a period matching any fixed-length duration:

print(Period.of_timedelta(timedelta(minutes=30)))
print(Period.of_timedelta(timedelta(milliseconds=20)))
print(Period.of_timedelta(timedelta(days=1, hours=12)))
PT30M
PT0.02S
P1DT12H

There is no equivalent for months and years - a timedelta cannot represent them.

Handling bad input#

Two exceptions are raised when a period cannot be built, both subclasses of PeriodError:

PeriodParsingError

The string does not match any supported format at all.

PeriodValidationError

The string parsed, but does not describe a usable period - a zero-length duration, or one mixing calendar and clock units.

Catch PeriodError to handle both:

def parse_resolution(text: str) -> Period | None:
    try:
        return Period.of(text)
    except PeriodError as err:
        print(f"{text!r}: {type(err).__name__} - {err}")
        return None

parse_resolution("PT15M")
parse_resolution("every 15 mins")  # no supported format matches
parse_resolution("P1M1D")  # parses, but mixes calendar and clock units
'every 15 mins': PeriodParsingError - Illegal period: every 15 mins
'P1M1D': PeriodValidationError - Illegal period: P1M1D