Working with Dates and Time in Python

This guide will walk you through everything you need to survive and thrive with dates and times. We’ll cover the main classes, formatting, timezones (with modern recommendations), practical examples, and the traps that bite most people. Let’s dive in — code examples are ready to copy-paste.

date, time, datetime, and timedelta Explained

The datetime module provides four key classes:

  • date: Just year, month, day. No time or timezone.
  • time: Hour, minute, second, microsecond. Optional timezone info.
  • datetime: Combines date + time. Most commonly used.
  • timedelta: Represents a duration (days, seconds, microseconds). Great for arithmetic.

All can be naive (no timezone info) or aware (with timezone).

Let’s see them in action. Create a file dates.py:

Python

from datetime import date, time, datetime, timedelta

# date: year-month-day
today = date.today()
print(today)                # e.g., 2026-01-14
print(today.year, today.month, today.day)  # 2026 1 14

# time: hour:minute:second.microsecond
meeting_time = time(14, 30)  # 2:30 PM
print(meeting_time)          # 14:30:00

# datetime: full timestamp (naive by default)
now = datetime.now()
print(now)                   # e.g., 2026-01-14 20:05:23.456789

# timedelta: durations
one_week = timedelta(days=7)
one_hour = timedelta(hours=1)
print(now + one_week)        # one week from now
print(now - one_hour)        # one hour ago

# Also useful: date from parts
birthday = date(1995, 7, 20)
age_days = (today - birthday).days
print(f"Age in days: {age_days}")

Key takeaway: Use datetime for most real-world work because you usually need both date and time. date is handy for birthdays or calendar days; time for schedules without dates.

Arithmetic is straightforward with timedelta:

Python

deadline = datetime(2026, 3, 1, 17, 0)  # March 1, 5 PM
now = datetime.now()
time_left = deadline - now
print(time_left)                       # e.g., 45 days, 18:54:36.123456

timedelta normalizes automatically (e.g., 25 hours → 1 day + 1 hour).

Formatting and Parsing Timestamps (strftime / strptime)

Humans read strings; computers love datetime objects. Convert between them with:

  • strftime(): datetime → string (“format to string”)
  • strptime(): string → datetime (“parse from string”)

Common format codes:

  • %Y: 4-digit year
  • %m: 2-digit month
  • %d: 2-digit day
  • %H: 24-hour
  • %M: minute
  • %S: second
  • %b: abbreviated month (Jan)
  • %A: full weekday

Example:

Python

from datetime import datetime

now = datetime.now()

# Format
iso_format = now.strftime("%Y-%m-%d %H:%M:%S")
print(iso_format)                    # 2026-01-14 20:05:23

friendly = now.strftime("%A, %B %d, %Y at %I:%M %p")
print(friendly)                      # Wednesday, January 14, 2026 at 08:05 PM

# Parse
log_line = "2026-01-14T15:30:00"
dt = datetime.strptime(log_line, "%Y-%m-%dT%H:%M:%S")
print(dt)                            # 2026-01-14 15:30:00

# With timezone offset (we'll cover timezones soon)
utc_str = "2026-01-14 13:00:00+00:00"
# Note: strptime supports %z for UTC offset
dt_utc = datetime.strptime(utc_str, "%Y-%m-%d %H:%M:%S%z")
print(dt_utc)                        # 2026-01-14 13:00:00+00:00 (aware!)

Pro tip: Use ISO 8601 (%Y-%m-%dT%H:%M:%S%z) for APIs/logs — it’s unambiguous and sorts correctly.

Timezones: What Beginners Need to Know

Timezones are the #1 source of datetime pain. Two concepts:

  • Naive datetime: No tzinfo (tzinfo=None). Assumes “local time” but ambiguous.
  • Aware datetime: Has tzinfo. Represents absolute moment in time.

Best practice in 2026: Use aware datetimes with UTC internally. Convert to local only for display.

Python 3.9+ includes zoneinfo (built-in, uses IANA database). Prefer it over pytz (older, but still works; zoneinfo is faster and standard).

