pendulum Interval ranges and DateTime timezone conversion: working versions (tested 2026-10-16)
Build day and month ranges with pendulum Interval (Period in pendulum 2), convert the same instant between timezones, and see how DST and DateTime subtraction behave, tested fully offline.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pendulum-period-range") (install), or see them on GitHub.
pendulum==3.3.0 pytest==9.1.1
from __future__ import annotations
from collections.abc import Iterator
import pendulum
from pendulum import DateTime, Interval
def day_range(start: DateTime, end: DateTime) -> Iterator[DateTime]:
return pendulum.interval(start, end).range("days")
def month_anchors(start: DateTime, end: DateTime) -> Iterator[DateTime]:
return pendulum.interval(start, end).range("months")
def to_timezone(dt: DateTime, tz: str) -> DateTime:
return dt.in_timezone(tz)
def span(start: DateTime, end: DateTime) -> Interval:
return end - start
import pendulum
import pytest
from ranges import day_range, month_anchors, span, to_timezone
def test_day_range_stops_before_the_end():
start = pendulum.datetime(2024, 1, 1, 10, 30, tz="UTC")
end = pendulum.datetime(2024, 1, 3, 9, 0, tz="UTC")
assert [d.to_date_string() for d in day_range(start, end)] == [
"2024-01-01",
"2024-01-02",
]
def test_month_anchors_keep_the_start_day():
start = pendulum.datetime(2024, 1, 15, tz="UTC")
end = pendulum.datetime(2024, 4, 1, tz="UTC")
assert [d.to_date_string() for d in month_anchors(start, end)] == [
"2024-01-15",
"2024-02-15",
"2024-03-15",
]
def test_conversion_keeps_the_instant():
moment = pendulum.datetime(2024, 3, 9, 12, tz="UTC")
tokyo = to_timezone(moment, "Asia/Tokyo")
assert tokyo == moment
assert (tokyo.timezone_name, tokyo.hour) == ("Asia/Tokyo", 21)
def test_day_range_across_dst_keeps_local_midnight():
start = pendulum.datetime(2024, 3, 9, tz="America/New_York")
end = pendulum.datetime(2024, 3, 11, tz="America/New_York")
days = list(day_range(start, end))
assert [d.hour for d in days] == [0, 0, 0]
assert [d.in_timezone("UTC").hour for d in days] == [5, 5, 4]
def test_subtracting_datetimes_returns_an_interval():
start = pendulum.datetime(2024, 3, 9, tz="America/New_York")
end = pendulum.datetime(2024, 3, 11, tz="America/New_York")
diff = span(start, end)
assert isinstance(diff, pendulum.Interval)
assert diff.in_days() == 2
assert diff.total_days() == pytest.approx(47 / 24)
def test_inverted_range_iterates_backwards():
interval = pendulum.interval(
pendulum.datetime(2024, 1, 5, tz="UTC"),
pendulum.datetime(2024, 1, 1, tz="UTC"),
)
assert interval.invert is True
assert [d.to_date_string() for d in interval.range("days")] == [
"2024-01-05",
"2024-01-04",
"2024-01-03",
"2024-01-02",
"2024-01-01",
]
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
croniter next run times as pendulum timezone-aware datetimes
Compute the next cron fire times as tz-aware pendulum DateTimes, handling DST gaps and folds, with focused offline tests.
Pendulum DateTime arithmetic and timezone shifts frozen with time-machine
Freeze the clock with time-machine to test Pendulum month-end clamping, DST-safe day additions and timezone shifts fully offline.