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}")
m.define(
Ticket.new(
id=201,
created_at=datetime(2026, 2, 1, 9, 0, 0, tz="UTC"),
due_by=date(2026, 2, 8),
),
Ticket.new(
id=202,
created_at=datetime(2026, 2, 10, 16, 45, 0, tz="UTC"),
due_by=date(2026, 2, 17),
),
)
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")
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, ISO provides common format strings as constants:

from relationalai.semantics import DateTime, Integer, Model
from relationalai.semantics.std.datetime import ISO, 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, ISO.SECONDS_TZ)
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.
  • ISO.SECONDS_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_at = m.Property(f"{Ticket} due at {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.diff() to measure the whole number of days, hours, or other units between two DateTime values:

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:created_at}")
Ticket.first_response_at = m.Property(f"{Ticket} first responded to at {DateTime:first_response_at}")
Ticket.response_hours = m.Property(f"{Ticket} response hours {Integer:response_hours}")
Ticket.response_sla_breached = m.Property(f"{Ticket} breached response SLA")
12 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"),
),
)
m.define(
Ticket.response_hours(
dt.datetime.diff("hour", Ticket.created_at, Ticket.first_response_at)
)
)
m.define(Ticket.response_sla_breached).where(Ticket.response_hours > 4)
df = m.select(
Ticket.id,
Ticket.response_hours,
Ticket.response_sla_breached.as_bool().alias("sla_breached"),
).to_df()
print(df)
  • datetime.diff("hour", start, end) returns the whole number of hours between the two datetimes, computed as end - start.
  • The derived Ticket.response_hours property stores that count so you can reuse it and display it.
  • Ticket.response_sla_breached is a unary property that holds only for tickets whose response took more than four hours.
  • In the select(), Ticket.response_sla_breached.as_bool() converts the unary property into a boolean flag for easier interpretation in the output.
  • Pass the unit as a singular string, such as "day", "hour", or "minute".
  • datetime.diff() returns a whole-number count of the unit, not a fractional amount. When you need exact sub-unit precision, use datetime.period_milliseconds() to get the gap in milliseconds and convert.
  • To shift a single value forward or backward by a time period instead of measuring a gap, use datetime.add() or datetime.subtract(), as shown in Add or subtract time periods.

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 ISO.SECONDS_TZ.
  • Validate in small steps: select() raw timestamps, then your computed boundary, then the final boolean condition.