Install tzdata on Windows if needed: pip install tzdata

Examples:

Python

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# UTC now (aware)
utc_now = datetime.now(timezone.utc)
print(utc_now)                       # 2026-01-14 13:05:23+00:00

# Alternative (Python 3.11+)
utc_now = datetime.now(ZoneInfo("UTC"))

# Hanoi time (Asia/Ho_Chi_Minh = +07:00)
hanoi_tz = ZoneInfo("Asia/Ho_Chi_Minh")
hanoi_now = datetime.now(hanoi_tz)
print(hanoi_now)                     # 2026-01-14 20:05:23+07:00

# Convert between timezones
utc_dt = datetime(2026, 1, 14, 13, 0, tzinfo=timezone.utc)
hanoi_dt = utc_dt.astimezone(hanoi_tz)
print(hanoi_dt)                      # 2026-01-14 20:00:00+07:00

# Make naive → aware (careful!)
naive = datetime(2026, 1, 14, 20, 0)  # Assume this is Hanoi time
aware = naive.replace(tzinfo=hanoi_tz)  # Attaches label, no conversion
print(aware)                         # 2026-01-14 20:00:00+07:00

# Better: assume local → convert properly
from zoneinfo import ZoneInfo
local_now = datetime.now()           # naive, system local
aware_local = local_now.replace(tzinfo=ZoneInfo("Asia/Ho_Chi_Minh"))
utc_from_local = aware_local.astimezone(timezone.utc)

Rule: Never mix naive and aware in comparisons/arithmetic — Python raises TypeError.

Store everything in UTC. Display in user’s timezone.

Real-world Examples (Deadlines, Logs, Scheduling)

  1. Deadline check (e.g., subscription expires):

Python

from datetime import datetime, timedelta, timezone

def is_expired(sub_end_str: str) -> bool:
    # Parse ISO with offset
    sub_end = datetime.fromisoformat(sub_end_str)
    now_utc = datetime.now(timezone.utc)
    return now_utc > sub_end

# Usage
print(is_expired("2026-02-01T00:00:00+00:00"))  # False (assuming current date)
  1. Logging with timestamps:

Python

import logging
from datetime import datetime, timezone

logging.basicConfig(
    format='%(asctime)s %(levelname)s %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S %Z',
    level=logging.INFO
)

# Force UTC in logs
logging.Formatter.converter = lambda *args: datetime.utcnow().timetuple()

logging.info("Processing started")
  1. Scheduling (e.g., next run in 30 min):

Python

from datetime import datetime, timedelta, timezone

def next_run():
    now = datetime.now(timezone.utc)
    next_time = now + timedelta(minutes=30)
    return next_time.replace(second=0, microsecond=0)

print(next_run())

Common Pitfalls and How to Avoid Them

  1. Mixing naive and aware → TypeError: can’t compare offset-naive and offset-aware datetimes
    • Fix: Always make aware. Use timezone.utc or ZoneInfo.
  2. Using datetime.utcnow() (deprecated in recent Python) — returns naive UTC.
    • Fix: datetime.now(timezone.utc) or datetime.now(ZoneInfo(“UTC”))
  3. Wrong timezone attachment — .replace(tzinfo=…) doesn’t convert.
    • Fix: Use .astimezone() for conversion; .replace() only for known-local naive times.
  4. DST surprises — Adding 1 day across DST change may shift wall time.
    • Fix: Work in UTC internally. Use zoneinfo — handles DST better than old pytz tricks.
  5. Parsing without timezone — Assume wrong offset.
    • Fix: Prefer ISO with %z. Use libraries like dateutil.parser for fuzzy parsing if needed.
  6. Storing local time in DB — Breaks on DST or server moves.
    • Fix: Store UTC, convert on display.

Bonus: For complex needs (recurring events, human-friendly durations), consider pendulum or arrow libraries later — but master datetime first.

You’ve now got the survival kit! Dates and times get easier with practice. Try rewriting an old script with aware UTC — you’ll sleep better.

Leave a Reply

Your email address will not be published. Required fields are marked *