Software Engineering

Working With Datetime Objects in Python: The Complete Guide

Master Python datetime: naive vs aware objects, parse with strptime(), format with strftime(), do arithmetic with timedelta, and handle timezones the modern way with zoneinfo (Python 3.9+). Correct, runnable examples throughout.

By Inventive HQ Team

To work with dates and times in Python, use the datetime module: datetime.strptime() parses a string into a datetime object, datetime.strftime() formats a datetime back into a string, and timedelta does date arithmetic. The module provides four core classes — date (calendar date), time (clock time), datetime (both together), and timedelta (a duration). The single most important distinction is naive vs aware: a naive datetime has no timezone, while an aware datetime carries a tzinfo and maps to an exact moment on the global timeline. The modern best practice is to store and compute in timezone-aware UTC using datetime.now(timezone.utc) — never the deprecated datetime.utcnow() — and to convert timezones with the standard-library zoneinfo module (ZoneInfo, Python 3.9+) rather than the older pytz. Convert to a local zone only at the display edge with .astimezone().

That paragraph is the summary. The rest of this guide is what a summary can't give you: a complete method reference table, a strftime directive cheat sheet, correct runnable code for every operation, and the timezone traps that quietly corrupt data in production.

The datetime cheat sheet: every core operation

TaskDo this (Python 3.9+)Notes
Current UTC time (aware)datetime.now(timezone.utc)Preferred. Aware, correct everywhere
Current local time (naive)datetime.now()No tzinfo — local wall clock only
Current UTC (naive)datetime.utcnow()Deprecated in 3.12. Naive despite being UTC — avoid
Parse a string → datetimedatetime.strptime(s, fmt)Format mask must match exactly
Parse ISO 8601datetime.fromisoformat(s)Z suffix needs 3.11+ (see below)
Format a datetime → stringdt.strftime(fmt)Uses the same directive codes
Emit ISO 8601 stringdt.isoformat()e.g. 2026-07-18T14:30:00+00:00
Date arithmeticdt + timedelta(days=7)Add/subtract durations
Difference between two datetimeslater - earliertimedelta.days, .total_seconds()
Convert timezonedt.astimezone(ZoneInfo("America/New_York"))Requires an aware source
Attach a timezone to a naive dtdt.replace(tzinfo=ZoneInfo("America/New_York"))Interprets the wall clock as that zone
datetime → Unix timestampdt.timestamp()Aware dt → true UTC epoch seconds
Unix timestamp → datetime (aware UTC)datetime.fromtimestamp(ts, timezone.utc)Pass tz to avoid a naive local result
Combine date + timedatetime.combine(d, t)Builds a datetime from parts

Understanding Date Format Masks

Format masks (also called format strings) are special codes that tell Python how to interpret different parts of a date string. These codes follow the strftime/strptime convention and are essential for accurate date parsing.

Essential Format Codes

CodeDescriptionExample
%Y4-digit year2023
%mMonth as number (01-12)07
%dDay of month (01-31)15
%HHour (24-hour format)14
%IHour (12-hour format)02
%MMinute (00-59)45
%SSecond (00-59)30
%pAM/PM indicatorPM

For example, the date string "7/11/2019" would use the mask "%m/%d/%Y" to indicate month/day/year format separated by forward slashes.

The datetime pipeline: string in, string out

Almost every real datetime job is the same round trip. A string comes in from a log, an API, or a form; you parse it into a datetime, do timezone-aware work in the middle, then format it back into a string for a human. The two ends are strptime and strftime; the middle is where astimezone lives.

The Python datetime round-trip pipeline A raw string is parsed by strptime into a datetime, converted to a target timezone with astimezone, then formatted back to a string by strftime. A token animates left to right along the pipeline. Parse in the middle, convert, then format out "2026-07-18 14:30 UTC" strptime parse astimezone aware UTC → local strftime format display str → datetime convert zone datetime → str

