Skip to content

Python

Every public class, method and exception of the workclock Python package, with signatures, parameters, return values and errors.

This page lists the public API of the workclock Python package: workclock.Schedule, the rules WorkDay, Break and Holidays, the operations, the batch methods and the exceptions. workclock needs CPython 3.9 or later. Every datetime it takes must be timezone-aware, and every result comes back in the tzinfo of the datetime passed in. The businesstimedelta-compatible API is on Python compat.

Schedule

Schedule(
    rules: Iterable[Rule],
    *,
    time_zone: str | tzinfo | None = None,
)

workclock.Schedule is working time defined by rules: the union of every WorkDay, minus every Break and every Holidays date. The order of the rules does not matter. Schedules explains how rules combine.

ParameterTypeDefaultDescription
rulesIterable[Rule]RequiredWorkDay, Break and Holidays rules, in any order.
time_zonestr, tzinfo or NoneNoneKeyword only. The time zone of rules that do not set their own: an IANA name, a zoneinfo.ZoneInfo, a pytz zone or datetime.timezone.utc. None means UTC.

Returns a Schedule with these attributes:

AttributeTypeDescription
rulestuple[Rule, ...]The rules the schedule was built from.
time_zonestr or NoneThe IANA name of time_zone, or None if it was not given.

A schedule pickles and deep-copies as its rules and time zone, and is built again from them.

RaisesWhen
TypeErrorA rule is not a WorkDay, Break or Holidays, or time_zone has no IANA name.
UnknownZoneErrorThe schedule's or a rule's time zone is not in the IANA database (E6).
NoWorkingTimeErrorThe working hours minus the breaks are empty over a 14-day reference window, including when there is no WorkDay (E1–E3).

In Python:

from datetime import date, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

support = Schedule(
    [
        WorkDay(time(9), time(17)),  # Monday to Friday by default
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=ZoneInfo("Europe/London"),
)
support.time_zone  # 'Europe/London'

WorkDay

WorkDay(
    start: time,
    end: time,
    days: Iterable[SupportsIndex] = range(5),
    *,
    time_zone: str | tzinfo | None = None,
)

workclock.WorkDay is working hours on some days of the week. On each local date whose weekday is in days, it makes the time from start to end available for work. A shift belongs to the date it starts on. Rules: WorkDay covers overnight and 24-hour shifts.

ParameterTypeDefaultDescription
startdatetime.timeRequiredWall-clock start, without a tzinfo.
enddatetime.timeRequiredWall-clock end, without a tzinfo. At or before start, the shift ends on the next date, so start == end is a 24-hour shift.
daysIterable[SupportsIndex]range(5)Weekdays on which shifts start, from 0 (Monday) to 6 (Sunday). Any integer type, such as NumPy's, but not bool. The default is Monday to Friday.
time_zonestr, tzinfo or NoneNoneKeyword only. The rule's time zone, as for Schedule. None uses the schedule's.

Returns an immutable WorkDay with the attributes start, end, days (a frozenset[int]) and time_zone (an IANA name or None). Rules are equal when these attributes are equal.

RaisesWhen
TypeErrorstart or end is not a datetime.time, or time_zone has no IANA name.
ValueErrorstart or end has a tzinfo, or days is a string or holds anything but integers from 0 to 6.

An unknown time zone name raises UnknownZoneError when the Schedule is built, not here.

In Python, a 22:00–06:00 night shift in UTC (DESIGN.md O2):

from datetime import datetime, time, timezone

from workclock import Schedule, WorkDay

nights = Schedule([WorkDay(time(22), time(6), days=[0, 1, 2, 3, 4])])
monday = datetime(2026, 1, 5, 22, tzinfo=timezone.utc)
nights.add(monday, hours=4)  # 2026-01-06 02:00, UTC

Break

Break(
    start: time,
    end: time,
    days: Iterable[SupportsIndex] = range(5),
    *,
    time_zone: str | tzinfo | None = None,
)

workclock.Break is time off on some days of the week, such as a lunch break. It takes the same parameters as WorkDay, including overnight breaks, has the same attributes and raises the same errors, and removes its time from the working time. A break outside working time has no effect. days defaults to Monday to Friday, so a break on a weekend WorkDay needs its own days.

In Python:

from datetime import datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [WorkDay(time(9), time(17)), Break(time(12), time(13))],
    time_zone=london,
)
opened = datetime(2026, 12, 24, 11, 30, tzinfo=london)
support.add(opened, hours=1)  # 2026-12-24 13:30, London time

