Skip to content

Use dates and datetimes

Use Date and DateTime values in your PyRel definitions when you need to compare facts based on calendar days, specific moments in time, or elapsed time. This guide covers constructing values, parsing and formatting, extracting components for bucketing logic, writing relative-time conditions, computing elapsed time, and building calendar/time windows.

There are a couple of reasons to use the std.datetime constructors instead of Python’s built-in datetime module:

  • They let you express time-based logic in PyRel that is tied to the model’s execution, rather than the notebook/script execution. A Python datetime is calculated in your notebook or script first, so PyRel sees it as a fixed timestamp.
  • They create PyRel Date and DateTime values, so comparisons behave the way you expect in define(), where(), and select().

Use the date() and datetime() constructors to create literal values in your definitions:

from relationalai.semantics import Date, DateTime, Integer, Model
from relationalai.semantics.std.datetime import date, datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
Ticket.due_by = m.Property(f"{Ticket} due by {Date:due_by}")
data = m.data([
{"id": 201, "created_at": datetime(2026, 2, 1, 9, 0, 0, tz="UTC"), "due_by": date(2026, 2, 8)},
{"id": 202, "created_at": datetime(2026, 2, 10, 16, 45, 0, tz="UTC"), "due_by": date(2026, 2, 17)},
])
m.define(Ticket.new(data.to_schema()))
df = m.select(Ticket.id, Ticket.created_at, Ticket.due_by).to_df()
print(df)
  • If you omit the tz argument, the datetime will be created in UTC by default.
  • You can pass a timezone string like "America/Los_Angeles" to create the datetime in that timezone, or to have it converted from UTC to that timezone.
  • You can also use Python’s built-in datetime.tzinfo objects for more complex timezone handling, and pass them to the tz argument.

Use the std.datetime parsing and formatting functions to convert between text and temporal types, and to produce formatted strings from Date and DateTime values values.

To parse text ISO dates, use date.fromisoformat():

from relationalai.semantics import Date, Integer, Model
from relationalai.semantics.std.datetime import date
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_on = m.Property(f"{Ticket} created on {Date:created_on}")
4 collapsed lines
data = m.data([
{"id": 401, "created_on": "2026-02-01"},
{"id": 402, "created_on": "2026-02-10"},
])
m.define(Ticket.new(id=Ticket.id, created_on=date.fromisoformat(data.created_on)))
df = m.select(Ticket.id, Ticket.created_on).to_df()
print(df)

To parse a datetime string, use datetime.strptime() with a format string:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std.datetime import datetime
9 collapsed lines
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
data = m.data([
{"id": 501, "created_at": "2026-02-01T09:00:00Z"},
{"id": 502, "created_at": "2026-02-10T16:45:00Z"},
])
parsed = datetime.strptime(data.created_at, "%Y-%m-%dT%H:%M:%SZ", tz="UTC")
m.define(Ticket.new(id=Ticket.id, created_at=parsed))
df = m.select(Ticket.id, Ticket.created_at).to_df()
print(df)

For your convenience, datetime.ISO provides common format strings as constants:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std.datetime import datetime
9 collapsed lines
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
data = m.data([
{"id": 601, "created_at": "2026-02-01T09:00:00Z"},
{"id": 602, "created_at": "2026-02-10T16:45:00Z"},
])
parsed = datetime.strptime(data.created_at, datetime.ISO.DATETIME_TZ, tz="UTC")
m.define(Ticket.new(id=Ticket.id, created_at=parsed))
df = m.select(Ticket.id, Ticket.created_at).to_df()
print(df)
  • datetime.strptime(...) parses the datetime string according to the provided format, and the tz="UTC" argument ensures the resulting DateTime value is in UTC.
  • datetime.ISO.DATETIME_TZ is a convenient constant for the common ISO format with timezone.

Format Date and DateTime values as strings

Section titled “Format Date and DateTime values as strings”