Converting Strings to Datetime Objects

The strptime() function (String Parse Time) is your primary tool for converting date strings into datetime objects. It takes two parameters: the date string and the corresponding format mask.

Basic String Conversion

from datetime import datetime

# Convert simple date string
date_string = "7/11/2019"
date_mask = "%m/%d/%Y"
datetime_object = datetime.strptime(date_string, date_mask)
print(datetime_object)  # Output: 2019-07-11 00:00:00

# Convert date with time
datetime_string = "07/11/2019 02:45PM"
datetime_mask = "%m/%d/%Y %I:%M%p"
datetime_object = datetime.strptime(datetime_string, datetime_mask)
print(datetime_object)  # Output: 2019-07-11 14:45:00

Handling Different Date Formats

# ISO format
iso_date = datetime.strptime("2019-07-11", "%Y-%m-%d")

# European format
eu_date = datetime.strptime("11/07/2019", "%d/%m/%Y")

# Full timestamp
full_timestamp = datetime.strptime("2019-07-11 14:45:30", "%Y-%m-%d %H:%M:%S")

# With milliseconds
ms_timestamp = datetime.strptime("2019-07-11 14:45:30.123", "%Y-%m-%d %H:%M:%S.%f")

Formatting Datetime Objects

The strftime() function (String Format Time) converts datetime objects back into formatted strings. This is essential for displaying dates in user-friendly formats or preparing data for output.

Common Formatting Examples

from datetime import datetime

# Get current datetime
now = datetime.now()

# Different formatting options
us_format = now.strftime("%m/%d/%Y")           # 07/11/2023
iso_format = now.strftime("%Y-%m-%d")          # 2023-07-11
full_date = now.strftime("%B %d, %Y")         # July 11, 2023
time_only = now.strftime("%I:%M %p")          # 02:45 PM
timestamp = now.strftime("%Y-%m-%d %H:%M:%S") # 2023-07-11 14:45:30

# Extract specific components
year_only = now.strftime("%Y")                # 2023
month_name = now.strftime("%B")               # July
day_name = now.strftime("%A")                 # Tuesday

Comparing and Calculating with Datetime

DateTime objects support comparison operations and arithmetic calculations, making it easy to determine time differences and relationships between dates.

Advertisement

Comparing Dates

from datetime import datetime

# Create two datetime objects
datetime1 = datetime.strptime('07/11/2019 02:45PM', '%m/%d/%Y %I:%M%p')
datetime2 = datetime.strptime('08/11/2019 05:45PM', '%m/%d/%Y %I:%M%p')

# Compare dates
if datetime1 > datetime2:
    print("datetime1 is later")
elif datetime2 > datetime1:
    print("datetime2 is later")  # This will print
else:
    print("Dates are equal")

# Check if date is in the past
now = datetime.now()
if datetime1 < now:
    print("datetime1 is in the past")

Calculating Time Differences

from datetime import datetime

datetime1 = datetime.strptime('07/11/2019', '%m/%d/%Y')
datetime2 = datetime.strptime('08/11/2019', '%m/%d/%Y')

# Calculate difference
difference = datetime2 - datetime1
print(f"Days between: {difference.days}")  # Output: 31

# Access total seconds
total_seconds = difference.total_seconds()
print(f"Total seconds: {total_seconds}")

Adding and Subtracting Time

from datetime import datetime, timedelta

# Current date
now = datetime.now()

# Add 7 days
future_date = now + timedelta(days=7)
print(future_date.strftime("%Y-%m-%d"))

# Subtract 2 weeks
past_date = now - timedelta(weeks=2)
print(past_date.strftime("%Y-%m-%d"))

# Add hours and minutes
future_time = now + timedelta(hours=3, minutes=30)
print(future_time.strftime("%Y-%m-%d %H:%M:%S"))

Working with Timezones (the modern way: zoneinfo)