Holidays

Holidays(
    dates: Iterable[date],
    *,
    time_zone: str | tzinfo | None = None,
)

workclock.Holidays is dates that are not worked. Each date removes the time from 00:00 to the next 00:00 in the rule's time zone. Searching past the last date is not an error. Rules: Holidays covers holidays on DST change days.

ParameterTypeDefaultDescription
datesIterable[date]Requireddatetime.date objects, or a python-holidays object. Read once, when the rule is created. A python-holidays object contributes only the years it already holds, so create it with years=....
time_zonestr, tzinfo or NoneNoneKeyword only. The rule's time zone, as for Schedule. None uses the schedule's.

Returns an immutable Holidays with the attributes dates (a frozenset[date]) and time_zone (an IANA name or None).

RaisesWhen
TypeErrordates is a string or holds anything but datetime.date objects, including datetime.datetime objects, or time_zone has no IANA name.
ValueErrordates is a python-holidays object that holds no years.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
support.next_period(datetime(2026, 12, 24, 17, tzinfo=london))
# (2026-12-29 09:00, 2026-12-29 17:00), London time

Rule

Rule = Union[WorkDay, Break, Holidays]

workclock.Rule is the type of any rule a Schedule takes, for type annotations.

In Python:

from datetime import date, time

from workclock import Break, Holidays, Rule, Schedule, WorkDay

rules: list[Rule] = [
    WorkDay(time(9), time(17)),
    Break(time(12), time(13)),
    Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
]
support = Schedule(rules, time_zone="Europe/London")

Schedule.add

Schedule.add(
    dt: datetime,
    delta: timedelta | None = None,
    /,
    *,
    hours: float = 0,
    minutes: float = 0,
    seconds: float = 0,
) -> datetime

Finds the instant a given amount of working time after another. The amount is delta plus hours, minutes and seconds. Adding time explains where results land.

ParameterTypeDefaultDescription
dtdatetimeRequiredPositional only. The aware datetime to count from.
deltatimedelta or NoneNonePositional only. Working time to add.
hoursfloat0Keyword only. Hours of working time to add.
minutesfloat0Keyword only. Minutes of working time to add.
secondsfloat0Keyword only. Seconds of working time to add.

Returns the earliest datetime with exactly the amount of working time between dt and it, in the tzinfo of dt. An amount of zero returns dt unchanged, and a negative amount subtracts.

RaisesWhen
TypeErrordt is not a datetime, delta is not a timedelta, or a keyword amount is not a number.
ValueErrordt is naive, or a keyword amount is NaN.
NoWorkingTimeErrorThe search crosses 366 days, plus seven days per holiday date, without finding working time.
OutOfRangeErrordt is outside the supported range, the amount does not fit in 64-bit microseconds, or the result is outside the supported range or datetime's years 1 to 9999.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
opened = datetime(2026, 12, 24, 16, 30, tzinfo=london)
support.add(opened, hours=8)  # 2026-12-30 09:30, London time

Schedule.sub

Schedule.sub(
    dt: datetime,
    delta: timedelta | None = None,
    /,
    *,
    hours: float = 0,
    minutes: float = 0,
    seconds: float = 0,
) -> datetime

Finds the instant a given amount of working time before another. It takes the same parameters as Schedule.add and raises the same errors.

Returns the latest datetime with exactly the amount of working time between it and dt, in the tzinfo of dt. An amount of zero returns dt unchanged, and a negative amount adds.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
due = datetime(2026, 12, 30, 9, 30, tzinfo=london)
support.sub(due, hours=8)  # 2026-12-24 16:30, London time

Schedule.between

Schedule.between(a: datetime, b: datetime) -> timedelta

Measures the working time between two instants. Measuring time explains what is counted.

ParameterTypeDescription
adatetimeThe aware start.
bdatetimeThe aware end.

Returns the working time as a timedelta, negative when b is before a. between never searches, so it never raises NoWorkingTimeError.

RaisesWhen
TypeErrora or b is not a datetime.
ValueErrora or b is naive.
OutOfRangeErrora or b is outside the supported range.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
opened = datetime(2026, 12, 24, 16, 30, tzinfo=london)
now = datetime(2026, 12, 29, 11, tzinfo=london)
support.between(opened, now)  # 2:30:00