Use datetime.format() to produce formatted strings from Date and DateTime values:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std.datetime import datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
4 collapsed lines
m.define(
Ticket.new(id=701, created_at=datetime(2026, 2, 1, 9, 0, 0, tz="UTC")),
Ticket.new(id=702, created_at=datetime(2026, 2, 10, 16, 45, 0, tz="UTC")),
)
formatted = datetime.format(Ticket.created_at, "Y-m-d H:M:S Z")
df = m.select(Ticket.id, Ticket.created_at, formatted.alias("created_at_fmt")).to_df()
print(df)
  • datetime(..., tz="UTC") creates the datetime in UTC.
  • datetime.format(...) formats the datetime into a string using the provided format string.
  • The format string "Y-m-d H:M:S Z" produces an ISO-like format with timezone offset.
  • datetime.format() uses Julia-style format strings, which are different from Python’s strftime format codes.

Use part methods like datetime.to_date(), datetime.weekday(), and datetime.hour() to extract components from DateTime values for bucketing and comparisons:

from relationalai.semantics import Date, DateTime, Integer, Model
from relationalai.semantics.std.datetime import datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
Ticket.created_on = m.Property(f"{Ticket} created on {Date:created_on}")
Ticket.weekday = m.Property(f"{Ticket} weekday {Integer:weekday}")
Ticket.hour = m.Property(f"{Ticket} hour {Integer:hour}")
WeekendTicket = m.Concept("WeekendTicket", extends=[Ticket])
4 collapsed lines
m.define(
Ticket.new(id=702, created_at=datetime(2024, 6, 13, 14, 30, tz="UTC")),
Ticket.new(id=703, created_at=datetime(2024, 6, 15, 10, 0, tz="UTC")),
)
m.define(
Ticket.created_on(datetime.to_date(Ticket.created_at)),
Ticket.weekday(datetime.weekday(Ticket.created_at)), # 0=Mon ... 6=Sun
Ticket.hour(datetime.hour(Ticket.created_at)),
)
m.define(WeekendTicket(Ticket)).where((Ticket.weekday == 5) | (Ticket.weekday == 6))
df = m.select(
WeekendTicket.id,
WeekendTicket.created_on,
WeekendTicket.weekday,
WeekendTicket.hour,
).to_df()
print(df)
  • datetime.to_date(...) converts DateTime to Date before date-based extraction.
  • datetime.weekday(...) returns 0 for Monday through 6 for Sunday.
  • datetime.hour(...) extracts the hour using the datetime’s timezone (or UTC).

Use time period constructors like datetime.days(), datetime.hours(), and datetime.minutes() to add or subtract time from Date and DateTime values for relative time conditions:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std import datetime as dt
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime}")
Ticket.due_by = m.Property(f"{Ticket} due by {DateTime}")
4 collapsed lines
m.define(
Ticket.new(id=801, created_at=dt.datetime(2024, 6, 1, 12, 0, 0)),
Ticket.new(id=802, created_at=dt.datetime(2024, 6, 2, 15, 30, 0)),
)
due_date = dt.datetime.add(Ticket.created_at, dt.days(7))
m.define(Ticket.due_at(due_date))
df = m.select(Ticket.id, Ticket.created_at, Ticket.due_at).to_df()
print(df)
  • datetime.days(7) creates a time period of 7 days that you can add to or subtract from a DateTime value.
  • datetime.subtract(...) subtracts the time period from the datetime, resulting in a new DateTime cutoff boundary.

