If you've ever tried to add a week to today's date, parse a log timestamp, or figure out why two datetimes that look identical won't compare equal โ welcome to Python's datetime module. It's powerful, it's in the standard library, and it's more confusing than it needs to be. This guide cuts straight to the 20% of features you'll actually use 80% of the time.
๐ Free: AI Publishing Checklist โ 7 steps in Python ยท Full pipeline: germy5.gumroad.com/l/xhxkzz (pay what you want, min $9.99)
The Three Types: date, time, and datetime
The datetime module exports three main classes, and picking the right one matters.
from datetime import date, time, datetime
# date: year, month, day only โ no time component
d = date(2025, 5, 6)
print(d) # 2025-05-06
# time: hours, minutes, seconds โ no date component
t = time(14, 30, 0)
print(t) # 14:30:00
# datetime: both date AND time โ what you'll use most
dt = datetime(2025, 5, 6, 14, 30, 0)
print(dt) # 2025-05-06 14:30:00
When to use which:
-
dateโ scheduling events, birth dates, expiration dates, anything where the time of day is irrelevant -
timeโ storing a recurring time (e.g., "run at 09:00 every day") independent of the date -
datetimeโ log timestamps, scheduling with time precision, anything that combines date and time
In practice, you'll use datetime for 90% of automation work.
Creating Datetimes: now(), today(), and date.today()
from datetime import datetime, date
# Get the current date and time (local time, no timezone info)
now = datetime.now()
print(now) # 2025-05-06 14:32:17.483921
# Get just today's date
today = date.today()
print(today) # 2025-05-06
# datetime.today() works too, but datetime.now() is preferred
# (now() accepts a timezone argument; today() does not)
The difference between datetime.now() and datetime.today() is subtle and rarely matters for naive datetimes. Prefer datetime.now() because it can accept a tz argument when you need timezone awareness later.
strftime() โ Format a Datetime to String
strftime stands for "string format time." You pass a format string with codes, and it returns a formatted string.
from datetime import datetime
now = datetime(2025, 5, 6, 14, 30, 5)
# ISO 8601 โ machine-readable, great for logs and APIs
print(now.strftime("%Y-%m-%dT%H:%M:%S")) # 2025-05-06T14:30:05
# Human-readable
print(now.strftime("%B %d, %Y at %I:%M %p")) # May 06, 2025 at 02:30 PM
# Compact date for filenames
print(now.strftime("%Y%m%d")) # 20250506
Common format codes:
| Code | Meaning | Example |
|------|---------|---------|
| %Y | 4-digit year | 2025 |
| %m | Month (01โ12) | 05 |
| %d | Day (01โ31) | 06 |
| %H | Hour 24h (00โ23) | 14 |
| %M | Minutes (00โ59) | 30 |
| %S | Seconds (00โ59) | 05 |
| %B | Full month name | May |
| %I | Hour 12h (01โ12) | 02 |
| %p | AM or PM | PM |
Tip: Use .isoformat() as a shortcut for ISO 8601 without memorizing the format string:
print(now.isoformat()) # 2025-05-06T14:30:05
strptime() โ Parse a String into datetime
strptime is the reverse: string in, datetime out. The p stands for "parse."
from datetime import datetime
# Parse an ISO timestamp from a log file
raw = "2025-05-06T14:30:05"
dt = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S")
print(type(dt)) # <class 'datetime.datetime'>
print(dt.year) # 2025
# Parse a human-readable date
raw2 = "May 06, 2025"
dt2 = datetime.strptime(raw2, "%B %d, %Y")
print(dt2) # 2025-05-06 00:00:00
Common mistake: The format string must exactly match the input string โ including separators. "2025/05/06" with the format "%Y-%m-%d" will raise a ValueError. Double-check your separators.
timedelta โ Date Arithmetic
timedelta represents a duration. Add it to or subtract it from a datetime to get a new datetime.
from datetime import datetime, timedelta
now = datetime(2025, 5, 6, 14, 30, 0)
# Add 7 days
next_week = now + timedelta(days=7)
print(next_week) # 2025-05-13 14:30:00
# Subtract 2 hours
two_hours_ago = now - timedelta(hours=2)
print(two_hours_ago) # 2025-05-06 12:30:00
# Find the difference between two datetimes
start = datetime(2025, 5, 1, 9, 0, 0)
end = datetime(2025, 5, 6, 14, 30, 0)
delta = end - start
print(delta.days) # 5
print(delta.total_seconds()) # 484200.0
timedelta accepts: weeks, days, hours, minutes, seconds, milliseconds, microseconds.
Comparing Datetimes โ Sorting and Filtering
Datetimes support all comparison operators, which makes sorting and filtering straightforward.
from datetime import datetime
dt1 = datetime(2025, 5, 1)
dt2 = datetime(2025, 5, 6)
print(dt1 < dt2) # True
print(dt1 == dt2) # False
# Filter a list to events in May 2025
events = [
datetime(2025, 4, 30),
datetime(2025, 5, 3),
datetime(2025, 5, 15),
datetime(2025, 6, 1),
]
may_start = datetime(2025, 5, 1)
may_end = datetime(2025, 5, 31, 23, 59, 59)
may_events = [e for e in events if may_start <= e <= may_end]
print(may_events)
# [datetime.datetime(2025, 5, 3, 0, 0), datetime.datetime(2025, 5, 15, 0, 0)]
# Sort a list of datetimes
sorted_events = sorted(events)
Timezone-Aware Datetimes with zoneinfo (Python 3.9+)
This is where most beginners hit a wall. By default, datetime.now() returns a naive datetime โ no timezone information attached. The moment your code runs on a server in a different timezone, or you compare timestamps from two sources, naive datetimes become a liability.
from datetime import datetime
from zoneinfo import ZoneInfo # Python 3.9+
# Naive datetime (no timezone โ avoid for production logs)
naive = datetime.now()
print(naive.tzinfo) # None
# Timezone-aware datetime
aware = datetime.now(tz=ZoneInfo("America/New_York"))
print(aware) # 2025-05-06 14:30:00-04:00
print(aware.tzinfo) # America/New_York
# Convert between timezones
utc_dt = datetime.now(tz=ZoneInfo("UTC"))
eastern = utc_dt.astimezone(ZoneInfo("America/New_York"))
print(eastern)
The rule: Always use UTC for storage and logging. Convert to local time only for display. Never mix naive and aware datetimes โ Python will raise a TypeError if you try to compare them.
# This raises TypeError โ don't do this
naive = datetime(2025, 5, 6, 14, 30)
aware = datetime(2025, 5, 6, 14, 30, tzinfo=ZoneInfo("UTC"))
print(naive < aware) # TypeError: can't compare offset-naive and offset-aware datetimes
Unix Timestamps: timestamp() and fromtimestamp()
A Unix timestamp is a float representing seconds since January 1, 1970 UTC. It's common in APIs, databases, and system logs.
from datetime import datetime
from zoneinfo import ZoneInfo
# datetime โ Unix timestamp
dt = datetime(2025, 5, 6, 14, 30, 0, tzinfo=ZoneInfo("UTC"))
ts = dt.timestamp()
print(ts) # 1746541800.0
# Unix timestamp โ datetime (local timezone by default)
dt_back = datetime.fromtimestamp(ts)
print(dt_back) # local time equivalent
# Better: specify UTC explicitly
dt_utc = datetime.fromtimestamp(ts, tz=ZoneInfo("UTC"))
print(dt_utc) # 2025-05-06 14:30:00+00:00
Warning: datetime.fromtimestamp() without a tz argument uses your system's local timezone. Always pass tz=ZoneInfo("UTC") to get consistent results across environments.
Real Pipeline Pattern: Logging, Scheduling, and Time-Since-Last-Run
Here's how these pieces fit together in a real automation pipeline โ logging timestamps, scheduling daily jobs, and calculating time since last run.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import json
from pathlib import Path
STATE_FILE = Path("pipeline_state.json")
UTC = ZoneInfo("UTC")
def get_now() -> datetime:
"""Always return timezone-aware UTC datetime."""
return datetime.now(tz=UTC)
def log_run(step: str) -> None:
"""Write a timestamped log entry."""
ts = get_now().isoformat()
print(f"[{ts}] {step}")
def save_last_run() -> None:
"""Persist the last run time to disk."""
STATE_FILE.write_text(json.dumps({"last_run": get_now().isoformat()}))
def should_run_today() -> bool:
"""Return True if the pipeline hasn't run in the last 23 hours."""
if not STATE_FILE.exists():
return True
state = json.loads(STATE_FILE.read_text())
last_run = datetime.fromisoformat(state["last_run"])
elapsed = get_now() - last_run
return elapsed > timedelta(hours=23)
# Usage
if should_run_today():
log_run("Starting pipeline")
# ... do work ...
save_last_run()
log_run("Pipeline complete")
else:
log_run("Skipping โ already ran today")
Key patterns here:
-
datetime.fromisoformat()parses the ISO strings you saved with.isoformat()โ clean round-trip - Storing
last_runas an ISO string in JSON means it's human-readable and portable - All datetimes are UTC-aware so the comparison
get_now() - last_runis always valid
If you're building automation pipelines in Python and want a complete publishing workflow โ from generating content to scheduling posts and tracking runs โ my pipeline guide covers the full stack: germy5.gumroad.com/l/xhxkzz (pay what you want, min $9.99).
Further Reading
- How to Schedule Python Scripts with Cron: A Beginner's Complete Guide
- Python logging: Stop Using print() in Your Automation Scripts
- Python dataclasses: Cleaner Code Than Dicts or NamedTuples
If this was useful, the โค๏ธ button helps other developers find it.
Building a Python content pipeline? I sell the complete automation system as a one-time download โ Dev.to API, Claude API, launchd, Gumroad. Check it out ($9.99)
Top comments (0)