Schedule.next_period

Schedule.next_period(dt: datetime) -> tuple[datetime, datetime]

Finds the period of working time that contains an instant, or else the first one after it. A period is a maximal stretch of working time. Periods explains which period is returned.

ParameterTypeDescription
dtdatetimeThe aware datetime to search from.

Returns the period as (start, end), in the tzinfo of dt. If dt is inside a period, start is dt. A period never runs longer than the search bound of 366 days, plus seven days per holiday date.

RaisesWhen
TypeErrordt is not a datetime.
ValueErrordt is naive.
NoWorkingTimeErrorNo working time starts within the search bound.
OutOfRangeErrordt is outside the supported range, or the period reaches past the supported range or datetime's years 1 to 9999.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
now = datetime(2026, 12, 29, 11, tzinfo=london)
support.next_period(now)
# (2026-12-29 11:00, 2026-12-29 12:00), London time

Schedule.previous_period

Schedule.previous_period(dt: datetime) -> tuple[datetime, datetime]

Finds the period of working time that contains an instant, or else the last one before it. It takes the same parameter as Schedule.next_period and raises the same errors.

Returns the period as (start, end), in the tzinfo of dt. If dt is inside a period, end is dt.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
now = datetime(2026, 12, 29, 11, tzinfo=london)
support.previous_period(now)
# (2026-12-29 09:00, 2026-12-29 11:00), London time

Schedule.between_many

Schedule.between_many(
    starts: Sequence[datetime] | NDArray[np.datetime64],
    ends: Sequence[datetime] | NDArray[np.datetime64],
) -> list[timedelta] | NDArray[np.timedelta64]

Runs Schedule.between for each pair of starts and ends in one call.

ParameterTypeDescription
startsSequence[datetime] or NDArray[np.datetime64]The aware starts, or a NumPy array of datetime64 values.
endsSequence[datetime] or NDArray[np.datetime64]The aware ends, as many as starts. If starts is a NumPy array, any value that broadcasts with it, such as one datetime64.

Returns a list[timedelta] for sequences. If starts is a NumPy array, both arguments are read as datetime64 values in UTC and broadcast together under NumPy's rules, and the result is a timedelta64[us] array of the broadcast shape.

RaisesWhen
The errors of Schedule.betweenAn item fails. The message starts with item {i}: , where i is the item's zero-based index in the flattened broadcast input.
ValueErrorThe sequences differ in length, the arrays' shapes do not broadcast together, or an array holds NaT or a value with digits below a microsecond.
TypeErrorAn array holds plain numbers, which carry no unit.

In Python:

from datetime import date, datetime, time
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
opened = [
    datetime(2026, 12, 24, 16, 30, tzinfo=london),
    datetime(2026, 12, 29, 9, tzinfo=london),
]
now = datetime(2026, 12, 29, 11, tzinfo=london)
support.between_many(opened, [now, now])  # [2:30:00, 2:00:00]

Schedule.add_many

Schedule.add_many(
    dts: Sequence[datetime] | NDArray[np.datetime64],
    deltas: Sequence[timedelta] | NDArray[np.timedelta64] | timedelta,
) -> list[datetime] | NDArray[np.datetime64]

Runs Schedule.add for each datetime and amount in one call.

ParameterTypeDescription
dtsSequence[datetime] or NDArray[np.datetime64]The aware datetimes to count from, or a NumPy array of datetime64 values.
deltasSequence[timedelta], NDArray[np.timedelta64] or timedeltaOne amount per datetime, or one timedelta for all.

Returns a list[datetime] for sequences, each result in the tzinfo of its datetime. If dts is a NumPy array, it is read as datetime64 values in UTC, deltas as timedelta64 values, the two broadcast together under NumPy's rules, and the result is a datetime64[us] array of the broadcast shape.

RaisesWhen
The errors of Schedule.addAn item fails. The message starts with item {i}: , where i is the item's zero-based index.
TypeErrordeltas is neither a timedelta nor a sequence, or an array holds plain numbers.
ValueErrorThe same cases as Schedule.between_many.

In Python, with a NumPy array of instants in UTC, which is London time in December:

from datetime import date, time, timedelta

import numpy as np
from workclock import Break, Holidays, Schedule, WorkDay