Use datetime.datetime.period_milliseconds() to compute elapsed time in milliseconds between two DateTime values, then apply transforms to convert to other units or compare against a value:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std import numbers, datetime as dt
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
Ticket.first_response_at = m.Property(f"{Ticket} first responded to at {DateTime:first_response_at}")
Ticket.response_sla_breached = m.Property(f"{Ticket} breached response SLA")
17 collapsed lines
m.define(
Ticket.new(
id=1001,
created_at=dt.datetime(2026, 2, 1, 9, 0, 0, tz="UTC"),
first_response_at=dt.datetime(2026, 2, 1, 11, 0, 0, tz="UTC"),
),
Ticket.new(
id=1002,
created_at=dt.datetime(2026, 2, 1, 9, 0, 0, tz="UTC"),
first_response_at=dt.datetime(2026, 2, 1, 15, 30, 0, tz="UTC"),
),
)
elapsed_ms = dt.datetime.period_milliseconds(Ticket.created_at, Ticket.first_response_at)
SLA_4_HOURS_MS = numbers.integer(4 * 60 * 60 * 1000)
m.define(Ticket.response_sla_breached).where(elapsed_ms > SLA_4_HOURS_MS)
df = m.select(
Ticket.id,
elapsed_ms.alias("response_ms"),
Ticket.response_sla_breached.as_bool().alias("breached_ticket_id"),
).to_df()
print(df)
  • datetime.datetime.period_milliseconds(...) computes the elapsed time in milliseconds between the two datetimes.
  • The SLA threshold is defined as a constant in milliseconds for the comparison.
  • The resulting Ticket.response_sla_breached property is a unary property that represents whether the SLA was breached.
  • In the select(), Ticket.response_sla_breached.as_bool() converts the unary property into a boolean flag for easier interpretation in the output.

Use datetime.date.range() and datetime.datetime.range() to build calendar and time windows for bucketing and filtering:

from relationalai.semantics import Date, Integer, Model
from relationalai.semantics.std import aggregates, datetime as dt
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.created_on = m.Property(f"{Ticket} created on {Date}")
START_DATE = dt.date(2024, 6, 1)
5 collapsed lines
m.define(
Ticket.new(id=1201, created_on=dt.date.subtract(START_DATE, dt.days(1))),
Ticket.new(id=1202, created_on=dt.date.subtract(START_DATE, dt.days(1))),
Ticket.new(id=1203, created_on=dt.date.subtract(START_DATE, dt.days(3))),
)
# Create a 4-day date range ending at START_DATE.
day = dt.date.range(end=START_DATE, periods=4, freq="D")
# Count how many tickets were created on each day in the range.
tickets_created = (
aggregates.count(Ticket).per(day).where(Ticket.created_on == day)
) | 0 # Fill in 0 for days with no tickets created
df = m.select(day.alias("day"), tickets_created.alias("tickets_created")).to_df()
print(df)
  • datetime.date.range(...) creates a range of 4 dates ending at START_DATE with a daily frequency.
  • aggregates.count(Ticket).per(day) counts how many tickets were created on each day in the range.
  • .where(Ticket.created_on == day) filters the tickets to those created on the current day in the range.
  • The | 0 fills in a count of 0 for any days in the range where no tickets were created.
  • datetime.date.range() and datetime.datetime.range() treat the end parameter as inclusive, so the range includes the START_DATE in this example.
  • The periods parameter specifies how many time periods to include in the range, and freq="D" specifies that the periods are days.
  • Other frequency options include "ms" for milliseconds, "s" for seconds, "m" for minutes, "H" for hours, "W" for weeks, "M" for months, and "Y" for years.

Time-based definitions often fail in subtle ways. The most common causes are type mismatches, timezone assumptions, and off-by-one boundaries.

Use this checklist when results are empty, shifted, or off by one day:

  • Verify types: compare Date to Date and DateTime to DateTime.
  • Set timezone on purpose: pass tz when you construct or parse datetimes, and when you extract local components.
  • Check range boundaries: date.range(..., end=...) and datetime.range(..., end=...) treat end as inclusive.
  • Keep token sets separate: range freq uses tokens like "m" (minutes) and "M" (months), and formatting uses datetime format strings and ISO constants like datetime.ISO.SECONDS_TZ.
  • Validate in small steps: select() raw timestamps, then your computed boundary, then the final boolean condition.