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
| Task | Do 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 → datetime | datetime.strptime(s, fmt) | Format mask must match exactly |
| Parse ISO 8601 | datetime.fromisoformat(s) | Z suffix needs 3.11+ (see below) |
| Format a datetime → string | dt.strftime(fmt) | Uses the same directive codes |
| Emit ISO 8601 string | dt.isoformat() | e.g. 2026-07-18T14:30:00+00:00 |
| Date arithmetic | dt + timedelta(days=7) | Add/subtract durations |
| Difference between two datetimes | later - earlier → timedelta | .days, .total_seconds() |
| Convert timezone | dt.astimezone(ZoneInfo("America/New_York")) | Requires an aware source |
| Attach a timezone to a naive dt | dt.replace(tzinfo=ZoneInfo("America/New_York")) | Interprets the wall clock as that zone |
| datetime → Unix timestamp | dt.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 + time | datetime.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
| Code | Description | Example |
|---|---|---|
| %Y | 4-digit year | 2023 |
| %m | Month as number (01-12) | 07 |
| %d | Day of month (01-31) | 15 |
| %H | Hour (24-hour format) | 14 |
| %I | Hour (12-hour format) | 02 |
| %M | Minute (00-59) | 45 |
| %S | Second (00-59) | 30 |
| %p | AM/PM indicator | PM |
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.
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.
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
- Default to timezone-aware UTC — use
datetime.now(timezone.utc), neverdatetime.utcnow()(naive, deprecated in 3.12) - Store dates in UTC and convert to local timezones only at the display edge with
astimezone() - Use
zoneinfo(ZoneInfo), notpytz, for new code on Python 3.9+ - Use ISO 8601 format for data exchange and storage (
fromisoformat()/isoformat()) - Handle parsing errors with try-except blocks around
strptime() - Be explicit with format masks to avoid ambiguity (e.g.
%m/%dvs%d/%m) - Never mix naive and aware datetimes — subtracting one from the other raises
TypeError - 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.