support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone="Europe/London",
)
opened = np.array(
    ["2026-12-24T16:30", "2026-12-29T11:00"], dtype="datetime64[m]"
)
support.add_many(opened, timedelta(hours=8))
# array(['2026-12-30T09:30:00.000000', '2026-12-30T12:00:00.000000'],
#       dtype='datetime64[us]')

Schedule.sub_many

Schedule.sub_many(
    dts: Sequence[datetime] | NDArray[np.datetime64],
    deltas: Sequence[timedelta] | NDArray[np.timedelta64] | timedelta,
) -> list[datetime] | NDArray[np.datetime64]

Runs Schedule.sub for each datetime and amount in one call. It takes the same parameters as Schedule.add_many, returns the same types and raises the same errors.

In Python:

from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo

from workclock import Break, Holidays, Schedule, WorkDay

london = ZoneInfo("Europe/London")
support = Schedule(
    [
        WorkDay(time(9), time(17)),
        Break(time(12), time(13)),
        Holidays([date(2026, 12, 25), date(2026, 12, 28)]),
    ],
    time_zone=london,
)
due = [
    datetime(2026, 12, 30, 9, 30, tzinfo=london),
    datetime(2026, 12, 30, 12, tzinfo=london),
]
support.sub_many(due, timedelta(hours=8))
# [2026-12-24 16:30, 2026-12-29 11:00], London time

WorkclockError

class WorkclockError(Exception)

workclock.WorkclockError is the base class of the three errors below. An error kind added in a later version is raised as a plain WorkclockError. Malformed input raises TypeError or ValueError instead, which is not a WorkclockError. Errors explains when each error is raised.

In Python:

from datetime import time

from workclock import Schedule, WorkclockError, WorkDay

try:
    Schedule([WorkDay(time(9), time(17))], time_zone="Mars/Olympus_Mons")
except WorkclockError as error:
    print(error)  # unknown time zone `Mars/Olympus_Mons`

NoWorkingTimeError

class NoWorkingTimeError(WorkclockError, ValueError)

workclock.NoWorkingTimeError is raised when a schedule has no working time, or a search finds none within its bound of 366 days, plus seven days per holiday date. Schedule raises it when built; add, sub, next_period, previous_period, add_many and sub_many raise it when a search fails. The message names the cause:

  • Building: schedule has no working time: its working hours minus its breaks are empty.
  • A failed search: no working time within 366 days, plus seven days per holiday date, of {start}, where start is the datetime the search began from.

In Python, a WorkDay with no days (E2):

from datetime import time

from workclock import NoWorkingTimeError, Schedule, WorkDay

try:
    Schedule([WorkDay(time(9), time(17), days=[])])
except NoWorkingTimeError as error:
    print(error)
    # schedule has no working time: its working hours minus its breaks
    # are empty

OutOfRangeError

class OutOfRangeError(WorkclockError, OverflowError)

workclock.OutOfRangeError is raised when an input datetime, an amount or a result is outside the supported range. Instants run from -009999-01-02T01:59:59Z to 9999-12-30T22:00:00Z; results must also fit in datetime's years 1 to 9999, and amounts in 64-bit microseconds. Inputs and precision has the details.

In Python, a result past the end of the range (E5):

from datetime import datetime, time, timezone

from workclock import OutOfRangeError, Schedule, WorkDay

office = Schedule([WorkDay(time(9), time(17))])
thursday = datetime(9999, 12, 30, 16, tzinfo=timezone.utc)
try:
    office.add(thursday, hours=2)
except OutOfRangeError as error:
    print(error)
    # result is outside the supported range -009999-01-02T01:59:59Z to
    # 9999-12-30T22:00:00Z

UnknownZoneError

class UnknownZoneError(WorkclockError, ValueError)

workclock.UnknownZoneError is raised when Schedule is built with a time zone name, for the schedule or a rule, that is not in the IANA database. Time zones and DST: Time zone data says which copy of the database the Python package uses.

In Python (E6):

from datetime import time

from workclock import Schedule, UnknownZoneError, WorkDay

try:
    Schedule([WorkDay(time(9), time(17))], time_zone="Mars/Olympus_Mons")
except UnknownZoneError as error:
    print(error)  # unknown time zone `Mars/Olympus_Mons`

__version__

__version__: str

workclock.__version__ is the version of the installed package.

In Python:

import workclock

workclock.__version__  # '0.1.1'

See also

Report a problem with this pageAsk a question in Discussions

On this page