Working with Timestamps in Python: Complete Guide
I'll be honest: Python's built-in datetime
module is... quirky. It works, sure, but it's got some sharp edges that'll bite you if you're not careful. The good news? The Python ecosystem has produced some genuinely excellent libraries that make datetime handling a breeze—once you know which ones to reach for.
Whether you're building a simple script or a production API that needs to handle timezones correctly across the globe, this guide's got you covered. Let's dive in.
The datetime Standard Library
Let's start with what comes in the box. Python's standard library has everything you need for basic datetime work—it's just not always intuitive.
datetime Module Overview
Python's datetime
module gives you four main types to work with:
from datetime import datetime, date, time, timedelta
# date: Year, month, day
today = date(2024, 1, 15)
# time: Hour, minute, second, microsecond
now_time = time(19, 0, 0)
# datetime: Combination of date and time
now = datetime(2024, 1, 15, 19, 0, 0)
# timedelta: Duration between two dates/times
duration = timedelta(days=1, hours=2, minutes=30)
Creating datetime Objects
from datetime import datetime
import time
# Current time (naive - no timezone)
now = datetime.now()
print(now) # 2024-01-15 19:00:00.123456
# Current time (UTC aware)
utc_now = datetime.utcnow()
print(utc_now) # 2024-01-15 19:00:00.123456
# From components
dt = datetime(2024, 1, 15, 19, 0, 0)
# From Unix timestamp (seconds)
timestamp = 1705341600
dt_from_ts = datetime.fromtimestamp(timestamp)
print(dt_from_ts) # 2024-01-15 14:00:00 (local time)
# From Unix timestamp (UTC)
dt_from_ts_utc = datetime.utcfromtimestamp(timestamp)
print(dt_from_ts_utc) # 2024-01-15 19:00:00
# From ISO 8601 string
dt_from_iso = datetime.fromisoformat('2024-01-15T19:00:00')
# Current time with high precision
precise_now = time.time() # Returns float with microseconds
print(precise_now) # 1705341600.123456
Converting to Timestamps
from datetime import datetime
dt = datetime(2024, 1, 15, 19, 0, 0)
# To Unix timestamp (seconds, as float)
timestamp = dt.timestamp()
print(timestamp) # 1705341600.0
# To ISO 8601 string
iso_string = dt.isoformat()
print(iso_string) # '2024-01-15T19:00:00'
# To formatted string
formatted = dt.strftime('%Y-%m-%d %H:%M:%S')
print(formatted) # '2024-01-15 19:00:00'
# To Unix timestamp (integer seconds)
timestamp_int = int(dt.timestamp())
Parsing Strings
from datetime import datetime
# Parse with strptime (string parse time)
date_str = '2024-01-15 19:00:00'
dt = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
# Parse ISO format
iso_str = '2024-01-15T19:00:00'
dt = datetime.fromisoformat(iso_str)
# Common format codes
formats = {
'%Y': 'Year (4 digits)', # 2024
'%m': 'Month (01-12)', # 01
'%d': 'Day (01-31)', # 15
'%H': 'Hour 24h (00-23)', # 19
'%I': 'Hour 12h (01-12)', # 07
'%M': 'Minute (00-59)', # 00
'%S': 'Second (00-59)', # 00
'%f': 'Microsecond', # 123456
'%p': 'AM/PM', # PM
'%z': 'UTC offset', # +0000
'%Z': 'Timezone name', # UTC
}
# Example with complex format
date_str = '15-Jan-2024 07:00:00 PM'
dt = datetime.strptime(date_str, '%d-%b-%Y %I:%M:%S %p')
Datetime Arithmetic
from datetime import datetime, timedelta
now = datetime(2024, 1, 15, 19, 0, 0)
# Add duration
tomorrow = now + timedelta(days=1)
next_week = now + timedelta(weeks=1)
in_2_hours = now + timedelta(hours=2)
# Subtract duration
yesterday = now - timedelta(days=1)
# Difference between datetimes
dt1 = datetime(2024, 1, 15, 19, 0, 0)
dt2 = datetime(2024, 1, 20, 21, 30, 0)
diff = dt2 - dt1
print(diff) # 5 days, 2:30:00
print(diff.days) # 5
print(diff.seconds) # 9000 (2.5 hours in seconds)
print(diff.total_seconds()) # 441000.0
# Check if date is in the past/future
is_past = now < datetime.now()
is_future = now > datetime.now()
Timezone-Aware Programming
The Problem with Naive Datetimes
from datetime import datetime
# ❌ NAIVE datetime (no timezone info)
naive = datetime.now()
print(naive.tzinfo) # None
print(naive.isoformat()) # '2024-01-15T19:00:00'
# Problem: Ambiguous - which timezone?
# Can't compare with other timezones reliably
Using pytz for Timezone Support
from datetime import datetime
import pytz
# Create timezone-aware datetime
utc = pytz.UTC
eastern = pytz.timezone('America/New_York')
tokyo = pytz.timezone('Asia/Tokyo')
# Current time in UTC
utc_now = datetime.now(utc)
print(utc_now) # 2024-01-15 19:00:00+00:00
# Current time in specific timezone
ny_now = datetime.now(eastern)
print(ny_now) # 2024-01-15 14:00:00-05:00
# Create aware datetime from naive
naive = datetime(2024, 1, 15, 19, 0, 0)
aware_utc = utc.localize(naive)
print(aware_utc) # 2024-01-15 19:00:00+00:00
# ⚠️ IMPORTANT: Use localize(), not replace()
# ❌ WRONG
wrong = naive.replace(tzinfo=eastern) # Doesn't handle DST!
# ✅ CORRECT
correct = eastern.localize(naive) # Handles DST properly
Converting Between Timezones
import pytz
from datetime import datetime
# Create datetime in UTC
utc = pytz.UTC
utc_time = datetime(2024, 1, 15, 19, 0, 0, tzinfo=utc)
# Convert to different timezones
eastern = pytz.timezone('America/New_York')
london = pytz.timezone('Europe/London')
tokyo = pytz.timezone('Asia/Tokyo')
ny_time = utc_time.astimezone(eastern)
london_time = utc_time.astimezone(london)
tokyo_time = utc_time.astimezone(tokyo)
print(f"UTC: {utc_time}") # 2024-01-15 19:00:00+00:00
print(f"NY: {ny_time}") # 2024-01-15 14:00:00-05:00
print(f"London: {london_time}") # 2024-01-15 19:00:00+00:00
print(f"Tokyo: {tokyo_time}") # 2024-01-16 04:00:00+09:00
List Available Timezones
import pytz
# All available timezones
all_timezones = pytz.all_timezones
print(f"Total timezones: {len(all_timezones)}")
# Common timezones
common = pytz.common_timezones
print(common[:10])
# Find timezone by name
for tz in pytz.all_timezones:
if 'New_York' in tz:
print(tz) # America/New_York
pendulum: Better Datetime Library
Okay, real talk: if you're doing anything serious with datetimes in Python, just use pendulum. It's what the standard library should have been.
Why pendulum?
Pendulum improves upon the standard library with:
- Automatic timezone handling (no more naive datetimes!)
- Better DST support (it just works)
- Simpler API (way more intuitive)
- Human-readable differences ("2 hours ago" instead of "timedelta(hours=2)")
pip install pendulum
Basic Usage
import pendulum
# Current time (timezone-aware by default!)
now = pendulum.now()
print(now) # 2024-01-15T19:00:00+00:00
# Current time in specific timezone
ny_now = pendulum.now('America/New_York')
print(ny_now) # 2024-01-15T14:00:00-05:00
# Create datetime
dt = pendulum.datetime(2024, 1, 15, 19, 0, 0, tz='UTC')
# From Unix timestamp
dt = pendulum.from_timestamp(1705341600)
# From string
dt = pendulum.parse('2024-01-15T19:00:00Z')
Timezone Conversion
import pendulum
# Create in one timezone, convert to another
ny = pendulum.datetime(2024, 1, 15, 14, 0, 0, tz='America/New_York')
tokyo = ny.in_timezone('Asia/Tokyo')
print(ny) # 2024-01-15T14:00:00-05:00
print(tokyo) # 2024-01-16T04:00:00+09:00
# Convert to UTC
utc = ny.in_utc()
print(utc) # 2024-01-15T19:00:00+00:00
Datetime Arithmetic
import pendulum
now = pendulum.now()
# Add/subtract with natural language
tomorrow = now.add(days=1)
next_week = now.add(weeks=1)
in_2_hours = now.add(hours=2)
yesterday = now.subtract(days=1)
# Chaining
future = now.add(days=5).add(hours=3).add(minutes=30)
# Period (human-readable difference)
start = pendulum.datetime(2024, 1, 15, tz='UTC')
end = pendulum.datetime(2024, 1, 20, tz='UTC')
period = end - start
print(period.in_days()) # 5
print(period.in_hours()) # 120
print(period.in_seconds()) # 432000
# Human-readable diff
print(start.diff_for_humans(end)) # "5 days before"
Handling DST with pendulum
import pendulum
# Spring forward (2024-03-10 in New York)
# 2:00 AM doesn't exist!
try:
# pendulum handles this gracefully
dt = pendulum.datetime(2024, 3, 10, 2, 30, tz='America/New_York')
print(dt) # Automatically adjusted to 3:30 AM EDT
except pendulum.exceptions.NonExistingTime as e:
print(f"Time doesn't exist: {e}")
# Fall back (2024-11-03 in New York)
# 1:30 AM occurs twice
dt = pendulum.datetime(2024, 11, 3, 1, 30, tz='America/New_York')
print(dt) # Uses first occurrence (EDT)
arrow: Another Alternative
arrow Features
pip install arrow
import arrow
# Current time
now = arrow.now()
utc_now = arrow.utcnow()
# Create arrow object
dt = arrow.get('2024-01-15T19:00:00Z')
dt = arrow.get(1705341600) # From timestamp
# From datetime
from datetime import datetime
dt = arrow.get(datetime(2024, 1, 15, 19, 0, 0))
# Timezone conversion
ny = arrow.now('America/New_York')
tokyo = ny.to('Asia/Tokyo')
# Humanize differences
past = arrow.now().shift(hours=-2)
print(past.humanize()) # "2 hours ago"
# Formatting
now = arrow.now()
print(now.format('YYYY-MM-DD HH:mm:ss'))
print(now.format('dddd, MMMM D, YYYY')) # "Monday, January 15, 2024"
python-dateutil
Parsing Flexible Date Strings
from dateutil import parser
# Parse various formats automatically
dates = [
"2024-01-15",
"January 15, 2024",
"15-Jan-2024",
"01/15/2024",
"2024-01-15T19:00:00Z",
]
for date_str in dates:
dt = parser.parse(date_str)
print(f"{date_str:30} → {dt}")
# Parse with timezone
dt = parser.parse("2024-01-15T19:00:00+00:00")
print(dt.tzinfo) # tzutc()
# Fuzzy parsing (extract date from text)
text = "The meeting is on January 15, 2024 at 2pm"
dt = parser.parse(text, fuzzy=True)
print(dt) # 2024-01-15 14:00:00
Relative Deltas
from dateutil.relativedelta import relativedelta
from datetime import datetime
now = datetime(2024, 1, 15)
# Add months (handles different month lengths)
next_month = now + relativedelta(months=1) # 2024-02-15
last_year = now + relativedelta(years=-1) # 2023-01-15
# Add specific units
future = now + relativedelta(
years=1,
months=2,
days=3,
hours=4,
minutes=5
)
# Go to specific day of month
first_of_month = now + relativedelta(day=1)
last_of_month = now + relativedelta(day=31) # Handles month length
# Go to specific weekday
from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
next_monday = now + relativedelta(weekday=MO)
next_friday = now + relativedelta(weekday=FR(1)) # Next friday
second_tuesday = now + relativedelta(day=1, weekday=TU(2)) # 2nd Tuesday of month
Best Practices
These are the patterns I wish someone had drilled into my head when I first started working with Python datetimes. Would've saved me so many production bugs.
1. Always Use UTC for Storage
import pendulum
# ✅ CORRECT: Store in UTC
def save_event(title, scheduled_time):
"""
Save event with UTC timestamp
Args:
scheduled_time: pendulum datetime or ISO string
"""
if isinstance(scheduled_time, str):
scheduled_time = pendulum.parse(scheduled_time)
# Convert to UTC for storage
utc_time = scheduled_time.in_utc()
db.save({
'title': title,
'scheduled_at': utc_time.timestamp(), # Unix timestamp
'scheduled_at_iso': utc_time.to_iso8601_string()
})
# Display in user's timezone
def display_event(event, user_timezone='America/New_York'):
utc_time = pendulum.from_timestamp(event['scheduled_at'])
local_time = utc_time.in_timezone(user_timezone)
return {
'title': event['title'],
'local_time': local_time.format('YYYY-MM-DD HH:mm:ss ZZ'),
'relative': local_time.diff_for_humans()
}
2. Validate Datetime Inputs
import pendulum
from pendulum.exceptions import ParserError
def parse_datetime_safe(date_input, timezone='UTC'):
"""
Safely parse datetime input with validation
Args:
date_input: Unix timestamp (int/float), ISO string, or pendulum datetime
timezone: Target timezone
Returns:
pendulum datetime or raises ValueError
"""
try:
if isinstance(date_input, (int, float)):
# Unix timestamp
if date_input < 0:
raise ValueError("Timestamp cannot be negative")
if date_input > 2**31: # Year 2038 check
# Assume milliseconds
date_input = date_input / 1000
dt = pendulum.from_timestamp(date_input, tz=timezone)
elif isinstance(date_input, str):
# ISO string or other format
dt = pendulum.parse(date_input, tz=timezone)
elif isinstance(date_input, pendulum.DateTime):
dt = date_input.in_timezone(timezone)
else:
raise ValueError(f"Unsupported datetime type: {type(date_input)}")
# Validate reasonable range (not too far past/future)
now = pendulum.now()
if dt < now.subtract(years=100):
raise ValueError("Date too far in the past")
if dt > now.add(years=100):
raise ValueError("Date too far in the future")
return dt
except ParserError as e:
raise ValueError(f"Invalid datetime format: {e}")
3. Use Context Managers for Testing
import pendulum
from freezegun import freeze_time
# Test time-sensitive code
@freeze_time("2024-01-15 19:00:00")
def test_expiry():
now = pendulum.now()
expires_at = now.add(hours=1)
assert not is_expired(expires_at)
# Move time forward
with freeze_time("2024-01-15 20:00:01"):
assert is_expired(expires_at)
4. Handle DST Explicitly
import pendulum
def schedule_recurring_event(base_time, timezone, occurrences=5):
"""
Schedule recurring event, handling DST correctly
Args:
base_time: Start time in specified timezone
timezone: IANA timezone name
occurrences: Number of occurrences
Returns:
List of pendulum datetimes
"""
events = []
current = pendulum.parse(base_time).in_timezone(timezone)
for i in range(occurrences):
events.append({
'occurrence': i + 1,
'utc': current.in_utc().to_iso8601_string(),
'local': current.to_iso8601_string(),
'offset': current.format('ZZ'),
'dst': current.is_dst()
})
# Add 1 week (handles DST automatically)
current = current.add(weeks=1)
return events
# Example spanning DST transition
events = schedule_recurring_event(
'2024-03-03 02:00:00', # Week before DST
'America/New_York',
occurrences=3
)
for event in events:
print(f"Week {event['occurrence']}: {event['local']} (DST: {event['dst']})")
# Output:
# Week 1: 2024-03-03T02:00:00-05:00 (DST: False)
# Week 2: 2024-03-10T03:00:00-04:00 (DST: True) ← Automatically adjusted!
# Week 3: 2024-03-17T02:00:00-04:00 (DST: True)
Working with Databases
Storing Timestamps
import pendulum
import psycopg2
# PostgreSQL with timezone support
def save_to_postgres(conn, event):
"""Save event with timezone-aware timestamp"""
utc_time = pendulum.parse(event['scheduled_at']).in_utc()
with conn.cursor() as cur:
cur.execute("""
INSERT INTO events (title, scheduled_at, timezone)
VALUES (%s, %s, %s)
""", (
event['title'],
utc_time.to_datetime_string(), # '2024-01-15 19:00:00'
event['timezone']
))
conn.commit()
# Retrieving with timezone conversion
def get_from_postgres(conn, event_id, user_timezone='America/New_York'):
"""Retrieve event and convert to user's timezone"""
with conn.cursor() as cur:
cur.execute("""
SELECT title, scheduled_at AT TIME ZONE 'UTC', timezone
FROM events
WHERE id = %s
""", (event_id,))
row = cur.fetchone()
if row:
title, scheduled_at_utc, original_tz = row
# Convert to user's timezone
utc = pendulum.instance(scheduled_at_utc, tz='UTC')
local = utc.in_timezone(user_timezone)
return {
'title': title,
'utc': utc.to_iso8601_string(),
'local': local.to_iso8601_string(),
'original_tz': original_tz
}
MongoDB
import pendulum
from pymongo import MongoClient
client = MongoClient()
db = client['myapp']
# Store as Unix timestamp
def save_to_mongo(event):
"""Save event with Unix timestamp"""
dt = pendulum.parse(event['scheduled_at']).in_utc()
db.events.insert_one({
'title': event['title'],
'scheduled_at': dt.timestamp(), # Unix timestamp
'timezone': event['timezone'],
'created_at': pendulum.now().timestamp()
})
# Query by date range
def query_events_in_range(start_date, end_date):
"""Query events between two dates"""
start_ts = pendulum.parse(start_date).in_utc().timestamp()
end_ts = pendulum.parse(end_date).in_utc().timestamp()
events = db.events.find({
'scheduled_at': {
'$gte': start_ts,
'$lt': end_ts
}
}).sort('scheduled_at', 1)
return [
{
'title': e['title'],
'scheduled_at': pendulum.from_timestamp(e['scheduled_at'])
.in_timezone(e['timezone'])
.to_iso8601_string()
}
for e in events
]
Testing Time-Dependent Code
Using freezegun
from freezegun import freeze_time
import pendulum
@freeze_time("2024-01-15 19:00:00")
def test_session_expiry():
"""Test session expiry logic"""
# Create session that expires in 1 hour
session = create_session(expires_in=3600)
assert session.is_valid() is True
# Move time forward 59 minutes (still valid)
with freeze_time("2024-01-15 19:59:00"):
assert session.is_valid() is True
# Move time forward 61 minutes (expired)
with freeze_time("2024-01-15 20:01:00"):
assert session.is_valid() is False
def test_dst_transition():
"""Test DST handling"""
# Before DST (EST)
with freeze_time("2024-03-09 12:00:00"):
dt = pendulum.now('America/New_York')
assert dt.offset_hours == -5
assert dt.is_dst() is False
# After DST (EDT)
with freeze_time("2024-03-11 12:00:00"):
dt = pendulum.now('America/New_York')
assert dt.offset_hours == -4
assert dt.is_dst() is True
pytest Fixtures
import pytest
import pendulum
from freezegun import freeze_time
@pytest.fixture
def fixed_time():
"""Fixture providing consistent test time"""
with freeze_time("2024-01-15 19:00:00"):
yield pendulum.now()
@pytest.fixture
def mock_clock():
"""Fixture for advancing time in tests"""
class MockClock:
def __init__(self):
self.frozen_time = freeze_time("2024-01-15 19:00:00")
self.frozen_time.start()
def advance(self, **kwargs):
"""Advance time by specified duration"""
current = pendulum.now()
new_time = current.add(**kwargs)
self.frozen_time.stop()
self.frozen_time = freeze_time(new_time.to_datetime_string())
self.frozen_time.start()
def stop(self):
self.frozen_time.stop()
clock = MockClock()
yield clock
clock.stop()
def test_with_mock_clock(mock_clock):
"""Test using mock clock"""
start = pendulum.now()
mock_clock.advance(hours=2)
end = pendulum.now()
assert (end - start).in_hours() == 2
Common Pitfalls
Let me save you some debugging time. These are the mistakes everyone makes (including me, repeatedly).
1. Comparing Naive and Aware Datetimes
import pendulum
from datetime import datetime
# ❌ WRONG: Can't compare naive and aware
naive = datetime.now()
aware = pendulum.now()
try:
if naive < aware:
pass
except TypeError as e:
print(e) # can't compare offset-naive and offset-aware datetimes
# ✅ CORRECT: Make both aware
aware_naive = pendulum.instance(naive, tz='UTC')
if aware_naive < aware:
pass
2. Using datetime.now() Instead of UTC
from datetime import datetime
# ❌ WRONG: Server-dependent
now = datetime.now() # Depends on server timezone!
# ✅ CORRECT: Always use UTC
import pendulum
now_utc = pendulum.now('UTC')
3. Forgetting to Handle DST
from datetime import datetime, timedelta
# ❌ WRONG: Naive arithmetic breaks with DST
dt = datetime(2024, 3, 9, 12, 0, 0) # Before DST
tomorrow = dt + timedelta(days=1) # DST transition!
# Result: Time might be off by 1 hour
# ✅ CORRECT: Use pendulum
import pendulum
dt = pendulum.datetime(2024, 3, 9, 12, 0, 0, tz='America/New_York')
tomorrow = dt.add(days=1) # Handles DST automatically
Performance Tips
Caching Timezone Objects
import pytz
from functools import lru_cache
@lru_cache(maxsize=128)
def get_timezone(tz_name):
"""Cache timezone objects for better performance"""
return pytz.timezone(tz_name)
# Use cached version
tz = get_timezone('America/New_York')
Batch Conversions
import pendulum
def convert_batch(timestamps, target_tz='America/New_York'):
"""Convert multiple timestamps efficiently"""
tz = pendulum.timezone(target_tz) # Create timezone object once
return [
pendulum.from_timestamp(ts, tz=tz)
for ts in timestamps
]
# More efficient than converting timezone in each iteration
timestamps = [1705341600, 1705428000, 1705514400]
results = convert_batch(timestamps)
Conclusion
Python's datetime ecosystem is actually pretty great once you know what to reach for.
Start Here: If you're doing anything beyond basic date math, install pendulum. It's timezone-aware by default, handles DST gracefully, and has an API that makes sense. For flexible date parsing, add python-dateutil. For testing, use freezegun.
My Setup:
In production code, I use pendulum for everything. In tests, I freeze time with freezegun. I only reach for the standard library datetime
when I'm writing something that absolutely can't have dependencies.
Golden Rules: Always store in UTC. Always use timezone-aware datetimes. Validate inputs. Test DST transitions. Treat time like the complex, weird thing it is—because it is.
The best Python apps (Instagram, Spotify, Dropbox) didn't get datetime handling right by accident—they used the right tools and followed these patterns religiously. Now you can too.
Further Reading
- Complete Guide to Unix Timestamps - Foundation of time representation
- ISO 8601 Standard Explained - Date format standard
- Timezone Conversion Best Practices - Cross-language timezone patterns
- Testing Time-Dependent Code - Python testing strategies
- API Design: Timestamp Formats - Build Python APIs with timestamps
Building Python applications with datetime? Contact us for consultation.