arrow is a human-friendly Python datetime library. pip install arrow. Now: import arrow; arrow.now() — local time with tzinfo. UTC: arrow.utcnow(). Parse: arrow.get("2024-01-15T09:30:00") — ISO 8601. Epoch: arrow.get(1700000000). String+format: arrow.get("15 Jan 2024", "DD MMM YYYY"). Timezone: arrow.now("America/New_York"). shift: arrow.utcnow().shift(days=+7, hours=-3). replace: a.replace(year=2025). Format: a.format("YYYY-MM-DD HH:mm:ss"). Human: a.humanize() → “2 hours ago”. Humanize from: a.humanize(other, granularity=["hour","minute"]). to: arrow.utcnow().to("Asia/Tokyo"). timestamp: a.timestamp() → float epoch. int: int(a.timestamp()). fromtimestamp: arrow.Arrow.fromtimestamp(ts). fromdatetime: arrow.Arrow.fromdatetime(dt). datetime: a.datetime — naive Python datetime. fold: daylight saving disambiguation. floor: a.floor("hour") — truncate to hour. ceil: a.ceil("day") — end of day. span: a.span("day") → (start, end). span_range: arrow.Arrow.span_range("week", start, end) — iterate weeks. range: arrow.Arrow.range("month", start, end). between: a.is_between(start, end). isoformat: a.isoformat(). date: a.date(). time: a.time(). year/month/day/hour/minute/second: attrs. Claude Code generates arrow date parsers, timezone converters, humanized displays, and calendar pipelines.
CLAUDE.md for arrow
## arrow Stack
- Version: arrow >= 1.3 | pip install arrow
- Now: arrow.utcnow() | arrow.now("America/New_York")
- Parse: arrow.get("2024-01-15") | arrow.get(epoch_float) | arrow.get(dt_obj)
- Shift: a.shift(days=+1, hours=-3, weeks=+1) — immutable
- Format: a.format("YYYY-MM-DD") | a.humanize() | a.isoformat()
- Convert: a.to("UTC") | a.timestamp() | a.datetime | a.date()
arrow Datetime Pipeline
# app/dates.py — arrow parsing, shifting, formatting, timezone conversion, humanize, ranges
from __future__ import annotations
import re
from typing import Generator
import arrow
from arrow import Arrow
# ─────────────────────────────────────────────────────────────────────────────
# 1. Construction helpers
# ─────────────────────────────────────────────────────────────────────────────
def now(tz: str = "UTC") -> Arrow:
"""
Current time in the given timezone.
Example:
now() # UTC
now("America/New_York") # Eastern
now("Europe/London") # London
"""
return arrow.now(tz)
def utcnow() -> Arrow:
"""Current UTC time."""
return arrow.utcnow()
def parse(
value: str | int | float,
fmt: str | list[str] | None = None,
tz: str = "UTC",
) -> Arrow:
"""
Parse a datetime from a string, int, or float.
fmt: strptime-style format or list of formats to try.
Defaults to ISO 8601 / epoch detection.
Example:
parse("2024-01-15") # ISO date
parse("15/01/2024", fmt="DD/MM/YYYY") # custom
parse(1700000000) # Unix epoch
parse("Jan 15, 2024 9:30am", fmt="MMM DD, YYYY h:mma")
"""
if fmt:
return arrow.get(value, fmt, tzinfo=tz)
return arrow.get(value, tzinfo=tz) if isinstance(value, str) and not value.isdigit() else arrow.get(value)
def try_parse(
value: str | None,
fmt: str | list[str] | None = None,
default: Arrow | None = None,
tz: str = "UTC",
) -> Arrow | None:
"""
Safe parse — returns default instead of raising on failure.
Example:
ts = try_parse(row.get("created_at"), default=arrow.utcnow())
"""
if not value:
return default
try:
return parse(value, fmt=fmt, tz=tz)
except Exception:
return default
def from_epoch(ts: int | float, tz: str = "UTC") -> Arrow:
"""
Create Arrow from a Unix timestamp.
Example:
from_epoch(1700000000) # second-precision
from_epoch(1700000000.123) # millisecond-precision
from_epoch(1700000000123, tz="UTC") # ms integer — divide by 1000 first
"""
if ts > 1e12: # milliseconds
ts /= 1000
return arrow.Arrow.fromtimestamp(ts, tzinfo=tz)
def from_parts(
year: int,
month: int,
day: int,
hour: int = 0,
minute: int = 0,
second: int = 0,
tz: str = "UTC",
) -> Arrow:
"""
Build an Arrow from explicit date/time parts.
Example:
from_parts(2024, 1, 15, 9, 30, tz="America/New_York")
"""
return arrow.Arrow(year, month, day, hour, minute, second, tzinfo=tz)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Arithmetic / manipulation
# ─────────────────────────────────────────────────────────────────────────────
def shift(
dt: Arrow,
years: int = 0,
months: int = 0,
weeks: int = 0,
days: int = 0,
hours: int = 0,
minutes: int = 0,
seconds: int = 0,
) -> Arrow:
"""
Shift an Arrow by the given offsets. All args can be negative.
Example:
tomorrow = shift(now(), days=+1)
last_quarter = shift(now(), months=-3)
next_friday = shift(start_of_week(), weeks=+1, days=-1)
"""
return dt.shift(
years=years, months=months, weeks=weeks,
days=days, hours=hours, minutes=minutes, seconds=seconds,
)
def floor_dt(dt: Arrow, frame: str) -> Arrow:
"""
Truncate to the start of the given time frame.
frame: "year" | "month" | "week" | "day" | "hour" | "minute" | "second"
Example:
floor_dt(now(), "day") # midnight today
floor_dt(now(), "month") # 1st of current month at 00:00
floor_dt(now(), "hour") # current hour at :00
"""
return dt.floor(frame)
def ceil_dt(dt: Arrow, frame: str) -> Arrow:
"""
Round up to end of the given time frame.
Example:
ceil_dt(now(), "day") # 23:59:59.999999 today
ceil_dt(now(), "week") # last second of Sunday
"""
return dt.ceil(frame)
def clamp(dt: Arrow, start: Arrow, end: Arrow) -> Arrow:
"""Clamp dt to within [start, end]."""
if dt < start:
return start
if dt > end:
return end
return dt
# ─────────────────────────────────────────────────────────────────────────────
# 3. Formatting
# ─────────────────────────────────────────────────────────────────────────────
def fmt(dt: Arrow, pattern: str = "YYYY-MM-DD HH:mm:ss ZZ") -> str:
"""
Format an Arrow to a string using arrow's token format.
Tokens: YYYY MM DD HH mm ss ZZ (offset) z (tz name).
Example:
fmt(now()) # "2024-01-15 09:30:00 +00:00"
fmt(dt, "MMM D, YYYY") # "Jan 15, 2024"
fmt(dt, "ddd, DD MMM YYYY HH:mm") # "Mon, 15 Jan 2024 09:30"
"""
return dt.format(pattern)
def iso(dt: Arrow) -> str:
"""Return ISO 8601 string (includes timezone offset)."""
return dt.isoformat()
def humanize(
dt: Arrow,
other: Arrow | None = None,
granularity: list[str] | None = None,
locale: str = "en",
) -> str:
"""
Return a human-readable time difference.
other: compare to this Arrow (default: now).
granularity: e.g. ["hour", "minute"] for "2 hours 30 minutes ago"
Example:
humanize(parse("2024-01-14")) # "a day ago"
humanize(shift(now(), hours=-3)) # "3 hours ago"
humanize(shift(now(), days=+5)) # "in 5 days"
humanize(dt, granularity=["day","hour"]) # "2 days 3 hours ago"
"""
kwargs: dict = {"locale": locale}
if other:
kwargs["other"] = other
if granularity:
kwargs["granularity"] = granularity
return dt.humanize(**kwargs)
def relative_label(dt: Arrow, now_dt: Arrow | None = None) -> str:
"""
Return a display label: "Today", "Yesterday", "Tomorrow", or humanized.
Example:
relative_label(parse("2024-01-15")) # "Today" (if today is the 15th)
"""
ref = (now_dt or arrow.utcnow()).to(dt.tzinfo)
today = ref.floor("day")
d = dt.floor("day")
if d == today:
return "Today"
if d == today.shift(days=-1):
return "Yesterday"
if d == today.shift(days=+1):
return "Tomorrow"
diff = (d - today).days
if abs(diff) < 7:
return dt.format("dddd") # "Monday", "Tuesday", ...
return humanize(dt, other=ref)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Timezone conversion
# ─────────────────────────────────────────────────────────────────────────────
def to_tz(dt: Arrow, tz: str) -> Arrow:
"""
Convert an Arrow to a different timezone.
Example:
utc_dt = parse("2024-01-15T14:00:00+00:00")
ny = to_tz(utc_dt, "America/New_York") # 09:00
tokyo = to_tz(utc_dt, "Asia/Tokyo") # 23:00
"""
return dt.to(tz)
def to_utc(dt: Arrow) -> Arrow:
"""Convert any Arrow to UTC."""
return dt.to("UTC")
def localize(epoch: int | float, tz: str) -> Arrow:
"""Convert a Unix timestamp to a localized Arrow."""
return from_epoch(epoch, tz=tz)
def world_times(dt: Arrow, zones: list[str] | None = None) -> dict[str, Arrow]:
"""
Return dt expressed in multiple timezones.
Example:
times = world_times(arrow.utcnow())
for tz_name, local in times.items():
print(f"{tz_name}: {fmt(local, 'HH:mm')}")
"""
default_zones = [
"UTC", "America/New_York", "America/Los_Angeles",
"Europe/London", "Europe/Paris", "Asia/Tokyo", "Australia/Sydney"
]
zones = zones or default_zones
return {z: dt.to(z) for z in zones}
# ─────────────────────────────────────────────────────────────────────────────
# 5. Ranges and spans
# ─────────────────────────────────────────────────────────────────────────────
def date_range(
start: Arrow,
end: Arrow,
frame: str = "day",
) -> Generator[Arrow, None, None]:
"""
Yield Arrow instances between start and end (inclusive) by frame.
frame: "day" | "week" | "month" | "hour" | "minute"
Example:
for day in date_range(parse("2024-01-01"), parse("2024-01-07")):
print(fmt(day, "YYYY-MM-DD"))
"""
yield from arrow.Arrow.range(frame, start, end)
def span_range(
start: Arrow,
end: Arrow,
frame: str = "month",
) -> list[tuple[Arrow, Arrow]]:
"""
Return list of (start, end) tuples for each period in the range.
Example:
for month_start, month_end in span_range(parse("2024-01-01"), parse("2024-06-30"), "month"):
print(fmt(month_start, "MMMM YYYY"))
"""
return list(arrow.Arrow.span_range(frame, start, end))
def working_days(start: Arrow, end: Arrow) -> list[Arrow]:
"""
Return a list of weekdays (Monday–Friday) between start and end.
Example:
days = working_days(parse("2024-01-01"), parse("2024-01-31"))
"""
return [
d for d in date_range(start, end, "day")
if d.weekday() < 5 # 0=Mon, 4=Fri
]
def is_between(dt: Arrow, start: Arrow, end: Arrow, inclusive: str = "both") -> bool:
"""
Return True if dt is between start and end.
inclusive: "both" | "neither" | "left" | "right"
Example:
is_between(now(), parse("2024-01-01"), parse("2024-12-31"))
"""
return dt.is_between(start, end, bounds=inclusive.replace("both","[]").replace("neither","()").replace("left","[)").replace("right","(]") if inclusive in {"both","neither","left","right"} else inclusive)
# ─────────────────────────────────────────────────────────────────────────────
# 6. Common utilities
# ─────────────────────────────────────────────────────────────────────────────
def age_in_days(dt: Arrow, ref: Arrow | None = None) -> int:
"""Days elapsed since dt (positive = past, negative = future)."""
ref = ref or arrow.utcnow()
return (ref - dt).days
def business_deadline(
start: Arrow,
days: int,
skip_weekends: bool = True,
) -> Arrow:
"""
Compute a deadline N business days from start.
Example:
deadline = business_deadline(arrow.utcnow(), 5)
"""
current = start
added = 0
while added < days:
current = current.shift(days=1)
if not skip_weekends or current.weekday() < 5:
added += 1
return current
def parse_duration(text: str) -> int:
"""
Parse a human duration string into seconds.
Supports: "2h", "30m", "1d 4h 30m", "90s"
Example:
parse_duration("1d 2h 30m") # 95400
parse_duration("90s") # 90
"""
units = {"d": 86400, "h": 3600, "m": 60, "s": 1}
total = 0
for num, unit in re.findall(r"(\d+)\s*([dhms])", text.lower()):
total += int(num) * units[unit]
return total
def time_until(dt: Arrow, ref: Arrow | None = None) -> dict:
"""
Return dict of time components until (or since) dt.
Example:
countdown = time_until(parse("2025-01-01"))
print(f"{countdown['days']}d {countdown['hours']}h until New Year")
"""
ref = ref or arrow.utcnow()
delta = dt - ref
total_seconds = int(delta.total_seconds())
sign = 1 if total_seconds >= 0 else -1
total_seconds = abs(total_seconds)
days, rem = divmod(total_seconds, 86400)
hours, rem = divmod(rem, 3600)
minutes, s = divmod(rem, 60)
return {
"past": sign < 0,
"days": days * sign,
"hours": hours,
"minutes": minutes,
"seconds": s,
"total_seconds": int(delta.total_seconds()),
}
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Construction ===")
print(f" utcnow(): {utcnow()}")
print(f" now(NY): {now('America/New_York')}")
print(f" parse ISO: {parse('2024-01-15T09:30:00+00:00')}")
print(f" parse custom: {parse('15 Jan 2024', fmt='DD MMM YYYY')}")
print(f" from_epoch: {from_epoch(1700000000)}")
print(f" from_parts: {from_parts(2024, 6, 15, 12, 0, tz='Europe/London')}")
print("\n=== Arithmetic ===")
base = parse("2024-01-15")
print(f" +7 days: {fmt(shift(base, days=7), 'YYYY-MM-DD')}")
print(f" -3 months: {fmt(shift(base, months=-3), 'YYYY-MM-DD')}")
print(f" floor month: {fmt(floor_dt(base, 'month'), 'YYYY-MM-DD')}")
print(f" ceil month: {fmt(ceil_dt(base, 'month'), 'YYYY-MM-DD')}")
print("\n=== Formatting ===")
dt = parse("2024-01-15T14:30:00+00:00")
print(f" default fmt: {fmt(dt)}")
print(f" long: {fmt(dt, 'dddd, MMMM D, YYYY [at] h:mm A')}")
print(f" iso: {iso(dt)}")
print(f" humanize: {humanize(dt)}")
print(f" relative_label: {relative_label(shift(utcnow(), days=-1))}")
print("\n=== Timezone conversion ===")
utc = parse("2024-01-15T14:00:00+00:00")
for tz_name, local in world_times(utc, ["UTC","America/New_York","Asia/Tokyo"]).items():
print(f" {tz_name:20s}: {fmt(local, 'YYYY-MM-DD HH:mm ZZ')}")
print("\n=== Ranges ===")
start = parse("2024-01-01")
end = parse("2024-01-07")
days = list(date_range(start, end))
print(f" Week days: {[fmt(d,'ddd DD') for d in days]}")
wdays = working_days(start, end)
print(f" Working days: {[fmt(d,'ddd DD') for d in wdays]}")
print("\n=== Utilities ===")
print(f" age_in_days(2024-01-01): {age_in_days(parse('2024-01-01'))} days ago")
deadline = business_deadline(parse("2024-01-15"), 5)
print(f" 5 biz days from Jan 15: {fmt(deadline, 'YYYY-MM-DD (ddd)')}")
print(f" parse_duration('1d 2h 30m'): {parse_duration('1d 2h 30m')} seconds")
countdown = time_until(parse("2025-01-01"))
print(f" time_until(2025-01-01): {countdown['days']}d {countdown['hours']}h {'ago' if countdown['past'] else 'away'}")
For the python-dateutil alternative — python-dateutil’s relativedelta handles month arithmetic and DST transitions, and parser.parse() covers a wide range of ambiguous date strings; arrow wraps relativedelta internally and adds a chainable fluent API, human-readable humanize(), and CLDR-locale support out of the box — use arrow when you want a single consistent API for parsing, shifting, formatting, and humanizing, python-dateutil when you need its low-level rrule recurrence engine or strict ambiguity control. For the pendulum alternative — pendulum has a stricter immutable API, first-class period objects, and ISO 8601 interval support; arrow has a larger install base, more lenient parsing (multi-format lists), and the humanize(locale=...) localization feature — use pendulum for calendar-math heavy applications where strict DST handling is critical, arrow for developer-friendly general datetime work including APIs, reporting, and web apps. The Claude Skills 360 bundle includes arrow skill sets covering now()/utcnow()/parse()/try_parse()/from_epoch()/from_parts() construction, shift() arithmetic, floor_dt()/ceil_dt() truncation, clamp(), fmt()/iso()/humanize()/relative_label() formatting, to_tz()/to_utc()/localize()/world_times() conversion, date_range()/span_range()/working_days() iteration, is_between(), age_in_days()/business_deadline()/parse_duration()/time_until() utilities. Start with the free tier to try human-friendly datetime parsing and conversion code generation.