DEV Community

Cover image for Python datetime: Dates, Times, and Timezones Without the Confusion
German Yamil
German Yamil

Posted on

Python datetime: Dates, Times, and Timezones Without the Confusion

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Key patterns here:

  • datetime.fromisoformat() parses the ISO strings you saved with .isoformat() โ€” clean round-trip
  • Storing last_run as an ISO string in JSON means it's human-readable and portable
  • All datetimes are UTC-aware so the comparison get_now() - last_run is 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


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)