Handling timezones properly is crucial for applications that work across different geographic regions. Since Python 3.9 you no longer need the third-party pytz library — the standard library ships zoneinfo, which reads your system's IANA timezone database. It works directly with astimezone() and the tzinfo argument, with none of pytz's localize()/normalize() awkwardness.

Creating Timezone-Aware Datetimes

from datetime import datetime
from zoneinfo import ZoneInfo   # Python 3.9+, standard library

# Current time, already aware, in a specific zone
now_eastern = datetime.now(ZoneInfo("America/New_York"))
print(now_eastern)  # e.g. 2026-07-18 10:30:00-04:00

# Build a specific aware datetime — pass tzinfo at construction
specific_time = datetime(2026, 7, 11, 14, 30, tzinfo=ZoneInfo("America/Los_Angeles"))
print(specific_time)  # 2026-07-11 14:30:00-07:00

Converting Between Timezones

from datetime import datetime
from zoneinfo import ZoneInfo

# Start with an AWARE datetime
eastern_time = datetime(2026, 7, 11, 14, 30, tzinfo=ZoneInfo("America/New_York"))

# astimezone() adjusts BOTH the clock and the tzinfo — same instant, new zone
pacific_time = eastern_time.astimezone(ZoneInfo("America/Los_Angeles"))

print(f"Eastern: {eastern_time}")   # 2026-07-11 14:30:00-04:00
print(f"Pacific: {pacific_time}")   # 2026-07-11 11:30:00-07:00

Trap: calling astimezone() on a naive datetime silently assumes it is local system time. Always make the source aware first.

UTC and Timezone Best Practices

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 1) Get the current moment in UTC — AWARE. This is the correct pattern.
utc_time = datetime.now(timezone.utc)
print(f"UTC: {utc_time}")          # 2026-07-18 14:30:00+00:00

# DO NOT use datetime.utcnow(): it returns a NAIVE datetime that is secretly
# UTC, so later conversions treat it as local time. Deprecated in Python 3.12.

# 2) Convert to a local timezone only at the display edge
local_time = utc_time.astimezone(ZoneInfo("America/New_York"))
print(f"Local: {local_time}")

# 3) Convert user input to UTC for storage
user_input = "07/11/2026 02:30PM"
naive = datetime.strptime(user_input, "%m/%d/%Y %I:%M%p")
local_dt = naive.replace(tzinfo=ZoneInfo("America/Los_Angeles"))  # interpret the wall clock
utc_dt = local_dt.astimezone(timezone.utc)
print(f"Stored as UTC: {utc_dt}")

Common Datetime Patterns

Getting Current Date and Time

from datetime import datetime, timezone

# Current local datetime (NAIVE — no timezone attached)
now = datetime.now()

# Current UTC datetime (AWARE — the recommended default for anything stored/shared)
now_utc = datetime.now(timezone.utc)

# Current date only
today = datetime.now().date()

# Current time only
current_time = datetime.now().time()

# Current year, month, day
year = datetime.now().year
month = datetime.now().month
day = datetime.now().day

Converting To and From Unix Timestamps

A Unix timestamp is the number of seconds since 1970-01-01 UTC. timestamp() goes datetime → float; fromtimestamp() goes back the other way.

from datetime import datetime, timezone

# datetime -> Unix timestamp (seconds since the epoch)
dt = datetime(2026, 7, 18, 14, 30, tzinfo=timezone.utc)
ts = dt.timestamp()
print(ts)  # 1784385000.0

# Unix timestamp -> datetime
# Pass tz=timezone.utc to get an AWARE UTC datetime (recommended).
# Omitting tz returns a NAIVE datetime in the machine's LOCAL zone.
back_utc = datetime.fromtimestamp(ts, timezone.utc)
print(back_utc)  # 2026-07-18 14:30:00+00:00

# Current epoch seconds, two equivalent ways
import time
print(datetime.now(timezone.utc).timestamp())
print(time.time())

Creating Specific Dates

