FireProx Dates and Timestamps Guide¶
This notebook demonstrates how Firestore handles dates, times, and timestamps, including:
- Storing datetime objects - How Python datetime works with Firestore
- Timezone behavior - What happens to timezone information
- Querying by date/time - Filtering and ordering by timestamps
- Date ranges - Finding documents within time periods
- Duration storage - How to store time durations (TimeDelta alternatives)
- Common patterns - Real-world datetime use cases
Key Findings¶
⚠️ Important Timezone Behavior:
- Firestore always stores timestamps in UTC
- When you store a timezone-naive datetime, Firestore treats it as UTC
- When you read timestamps back, you get timezone-aware datetime in UTC
- Original timezone information is lost (converted to UTC)
📝 TimeDelta Support:
- Firestore has no native TimeDelta type
- Store durations as numbers (seconds, milliseconds, etc.)
- Convert back to TimeDelta when reading
Setup¶
Import modules for datetime handling and FireProx.
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from fire_prox import AsyncFireProx, FireProx
from fire_prox.testing import async_demo_client, demo_client
Part 1: Storing Datetime Objects¶
Let's explore how Firestore handles different types of datetime objects.
Initialize Client¶
# Create sync client and collection
client = demo_client()
db = FireProx(client)
events = db.collection('datetime_demo_events')
print("✅ Client initialized")
✅ Client initialized
Feature 1: Timezone-Naive DateTime¶
What happens when you store a datetime without timezone information?
# Create a timezone-naive datetime (no timezone info)
naive_dt = datetime(2024, 10, 12, 14, 30, 0)
print(f"📅 Original (naive): {naive_dt}")
print(f" Timezone: {naive_dt.tzinfo}")
print(f" Timezone-aware: {naive_dt.tzinfo is not None}")
# Store in Firestore
event = events.new()
event.name = "Naive DateTime Test"
event.created_at = naive_dt
event.save(doc_id='naive_test')
# Read it back
retrieved = events.doc('naive_test')
retrieved.fetch()
print(f"\n📅 Retrieved: {retrieved.created_at}")
print(f" Timezone: {retrieved.created_at.tzinfo}")
print(f" Timezone-aware: {retrieved.created_at.tzinfo is not None}")
print("\n⚠️ OBSERVATION:")
print(" - Input was timezone-NAIVE")
print(" - Firestore stored it as UTC")
print(" - Retrieved datetime is timezone-AWARE (UTC)")
print(" - Original 'naive' interpretation is lost!")
📅 Original (naive): 2024-10-12 14:30:00 Timezone: None Timezone-aware: False 📅 Retrieved: 2024-10-12 14:30:00+00:00 Timezone: UTC Timezone-aware: True ⚠️ OBSERVATION: - Input was timezone-NAIVE - Firestore stored it as UTC - Retrieved datetime is timezone-AWARE (UTC) - Original 'naive' interpretation is lost!
Feature 2: Timezone-Aware DateTime (UTC)¶
Storing an explicitly UTC datetime.
# Create a UTC timezone-aware datetime
utc_dt = datetime(2024, 10, 12, 14, 30, 0, tzinfo=timezone.utc)
print(f"📅 Original (UTC): {utc_dt}")
print(f" Timezone: {utc_dt.tzinfo}")
print(f" ISO format: {utc_dt.isoformat()}")
# Store in Firestore
event = events.new()
event.name = "UTC DateTime Test"
event.created_at = utc_dt
event.save(doc_id='utc_test')
# Read it back
retrieved = events.doc('utc_test')
retrieved.fetch()
print(f"\n📅 Retrieved: {retrieved.created_at}")
print(f" Timezone: {retrieved.created_at.tzinfo}")
print(f" ISO format: {retrieved.created_at.isoformat()}")
print("\n✅ OBSERVATION:")
print(" - Input was UTC timezone-aware")
print(" - Retrieved datetime is identical (UTC)")
print(" - This is the recommended approach!")
📅 Original (UTC): 2024-10-12 14:30:00+00:00 Timezone: UTC ISO format: 2024-10-12T14:30:00+00:00 📅 Retrieved: 2024-10-12 14:30:00+00:00 Timezone: UTC ISO format: 2024-10-12T14:30:00+00:00 ✅ OBSERVATION: - Input was UTC timezone-aware - Retrieved datetime is identical (UTC) - This is the recommended approach!
Feature 3: Non-UTC Timezone-Aware DateTime¶
What happens when you store a datetime in a different timezone?
# Create a datetime in US Eastern timezone
eastern = ZoneInfo('America/New_York')
eastern_dt = datetime(2024, 10, 12, 10, 30, 0, tzinfo=eastern)
print(f"📅 Original (Eastern): {eastern_dt}")
print(f" Timezone: {eastern_dt.tzinfo}")
print(f" ISO format: {eastern_dt.isoformat()}")
# Store in Firestore
event = events.new()
event.name = "Eastern Timezone Test"
event.created_at = eastern_dt
event.save(doc_id='eastern_test')
# Read it back
retrieved = events.doc('eastern_test')
retrieved.fetch()
print(f"\n📅 Retrieved: {retrieved.created_at}")
print(f" Timezone: {retrieved.created_at.tzinfo}")
print(f" ISO format: {retrieved.created_at.isoformat()}")
# Convert back to Eastern to compare
retrieved_eastern = retrieved.created_at.astimezone(eastern)
print(f"\n📅 Converted back to Eastern: {retrieved_eastern}")
print(f" ISO format: {retrieved_eastern.isoformat()}")
print("\n⚠️ OBSERVATION:")
print(" - Input was Eastern timezone (UTC-4 or UTC-5)")
print(" - Firestore converted to UTC automatically")
print(" - Retrieved datetime is in UTC (timezone info lost)")
print(" - You must manually convert back if needed")
print(" - The MOMENT in time is preserved, but not the timezone!")
📅 Original (Eastern): 2024-10-12 10:30:00-04:00 Timezone: America/New_York ISO format: 2024-10-12T10:30:00-04:00 📅 Retrieved: 2024-10-12 14:30:00+00:00 Timezone: UTC ISO format: 2024-10-12T14:30:00+00:00 📅 Converted back to Eastern: 2024-10-12 10:30:00-04:00 ISO format: 2024-10-12T10:30:00-04:00 ⚠️ OBSERVATION: - Input was Eastern timezone (UTC-4 or UTC-5) - Firestore converted to UTC automatically - Retrieved datetime is in UTC (timezone info lost) - You must manually convert back if needed - The MOMENT in time is preserved, but not the timezone!
Feature 4: Multiple Timezones Comparison¶
Let's store the same moment in time from different timezones.
# Same moment in time, different timezones
utc = ZoneInfo('UTC')
eastern = ZoneInfo('America/New_York')
tokyo = ZoneInfo('Asia/Tokyo')
london = ZoneInfo('Europe/London')
# Create the same moment in different timezones
base_utc = datetime(2024, 10, 12, 18, 0, 0, tzinfo=utc)
same_eastern = base_utc.astimezone(eastern)
same_tokyo = base_utc.astimezone(tokyo)
same_london = base_utc.astimezone(london)
print("🌍 Same moment in different timezones:")
print(f" UTC: {base_utc.isoformat()}")
print(f" Eastern: {same_eastern.isoformat()}")
print(f" Tokyo: {same_tokyo.isoformat()}")
print(f" London: {same_london.isoformat()}")
# Store all four versions
for tz_name, dt in [('utc', base_utc), ('eastern', same_eastern),
('tokyo', same_tokyo), ('london', same_london)]:
event = events.new()
event.name = f"Multi Timezone Test - {tz_name}"
event.created_at = dt
event.original_tz = tz_name
event.save(doc_id=f'multi_{tz_name}')
# Read them all back
print("\n📥 Retrieved from Firestore (all in UTC):")
for tz_name in ['utc', 'eastern', 'tokyo', 'london']:
retrieved = events.doc(f'multi_{tz_name}')
retrieved.fetch()
print(f" {tz_name:8s}: {retrieved.created_at.isoformat()}")
print("\n✅ OBSERVATION:")
print(" - All four datetimes represent THE SAME MOMENT")
print(" - All four stored values are IDENTICAL in Firestore (UTC)")
print(" - Firestore correctly preserves the moment in time")
print(" - Original timezone context is lost (you must track separately)")
🌍 Same moment in different timezones: UTC: 2024-10-12T18:00:00+00:00 Eastern: 2024-10-12T14:00:00-04:00 Tokyo: 2024-10-13T03:00:00+09:00 London: 2024-10-12T19:00:00+01:00 📥 Retrieved from Firestore (all in UTC): utc : 2024-10-12T18:00:00+00:00 eastern : 2024-10-12T18:00:00+00:00 tokyo : 2024-10-12T18:00:00+00:00 london : 2024-10-12T18:00:00+00:00 ✅ OBSERVATION: - All four datetimes represent THE SAME MOMENT - All four stored values are IDENTICAL in Firestore (UTC) - Firestore correctly preserves the moment in time - Original timezone context is lost (you must track separately)
Feature 5: Querying by DateTime¶
Filtering and ordering by timestamps.
# Create events at different times
now = datetime.now(timezone.utc)
events_data = [
{'name': 'Event 1', 'time': now - timedelta(days=10)},
{'name': 'Event 2', 'time': now - timedelta(days=5)},
{'name': 'Event 3', 'time': now - timedelta(days=1)},
{'name': 'Event 4', 'time': now},
{'name': 'Event 5', 'time': now + timedelta(days=1)},
]
print("📝 Creating events:")
for data in events_data:
event = events.new()
event.name = data['name']
event.timestamp = data['time']
event.save()
print(f" {data['name']}: {data['time'].isoformat()}")
# Query: Events in the past (before now)
past_query = events.where('timestamp', '<', now)
past_events = past_query.get()
print(f"\n📅 Events in the past: {len(past_events)}")
for event in past_events:
print(f" {event.name}")
# Query: Events in the future (after now)
future_query = events.where('timestamp', '>', now)
future_events = future_query.get()
print(f"\n📅 Events in the future: {len(future_events)}")
for event in future_events:
print(f" {event.name}")
# Query: Events in the last week
week_ago = now - timedelta(days=7)
recent_query = (events
.where('timestamp', '>', week_ago)
.where('timestamp', '<=', now)
.order_by('timestamp'))
recent_events = recent_query.get()
print(f"\n📅 Events in the last week: {len(recent_events)}")
for event in recent_events:
print(f" {event.name}")
📝 Creating events: Event 1: 2025-10-02T16:11:55.015046+00:00 Event 2: 2025-10-07T16:11:55.015046+00:00 Event 3: 2025-10-11T16:11:55.015046+00:00 Event 4: 2025-10-12T16:11:55.015046+00:00 Event 5: 2025-10-13T16:11:55.015046+00:00 📅 Events in the past: 3 Event 1 Event 2 Event 3 📅 Events in the future: 1 Event 5 📅 Events in the last week: 3 Event 2 Event 3 Event 4
Feature 6: Date Ranges¶
Finding documents within specific time periods.
# Define a date range: October 1-15, 2024
start_date = datetime(2024, 10, 1, tzinfo=timezone.utc)
end_date = datetime(2024, 10, 15, tzinfo=timezone.utc)
print("🔍 Searching for events between:")
print(f" Start: {start_date.isoformat()}")
print(f" End: {end_date.isoformat()}")
# Create test events across different dates
test_events = [
{'name': 'Before Range', 'date': datetime(2024, 9, 30, tzinfo=timezone.utc)},
{'name': 'Start of Range', 'date': datetime(2024, 10, 1, 12, 0, tzinfo=timezone.utc)},
{'name': 'Middle of Range', 'date': datetime(2024, 10, 8, tzinfo=timezone.utc)},
{'name': 'End of Range', 'date': datetime(2024, 10, 14, 12, 0, tzinfo=timezone.utc)},
{'name': 'After Range', 'date': datetime(2024, 10, 20, tzinfo=timezone.utc)},
]
for data in test_events:
event = events.new()
event.name = data['name']
event.event_date = data['date']
event.save()
# Query for events in range (inclusive)
range_query = (events
.where('event_date', '>=', start_date)
.where('event_date', '<=', end_date)
.order_by('event_date'))
range_results = range_query.get()
print(f"\n✅ Events in range: {len(range_results)}")
for event in range_results:
print(f" {event.name}: {event.event_date.strftime('%Y-%m-%d')}")
print("\n💡 TIP: Always use timezone-aware datetimes for range queries!")
🔍 Searching for events between: Start: 2024-10-01T00:00:00+00:00 End: 2024-10-15T00:00:00+00:00 ✅ Events in range: 3 Start of Range: 2024-10-01 Middle of Range: 2024-10-08 End of Range: 2024-10-14 💡 TIP: Always use timezone-aware datetimes for range queries!
Feature 7: Duration Storage (TimeDelta Alternative)¶
Since Firestore doesn't have a native TimeDelta type, we store durations as numbers.
print("⏱️ Duration Storage Patterns\n")
# Pattern 1: Store as total seconds (most common)
duration1 = timedelta(hours=2, minutes=30, seconds=45)
duration_seconds = duration1.total_seconds()
event = events.new()
event.name = "Video Duration"
event.duration_seconds = duration_seconds
event.save(doc_id='duration_seconds')
print("1️⃣ Store as seconds:")
print(f" Original: {duration1}")
print(f" Stored: {duration_seconds} seconds")
# Read back and reconstruct
retrieved = events.doc('duration_seconds')
retrieved.fetch()
reconstructed = timedelta(seconds=retrieved.duration_seconds)
print(f" Retrieved: {reconstructed}")
# Pattern 2: Store as milliseconds (for precision)
duration2 = timedelta(seconds=45, microseconds=123456)
duration_ms = duration2.total_seconds() * 1000
event = events.new()
event.name = "API Response Time"
event.duration_milliseconds = duration_ms
event.save(doc_id='duration_ms')
print("\n2️⃣ Store as milliseconds:")
print(f" Original: {duration2}")
print(f" Stored: {duration_ms} ms")
retrieved = events.doc('duration_ms')
retrieved.fetch()
reconstructed = timedelta(milliseconds=retrieved.duration_milliseconds)
print(f" Retrieved: {reconstructed}")
# Pattern 3: Store as separate fields (for readability)
duration3 = timedelta(days=5, hours=3, minutes=20)
event = events.new()
event.name = "Project Duration"
event.duration_days = duration3.days
event.duration_seconds = duration3.seconds # Remaining seconds after days
event.save(doc_id='duration_fields')
print("\n3️⃣ Store as separate fields:")
print(f" Original: {duration3}")
print(f" Stored: days={duration3.days}, seconds={duration3.seconds}")
retrieved = events.doc('duration_fields')
retrieved.fetch()
reconstructed = timedelta(days=retrieved.duration_days, seconds=retrieved.duration_seconds)
print(f" Retrieved: {reconstructed}")
print("\n⚠️ IMPORTANT:")
print(" - Firestore has NO native TimeDelta type")
print(" - Store as numbers (seconds, milliseconds, etc.)")
print(" - Convert back to TimeDelta when reading")
print(" - Choose storage format based on your use case")
⏱️ Duration Storage Patterns 1️⃣ Store as seconds: Original: 2:30:45 Stored: 9045.0 seconds Retrieved: 2:30:45 2️⃣ Store as milliseconds: Original: 0:00:45.123456 Stored: 45123.456 ms Retrieved: 0:00:45.123456 3️⃣ Store as separate fields: Original: 5 days, 3:20:00 Stored: days=5, seconds=12000 Retrieved: 5 days, 3:20:00 ⚠️ IMPORTANT: - Firestore has NO native TimeDelta type - Store as numbers (seconds, milliseconds, etc.) - Convert back to TimeDelta when reading - Choose storage format based on your use case
Feature 8: Common DateTime Patterns¶
Real-world datetime use cases.
print("📋 Common DateTime Patterns\n")
# Pattern 1: Created/Updated timestamps
print("1️⃣ Audit timestamps (created_at, updated_at):")
doc = events.new()
doc.name = "User Account"
doc.created_at = datetime.now(timezone.utc)
doc.updated_at = datetime.now(timezone.utc)
doc.save(doc_id='audit_example')
print(f" Created: {doc.created_at.isoformat()}")
print(f" Updated: {doc.updated_at.isoformat()}")
# Pattern 2: Scheduled events (future timestamps)
print("\n2️⃣ Scheduled event (future timestamp):")
scheduled_time = datetime.now(timezone.utc) + timedelta(hours=24)
doc = events.new()
doc.name = "Scheduled Email"
doc.scheduled_for = scheduled_time
doc.status = "pending"
doc.save(doc_id='scheduled_example')
print(f" Scheduled: {doc.scheduled_for.isoformat()}")
print(f" Hours from now: {24}")
# Pattern 3: Expiration timestamps
print("\n3️⃣ Expiration timestamp (TTL pattern):")
doc = events.new()
doc.name = "Session Token"
doc.created_at = datetime.now(timezone.utc)
doc.expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
doc.save(doc_id='expiration_example')
print(f" Created: {doc.created_at.isoformat()}")
print(f" Expires: {doc.expires_at.isoformat()}")
# Check if expired
now = datetime.now(timezone.utc)
is_expired = doc.expires_at < now
print(f" Is expired: {is_expired}")
# Pattern 4: Date-only (midnight UTC)
print("\n4️⃣ Date-only (no time component):")
doc = events.new()
doc.name = "Birthday"
# Store as midnight UTC for the date
doc.birth_date = datetime(1815, 12, 10, tzinfo=timezone.utc)
doc.save(doc_id='date_only_example')
print(f" Birth date: {doc.birth_date.strftime('%Y-%m-%d')}")
print(f" Stored as: {doc.birth_date.isoformat()}")
# Pattern 5: Unix timestamp (for compatibility)
print("\n5️⃣ Unix timestamp (epoch seconds):")
now_dt = datetime.now(timezone.utc)
unix_timestamp = now_dt.timestamp()
doc = events.new()
doc.name = "Unix Timestamp"
doc.unix_time = unix_timestamp
doc.save(doc_id='unix_example')
print(f" DateTime: {now_dt.isoformat()}")
print(f" Unix: {unix_timestamp}")
# Convert back
retrieved = events.doc('unix_example')
retrieved.fetch()
reconstructed = datetime.fromtimestamp(retrieved.unix_time, tz=timezone.utc)
print(f" Reconstructed: {reconstructed.isoformat()}")
📋 Common DateTime Patterns 1️⃣ Audit timestamps (created_at, updated_at): Created: 2025-10-12T16:12:40.159073+00:00 Updated: 2025-10-12T16:12:40.159163+00:00 2️⃣ Scheduled event (future timestamp): Scheduled: 2025-10-13T16:12:40.162461+00:00 Hours from now: 24 3️⃣ Expiration timestamp (TTL pattern): Created: 2025-10-12T16:12:40.164808+00:00 Expires: 2025-10-12T17:12:40.164832+00:00 Is expired: False 4️⃣ Date-only (no time component): Birth date: 1815-12-10 Stored as: 1815-12-10T00:00:00+00:00 5️⃣ Unix timestamp (epoch seconds): DateTime: 2025-10-12T16:12:40.169478+00:00 Unix: 1760285560.169478 Reconstructed: 2025-10-12T16:12:40.169478+00:00
Initialize Async Client¶
# Create async client and collection
async_client = async_demo_client()
async_db = AsyncFireProx(async_client)
async_events = async_db.collection('datetime_demo_events_async')
print("✅ Async client initialized")
✅ Async client initialized
Async DateTime Storage and Retrieval¶
# Store datetime asynchronously
now_utc = datetime.now(timezone.utc)
eastern = ZoneInfo('America/New_York')
now_eastern = now_utc.astimezone(eastern)
print("📅 Storing datetimes asynchronously:")
print(f" UTC: {now_utc.isoformat()}")
print(f" Eastern: {now_eastern.isoformat()}")
# Store both
doc1 = async_events.new()
doc1.name = "Async UTC Test"
doc1.timestamp = now_utc
await doc1.save(doc_id='async_utc')
doc2 = async_events.new()
doc2.name = "Async Eastern Test"
doc2.timestamp = now_eastern
await doc2.save(doc_id='async_eastern')
# Retrieve both
retrieved1 = async_events.doc('async_utc')
await retrieved1.fetch()
retrieved2 = async_events.doc('async_eastern')
await retrieved2.fetch()
print("\n📥 Retrieved (both in UTC):")
print(f" From UTC: {retrieved1.timestamp.isoformat()}")
print(f" From Eastern: {retrieved2.timestamp.isoformat()}")
print("\n✅ Async datetime handling is identical to sync!")
📅 Storing datetimes asynchronously: UTC: 2025-10-12T16:12:45.855954+00:00 Eastern: 2025-10-12T12:12:45.855954-04:00 📥 Retrieved (both in UTC): From UTC: 2025-10-12T16:12:45.855954+00:00 From Eastern: 2025-10-12T16:12:45.855954+00:00 ✅ Async datetime handling is identical to sync!
Async DateTime Queries¶
# Create events at different times
now = datetime.now(timezone.utc)
async_events_data = [
{'name': 'Past Event', 'time': now - timedelta(days=3)},
{'name': 'Recent Event', 'time': now - timedelta(hours=6)},
{'name': 'Current Event', 'time': now},
{'name': 'Future Event', 'time': now + timedelta(days=2)},
]
print("📝 Creating async events:")
for data in async_events_data:
event = async_events.new()
event.name = data['name']
event.event_time = data['time']
await event.save()
print(f" {data['name']}")
# Query for future events
future_query = (async_events
.where('event_time', '>', now)
.order_by('event_time'))
future_results = await future_query.get()
print(f"\n🔮 Future events: {len(future_results)}")
for event in future_results:
print(f" {event.name}")
# Query for past 24 hours
day_ago = now - timedelta(days=1)
recent_query = (async_events
.where('event_time', '>', day_ago)
.where('event_time', '<=', now)
.order_by('event_time'))
recent_results = await recent_query.get()
print(f"\n📅 Events in last 24 hours: {len(recent_results)}")
for event in recent_results:
print(f" {event.name}")
📝 Creating async events: Past Event Recent Event Current Event Future Event 🔮 Future events: 1 Future Event 📅 Events in last 24 hours: 2 Recent Event Current Event
Summary¶
✅ Key Findings¶
Timezone Behavior¶
- Storage: Firestore always stores timestamps in UTC
- Naive DateTime: Treated as UTC when stored (no timezone conversion)
- Aware DateTime: Converted to UTC, original timezone lost
- Retrieval: Always returns timezone-aware datetime in UTC
- Moment Preservation: The actual moment in time is preserved correctly
Best Practices¶
✅ DO: Always use timezone-aware datetimes
from datetime import datetime, timezone
dt = datetime.now(timezone.utc)
✅ DO: Store everything in UTC
doc.created_at = datetime.now(timezone.utc)
✅ DO: Convert to local timezone in your application
local_tz = ZoneInfo('America/New_York')
local_time = retrieved.created_at.astimezone(local_tz)
❌ DON'T: Store timezone-naive datetimes
# Bad - ambiguous timezone
dt = datetime.now() # No tzinfo!
❌ DON'T: Expect to preserve original timezone
# Original timezone info will be lost
eastern_dt = datetime.now(ZoneInfo('America/New_York'))
# After Firestore round-trip, you get UTC
Duration Storage¶
⚠️ TimeDelta Not Supported: Firestore has no native TimeDelta type
Alternatives:
- Store as seconds:
duration.total_seconds() - Store as milliseconds:
duration.total_seconds() * 1000 - Store as separate fields:
{days: int, seconds: int}
🎯 Recommended Patterns¶
1. Audit Timestamps¶
doc.created_at = datetime.now(timezone.utc)
doc.updated_at = datetime.now(timezone.utc)
2. Date Ranges¶
start = datetime(2024, 1, 1, tzinfo=timezone.utc)
end = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
query = collection.where('date', '>=', start).where('date', '<=', end)
3. Expiration Times¶
doc.expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
is_expired = datetime.now(timezone.utc) > doc.expires_at
4. User-Facing Times¶
# Store in UTC
doc.event_time = datetime.now(timezone.utc)
# Display in user's timezone
user_tz = ZoneInfo(user.timezone) # e.g., 'America/Los_Angeles'
local_time = doc.event_time.astimezone(user_tz)
📚 Learn More¶
- Python datetime docs: https://docs.python.org/3/library/datetime.html
- Firestore timestamp docs: https://firebase.google.com/docs/firestore/manage-data/data-types#date_and_time
- zoneinfo (Python 3.9+): https://docs.python.org/3/library/zoneinfo.html