Python’s datetime module represents dates, times, and durations. from datetime import date, time, datetime, timedelta, timezone, UTC. date: d = date(2024, 1, 15); d.year, d.month, d.day; date.today(). time: t = time(10, 30, 0). datetime: dt = datetime(2024, 1, 15, 10, 30, 0). now: datetime.now(); datetime.now(UTC) — UTC-aware. utcnow: datetime.utcnow() — naive UTC (prefer datetime.now(UTC)). fromisoformat: datetime.fromisoformat("2024-01-15T10:30:00"). isoformat: dt.isoformat() → “2024-01-15T10:30:00”. strftime: dt.strftime("%Y-%m-%d %H:%M:%S"). strptime: datetime.strptime("01/15/2024", "%m/%d/%Y"). timedelta: td = timedelta(days=7, hours=2, minutes=30); dt + td; dt - td; td.total_seconds(). replace: dt.replace(hour=0, minute=0, second=0, microsecond=0) — start of day. combine: datetime.combine(date(2024,1,15), time(10,30)). timestamp: dt.timestamp() — Unix epoch float. fromtimestamp: datetime.fromtimestamp(ts, tz=UTC). astimezone: dt.astimezone(target_tz). UTC: from datetime import UTC (Python 3.11+) or timezone.utc. zoneinfo: from zoneinfo import ZoneInfo; dt.replace(tzinfo=ZoneInfo("America/New_York")). is_aware: dt.tzinfo is not None. weekday: d.weekday() → 0=Mon. date.fromordinal/toordinal. date arithmetic: (date.today() - date(2000,1,1)).days. Claude Code generates date range iterators, business-day calculators, ISO 8601 parsers, and timezone converters.
CLAUDE.md for datetime
## datetime Stack
- Stdlib: from datetime import date, time, datetime, timedelta, timezone, UTC
- Timezone: from zoneinfo import ZoneInfo — IANA zones (Python 3.9+)
- Now (aware): datetime.now(UTC) — always prefer over utcnow() (naive)
- Parse: datetime.fromisoformat(s) | datetime.strptime(s, fmt)
- Format: dt.isoformat() | dt.strftime("%Y-%m-%d") | dt.timestamp()
- Arithmetic: dt + timedelta(days=N) | (dt2 - dt1).total_seconds()
datetime Date and Time Pipeline
# app/dtutil.py — datetime, timedelta, timezone, zoneinfo, formatters, ranges
from __future__ import annotations
import calendar
from datetime import UTC, date, datetime, time, timedelta, timezone
from typing import Iterator
try:
from zoneinfo import ZoneInfo
except ImportError: # Python < 3.9 fallback
ZoneInfo = None # type: ignore[assignment,misc]
# ─────────────────────────────────────────────────────────────────────────────
# 1. Aware datetime helpers
# ─────────────────────────────────────────────────────────────────────────────
def now_utc() -> datetime:
"""
Return current time as UTC-aware datetime.
Prefer over datetime.utcnow() which returns a naive datetime.
Example:
ts = now_utc()
print(ts.isoformat()) # 2024-01-15T10:30:00+00:00
"""
return datetime.now(UTC)
def now_local() -> datetime:
"""Return current local time with UTC offset attached."""
return datetime.now().astimezone()
def as_utc(dt: datetime) -> datetime:
"""
Convert a naive or aware datetime to UTC.
Naive datetimes are assumed to already be UTC.
Example:
as_utc(datetime(2024, 1, 15, 10, 30)) # → 2024-01-15T10:30:00+00:00
"""
if dt.tzinfo is None:
return dt.replace(tzinfo=UTC)
return dt.astimezone(UTC)
def to_timezone(dt: datetime, tz_name: str) -> datetime:
"""
Convert datetime to a named IANA timezone.
Example:
ny_time = to_timezone(now_utc(), "America/New_York")
print(ny_time.strftime("%Y-%m-%d %H:%M %Z"))
"""
if ZoneInfo is None:
raise RuntimeError("zoneinfo requires Python 3.9+ or the backports.zoneinfo package")
tz = ZoneInfo(tz_name)
return as_utc(dt).astimezone(tz)
def is_aware(dt: datetime) -> bool:
"""Return True if dt has timezone info attached."""
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
# ─────────────────────────────────────────────────────────────────────────────
# 2. Parsing and formatting
# ─────────────────────────────────────────────────────────────────────────────
_COMMON_FORMATS = [
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%d/%m/%Y %H:%M:%S",
"%d/%m/%Y",
"%m/%d/%Y",
]
def parse_datetime(s: str) -> datetime:
"""
Parse a datetime string trying common formats.
Raises ValueError if no format matches.
Example:
parse_datetime("2024-01-15")
parse_datetime("2024-01-15T10:30:00Z")
parse_datetime("01/15/2024")
"""
# Try fromisoformat first (fastest, handles ISO 8601)
try:
return datetime.fromisoformat(s.rstrip("Z").replace("Z", "+00:00"))
except ValueError:
pass
for fmt in _COMMON_FORMATS:
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
raise ValueError(f"Cannot parse datetime: {s!r}")
def parse_date(s: str) -> date:
"""
Parse a date string (YYYY-MM-DD or common variants).
Example:
parse_date("2024-01-15") # date(2024, 1, 15)
parse_date("15/01/2024") # date(2024, 1, 15)
"""
for fmt in ["%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%d-%m-%Y"]:
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
raise ValueError(f"Cannot parse date: {s!r}")
def fmt_iso(dt: datetime) -> str:
"""Format as compact ISO 8601 string."""
return dt.strftime("%Y-%m-%dT%H:%M:%S")
def fmt_human(dt: datetime) -> str:
"""Format as human-readable date/time string."""
return dt.strftime("%B %-d, %Y at %-I:%M %p")
def fmt_date(d: date) -> str:
"""Format date as YYYY-MM-DD."""
return d.strftime("%Y-%m-%d")
def days_ago(n: int, tz: datetime = None) -> datetime:
"""
Return datetime n days ago at midnight UTC.
Example:
since = days_ago(30) # 30 days ago
"""
base = now_utc().replace(hour=0, minute=0, second=0, microsecond=0)
return base - timedelta(days=n)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Date arithmetic
# ─────────────────────────────────────────────────────────────────────────────
def start_of_day(dt: datetime) -> datetime:
"""
Truncate datetime to midnight (start of day), preserving timezone.
Example:
start_of_day(datetime(2024, 1, 15, 10, 30, tzinfo=UTC))
# datetime(2024, 1, 15, 0, 0, tzinfo=UTC)
"""
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
def end_of_day(dt: datetime) -> datetime:
"""Return 23:59:59.999999 of the given day."""
return dt.replace(hour=23, minute=59, second=59, microsecond=999999)
def start_of_month(dt: datetime) -> datetime:
"""Return midnight on the 1st of the given month."""
return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
def end_of_month(dt: datetime) -> datetime:
"""Return the last moment of the given month."""
last_day = calendar.monthrange(dt.year, dt.month)[1]
return dt.replace(day=last_day, hour=23, minute=59, second=59, microsecond=999999)
def add_months(dt: datetime, months: int) -> datetime:
"""
Add (or subtract) months, clamping day to last valid day.
Example:
add_months(datetime(2024, 1, 31), 1) # 2024-02-29 (leap year)
add_months(datetime(2024, 3, 31), -1) # 2024-02-29
"""
month = dt.month - 1 + months
year = dt.year + month // 12
month = month % 12 + 1
day = min(dt.day, calendar.monthrange(year, month)[1])
return dt.replace(year=year, month=month, day=day)
def is_business_day(d: date) -> bool:
"""
Return True if d is Monday–Friday (ignores public holidays).
Example:
is_business_day(date(2024, 1, 15)) # True (Monday)
is_business_day(date(2024, 1, 13)) # False (Saturday)
"""
return d.weekday() < 5
def next_business_day(d: date) -> date:
"""
Return the next business day after d.
Example:
next_business_day(date(2024, 1, 12)) # 2024-01-15 (Mon)
"""
d = d + timedelta(days=1)
while not is_business_day(d):
d += timedelta(days=1)
return d
def business_days_between(start: date, end: date) -> int:
"""
Count business days in the half-open interval [start, end).
Example:
business_days_between(date(2024, 1, 15), date(2024, 1, 22)) # 5
"""
count = 0
d = start
while d < end:
if is_business_day(d):
count += 1
d += timedelta(days=1)
return count
# ─────────────────────────────────────────────────────────────────────────────
# 4. Date range iterators
# ─────────────────────────────────────────────────────────────────────────────
def date_range(start: date, end: date, step: int = 1) -> Iterator[date]:
"""
Yield dates from start to end (exclusive) by step days.
Example:
list(date_range(date(2024,1,1), date(2024,1,5)))
# [2024-01-01, 2024-01-02, 2024-01-03, 2024-01-04]
"""
d = start
delta = timedelta(days=step)
while d < end:
yield d
d += delta
def month_range(year: int, month: int) -> Iterator[date]:
"""
Yield every date in the given month.
Example:
days = list(month_range(2024, 2)) # all 29 days of Feb 2024
"""
last = calendar.monthrange(year, month)[1]
return date_range(date(year, month, 1), date(year, month, last + 1))
def week_range(d: date) -> tuple[date, date]:
"""
Return (monday, sunday) of the ISO week containing d.
Example:
week_range(date(2024, 1, 17)) # (2024-01-15, 2024-01-21)
"""
monday = d - timedelta(days=d.weekday())
sunday = monday + timedelta(days=6)
return monday, sunday
# ─────────────────────────────────────────────────────────────────────────────
# 5. Duration helpers
# ─────────────────────────────────────────────────────────────────────────────
def human_duration(seconds: float) -> str:
"""
Format a duration in seconds as a human-readable string.
Example:
human_duration(3661) # "1h 1m 1s"
human_duration(90) # "1m 30s"
human_duration(0.5) # "0.500s"
"""
if seconds < 1:
return f"{seconds:.3f}s"
s = int(seconds)
m, s = divmod(s, 60)
h, m = divmod(m, 60)
d, h = divmod(h, 24)
parts = []
if d:
parts.append(f"{d}d")
if h:
parts.append(f"{h}h")
if m:
parts.append(f"{m}m")
if s or not parts:
parts.append(f"{s}s")
return " ".join(parts)
def age(dt: datetime, reference: datetime | None = None) -> timedelta:
"""
Return age (elapsed time) of a datetime relative to reference (default: now).
Example:
born = datetime(1990, 6, 15, tzinfo=UTC)
print(age(born).days // 365, "years old")
"""
ref = reference or now_utc()
return as_utc(ref) - as_utc(dt)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== datetime demo ===")
print(f"\n--- now (UTC-aware) ---")
print(f" now_utc(): {now_utc().isoformat()}")
print(f" now_local(): {now_local().isoformat()}")
print(f"\n--- parse_datetime ---")
for s in ["2024-01-15", "2024-01-15T10:30:00Z", "2024-01-15 10:30", "01/15/2024"]:
try:
dt = parse_datetime(s)
print(f" {s!r:35s} → {dt.isoformat()!r}")
except ValueError as e:
print(f" {s!r}: {e}")
print(f"\n--- date arithmetic ---")
dt = datetime(2024, 1, 15, 10, 30, tzinfo=UTC)
print(f" start_of_day: {start_of_day(dt).isoformat()}")
print(f" end_of_day: {end_of_day(dt).isoformat()}")
print(f" start_of_month: {start_of_month(dt).isoformat()}")
print(f" add_months(+1): {add_months(dt, 1).isoformat()}")
print(f" add_months(+13):{add_months(dt, 13).isoformat()}")
print(f"\n--- business days ---")
d1 = date(2024, 1, 15) # Monday
d2 = date(2024, 1, 22) # Monday
print(f" is_business_day(Mon): {is_business_day(d1)}")
print(f" is_business_day(Sat): {is_business_day(date(2024,1,13))}")
print(f" business_days_between: {business_days_between(d1, d2)}")
print(f" next_business_day(Fri): {next_business_day(date(2024,1,12))}")
print(f"\n--- date_range ---")
days = list(date_range(date(2024,1,1), date(2024,1,6)))
print(f" {[str(d) for d in days]}")
print(f"\n--- week_range ---")
mon, sun = week_range(date(2024, 1, 17))
print(f" week containing 2024-01-17: {mon} → {sun}")
print(f"\n--- human_duration ---")
for s in [0.5, 90, 3661, 90061, 86400*3 + 7261]:
print(f" {s:10.1f}s → {human_duration(s)!r}")
print(f"\n--- age ---")
born = datetime(1990, 6, 15, tzinfo=UTC)
ref = datetime(2024, 6, 15, tzinfo=UTC)
td = age(born, ref)
print(f" age at 2024-06-15: {td.days} days ({td.days//365} years)")
print("\n=== done ===")
For the pendulum alternative — pendulum (PyPI) wraps Python datetime with a friendlier API: pendulum.now("UTC"), pendulum.parse("2024-01-15"), duration arithmetic with add(months=3), and human-readable diff_for_humans() output; Python’s stdlib datetime + zoneinfo handles timezone-aware arithmetic without dependencies — use pendulum for date-math-heavy code where add_months(), natural language output, or Period intervals matter, stdlib datetime + zoneinfo for applications where minimizing dependencies is important. For the arrow alternative — arrow (PyPI) provides a unified Arrow object combining date + time + timezone + formatting with a fluent API: arrow.utcnow().shift(days=+7).format("YYYY-MM-DD"); stdlib datetime is more verbose but is always available and integrates directly with json, csv, sqlite3, and other stdlib modules — use arrow for rapid prototyping needing elegant date shift syntax, stdlib datetime for production libraries and applications where the UTC/aware discipline in this post’s helpers is sufficient. The Claude Skills 360 bundle includes datetime skill sets covering now_utc()/now_local()/as_utc()/to_timezone()/is_aware() awareness helpers, parse_datetime()/parse_date()/fmt_iso()/fmt_human() parsing and formatting, start_of_day()/end_of_day()/start_of_month()/add_months() arithmetic, is_business_day()/next_business_day()/business_days_between() business-day logic, date_range()/month_range()/week_range() iterators, and human_duration()/age() duration utilities. Start with the free tier to try date arithmetic and datetime pipeline code generation.