from datetime import datetime, date, time

# Create specific datetime
specific = datetime(2023, 7, 11, 14, 30, 0)

# Create date only
date_only = date(2023, 7, 11)

# Create time only
time_only = time(14, 30, 0)

# Combine date and time
combined = datetime.combine(date_only, time_only)

Parsing ISO Format Dates

datetime.fromisoformat() is the fast, built-in ISO 8601 parser. One version gotcha: the trailing Z (Zulu / UTC) suffix is only accepted in Python 3.11+. On 3.9 and 3.10, replace Z with +00:00 first.

from datetime import datetime

# Basic ISO 8601 (naive)
iso_datetime = datetime.fromisoformat("2026-07-11T14:30:00")

# With an explicit offset -> AWARE
datetime_with_tz = datetime.fromisoformat("2026-07-11T14:30:00-04:00")

# 'Z' suffix: works natively on Python 3.11+
# datetime.fromisoformat("2026-07-11T14:30:00Z")

# Cross-version-safe handling of 'Z' (works on 3.9+)
raw = "2026-07-11T14:30:00Z"
safe = datetime.fromisoformat(raw.replace("Z", "+00:00"))
print(safe)  # 2026-07-11 14:30:00+00:00

# Going the other way — emit an ISO 8601 string
print(safe.isoformat())  # 2026-07-11T14:30:00+00:00

Error Handling

When working with datetime parsing, always handle potential errors:

from datetime import datetime

def safe_parse_date(date_string, format_mask):
    """Safely parse a date string with error handling."""
    try:
        return datetime.strptime(date_string, format_mask)
    except ValueError as e:
        print(f"Error parsing date '{date_string}': {e}")
        return None

# Example usage
date = safe_parse_date("2023-07-32", "%Y-%m-%d")  # Invalid day
if date:
    print(f"Parsed: {date}")
else:
    print("Failed to parse date")

Real-World Applications

Log File Timestamp Parsing

from datetime import datetime

def parse_log_timestamp(log_line):
    """Extract and parse timestamp from log line."""
    # Example log: "[2023-07-11 14:30:15] INFO: Application started"
    timestamp_str = log_line.split(']')[0][1:]
    return datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")

log = "[2023-07-11 14:30:15] INFO: Application started"
timestamp = parse_log_timestamp(log)
print(f"Log timestamp: {timestamp}")

Date Range Validation

from datetime import datetime, timedelta

def is_date_in_range(check_date, start_date, end_date):
    """Check if a date falls within a range."""
    return start_date <= check_date <= end_date

today = datetime.now()
week_ago = today - timedelta(days=7)
check_this = today - timedelta(days=3)

if is_date_in_range(check_this, week_ago, today):
    print("Date is within the last week")

Age Calculation

from datetime import datetime

def calculate_age(birth_date):
    """Calculate age from birth date."""
    today = datetime.now()
    age = today.year - birth_date.year

    # Adjust if birthday hasn't occurred this year
    if (today.month, today.day) < (birth_date.month, birth_date.day):
        age -= 1

    return age

birthdate = datetime(1990, 5, 15)
age = calculate_age(birthdate)
print(f"Age: {age} years")

Best Practices

  1. Default to timezone-aware UTC — use datetime.now(timezone.utc), never datetime.utcnow() (naive, deprecated in 3.12)
  2. Store dates in UTC and convert to local timezones only at the display edge with astimezone()
  3. Use zoneinfo (ZoneInfo), not pytz, for new code on Python 3.9+
  4. Use ISO 8601 format for data exchange and storage (fromisoformat() / isoformat())
  5. Handle parsing errors with try-except blocks around strptime()
  6. Be explicit with format masks to avoid ambiguity (e.g. %m/%d vs %d/%m)
  7. Never mix naive and aware datetimes — subtracting one from the other raises TypeError
  8. Test with edge cases like leap years, DST changes, and month boundaries

Conclusion

Python's datetime module provides a comprehensive set of tools for working with dates and times. By mastering string parsing with strptime(), formatting with strftime(), performing comparisons and calculations, and properly handling timezones, you'll be well-equipped to handle any date and time requirements in your Python applications.

Remember to always validate user input, handle errors gracefully, and be explicit about timezones to avoid common pitfalls. With practice, these datetime operations will become second nature and you'll be able to build robust time-aware applications with confidence.

Frequently Asked Questions

What is the difference between a naive and an aware datetime in Python?

A naive datetime has no timezone attached (its tzinfo is None), so it represents "wall clock" time with no way to know which zone it belongs to. An aware datetime carries a tzinfo object, so it maps unambiguously to a single moment on the global timeline. Only aware datetimes can be safely compared or subtracted across timezones, and only aware datetimes convert correctly with astimezone(). The rule: use aware datetimes anywhere times cross a machine, a user, or a timezone boundary; reserve naive ones for local, single-zone throwaway logic.

Why is datetime.utcnow() discouraged in modern Python?

Because utcnow() returns a naive datetime even though the value is UTC. The object has no tzinfo, so Python has no idea it is UTC, and any later astimezone() or comparison silently treats it as local time and produces wrong results. It was formally deprecated in Python 3.12. Use datetime.now(timezone.utc) instead, which returns an aware UTC datetime that behaves correctly everywhere.

What is the difference between strptime() and strftime() in Python?

strptime() (String Parse Time) converts a date string INTO a datetime object using a format mask, e.g. datetime.strptime('2026-07-18', '%Y-%m-%d'). strftime() (String Format Time) converts a datetime object BACK INTO a formatted string for display, e.g. dt.strftime('%B %d, %Y'). Memory aid: strPtime = Parse, strFtime = Format.

How do I get the current UTC time correctly in Python?

Call datetime.now(timezone.utc) after importing timezone from datetime. This returns an aware datetime whose tzinfo is UTC. Do not use datetime.utcnow() (naive, deprecated in 3.12) or datetime.now() with no argument (returns naive local time). For a Unix timestamp of the current moment, use datetime.now(timezone.utc).timestamp() or the shortcut time.time().

How do I convert a datetime from one timezone to another?

Start with an aware datetime, then call .astimezone(target_zone). Build zones with ZoneInfo from the standard library (Python 3.9+): from zoneinfo import ZoneInfo; then dt.astimezone(ZoneInfo('America/Los_Angeles')). astimezone() adjusts both the clock time and the tzinfo so it still points at the same instant. Calling astimezone() on a naive datetime assumes it is local time, which is usually a bug.

How do I parse an ISO 8601 datetime string in Python?

Use datetime.fromisoformat(). In Python 3.11+ it handles the full ISO 8601 range including a trailing 'Z' for UTC, e.g. datetime.fromisoformat('2026-07-18T14:30:00Z'). On 3.9 and 3.10 it accepts offsets like +00:00 but not the 'Z' suffix, so replace 'Z' with '+00:00' first, or use strptime with '%Y-%m-%dT%H:%M:%S%z'. To emit ISO 8601, call dt.isoformat().

Should I still use pytz for timezones?

For new code on Python 3.9 or later, no. Use the built-in zoneinfo module (ZoneInfo), which reads the system IANA tz database and needs no third-party dependency. pytz is still functional but requires the awkward localize() call and its own normalize() to handle DST correctly. zoneinfo works directly with the tzinfo argument and astimezone(), so it is both simpler and less error-prone.

How do I calculate the difference between two dates in Python?

Subtract one datetime from another to get a timedelta: delta = later - earlier. Read the gap with delta.days and delta.total_seconds(). Both operands must be the same kind: subtracting an aware datetime from a naive one raises TypeError. To shift a date, add or subtract a timedelta, e.g. now + timedelta(days=7, hours=3).

pythonprogrammingdatetimetutorial