Python Date & Time — datetime Module Mastery
The datetime module provides classes for manipulating dates, times, and time intervals.
Learning Objectives
- Create and manipulate date, time, and datetime objects
- Format dates with strftime and parse with strptime
- Calculate date differences with timedelta
- Handle timezones correctly
- Build real-world scheduling and timezone tools
Creating Dates and Times
from datetime import date, time, datetime, timedelta
# Date object
today = date.today()
print(today) # 2024-01-15
print(today.year) # 2024
print(today.month) # 1
print(today.day) # 15
print(today.weekday()) # 0 (Monday)
specific = date(2024, 12, 25)
print(specific) # 2024-12-25
# Time object
noon = time(12, 0, 0)
print(noon.hour) # 12
print(noon.minute) # 0
print(noon.second) # 0
# Datetime object
now = datetime.now()
specific_dt = datetime(2024, 6, 15, 14, 30, 0)
print(now) # 2024-01-15 14:30:00.123456
# From timestamp
dt_from_timestamp = datetime.fromtimestamp(1700000000)
print(dt_from_timestamp)
# Combine date and time
dt = datetime.combine(date.today(), time(12, 0))
print(dt)
# Extract components
dt = datetime(2024, 6, 15, 14, 30, 45)
print(dt.date()) # 2024-06-15
print(dt.time()) # 14:30:45
datetime vs date vs time
from datetime import date, time, datetime
# date: calendar date only
d = date(2024, 6, 15)
print(f"Date: {d}")
print(f"Year: {d.year}, Month: {d.month}, Day: {d.day}")
# time: time of day only
t = time(14, 30, 45, 123456)
print(f"Time: {t}")
print(f"Hour: {t.hour}, Minute: {t.minute}, Second: {t.second}, Microsecond: {t.microsecond}")
# datetime: date and time combined
dt = datetime(2024, 6, 15, 14, 30, 45, 123456)
print(f"Datetime: {dt}")
print(f"Date: {dt.date()}, Time: {dt.time()}")
Formatting Dates
now = datetime(2024, 6, 15, 14, 30, 45)
# strftime — format datetime to string
print(now.strftime("%Y-%m-%d")) # 2024-06-15
print(now.strftime("%B %d, %Y")) # June 15, 2024
print(now.strftime("%I:%M %p")) # 02:30 PM
print(now.strftime("%Y-%m-%d %H:%M:%S")) # 2024-06-15 14:30:45
print(now.strftime("%a, %b %d %Y")) # Sat, Jun 15 2024
print(now.strftime("%Y-%m-%dT%H:%M:%SZ")) # ISO 8601 format
# Common format codes
# %Y — Year (4 digits) %y — Year (2 digits)
# %m — Month (01-12) %B — Full month name
# %b — Abbreviated month %d — Day (01-31)
# %H — Hour (00-23) %I — Hour (01-12)
# %M — Minute (00-59) %S — Second (00-59)
# %A — Full weekday name %a — Abbreviated weekday
# %p — AM/PM %j — Day of year (001-366)
# %U — Week number (Sun) %W — Week number (Mon)
# %Z — Timezone name %z — UTC offset
Format Codes Reference Table
| Code | Meaning | Example |
|---|---|---|
%Y | 4-digit year | 2024 |
%y | 2-digit year | 24 |
%m | Month (zero-padded) | 06 |
%d | Day (zero-padded) | 15 |
%H | Hour (24-hour) | 14 |
%I | Hour (12-hour) | 02 |
%M | Minute | 30 |
%S | Second | 45 |
%p | AM/PM | PM |
%A | Weekday name | Saturday |
%B | Month name | June |
%b | Abbreviated month | Jun |
%a | Abbreviated weekday | Sat |
%j | Day of year | 167 |
%U | Week number (Sun) | 24 |
%W | Week number (Mon) | 24 |
%Z | Timezone name | UTC |
%z | UTC offset | +0000 |
%f | Microsecond | 123456 |
%% | Literal % | % |
Parsing Dates
from datetime import datetime
# strptime — parse string to datetime
date_str = "2024-06-15"
dt = datetime.strptime(date_str, "%Y-%m-%d")
print(dt) # 2024-06-15 00:00:00
# Flexible parsing
dt = datetime.strptime("15/06/2024 14:30", "%d/%m/%Y %H:%M")
dt = datetime.strptime("June 15, 2024", "%B %d, %Y")
dt = datetime.strptime("15-Jun-2024", "%d-%b-%Y")
dt = datetime.strptime("2024-06-15T14:30:00", "%Y-%m-%dT%H:%M:%S")
# Handle multiple formats
def parse_date(date_string):
formats = [
"%Y-%m-%d",
"%d/%m/%Y",
"%B %d, %Y",
"%m-%d-%Y",
"%Y/%m/%d",
]
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except ValueError:
continue
raise ValueError(f"Cannot parse date: {date_string}")
print(parse_date("2024-06-15")) # 2024-06-15 00:00:00
print(parse_date("15/06/2024")) # 2024-06-15 00:00:00
print(parse_date("June 15, 2024")) # 2024-06-15 00:00:00
Parsing Examples
from datetime import datetime
# ISO 8601 format
dt = datetime.fromisoformat("2024-06-15T14:30:45")
print(dt) # 2024-06-15 14:30:45
# With timezone
dt = datetime.fromisoformat("2024-06-15T14:30:45+05:30")
print(dt) # 2024-06-15 14:30:45+05:30
# Common formats
formats = [
"%Y-%m-%d", # 2024-06-15
"%Y/%m/%d", # 2024/06/15
"%d-%m-%Y", # 15-06-2024
"%d/%m/%Y", # 15/06/2024
"%B %d, %Y", # June 15, 2024
"%b %d, %Y", # Jun 15, 2024
"%Y-%m-%d %H:%M:%S", # 2024-06-15 14:30:45
"%Y-%m-%dT%H:%M:%S", # 2024-06-15T14:30:45
]
test_dates = [
"2024-06-15",
"15/06/2024",
"June 15, 2024",
"2024-06-15 14:30:45",
]
for date_str in test_dates:
for fmt in formats:
try:
dt = datetime.strptime(date_str, fmt)
print(f"Parsed '{date_str}' with '{fmt}': {dt}")
break
except ValueError:
continue
timedelta — Date Arithmetic
from datetime import timedelta, date, datetime
today = date.today()
# Add/subtract days
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
next_month = today + timedelta(days=30)
next_year = today + timedelta(days=365)
in_two_hours = datetime.now() + timedelta(hours=2)
# Subtract dates
birthday = date(1990, 5, 15)
age = today - birthday
print(f"You are {age.days} days old")
print(f"That's about {age.days // 365} years")
# Time difference
start = datetime.now()
import time
time.sleep(2)
elapsed = datetime.now() - start
print(f"Elapsed: {elapsed.total_seconds():.1f} seconds")
# timedelta components
td = timedelta(days=1, hours=2, minutes=30)
print(td.days) # 1
print(td.seconds) # 9000 (2*3600 + 30*60)
print(td.total_seconds()) # 99000.0
# Business days calculation
def add_business_days(start_date, days):
current = start_date
added = 0
while added < days:
current += timedelta(days=1)
if current.weekday() < 5: # Monday=0, Friday=4
added += 1
return current
next_business = add_business_days(date.today(), 5)
print(f"5 business days from now: {next_business}")
timedelta Arithmetic
from datetime import timedelta, date, datetime
# Basic arithmetic
td1 = timedelta(days=1)
td2 = timedelta(hours=12)
td3 = timedelta(minutes=30)
# Addition
total = td1 + td2 + td3
print(f"Total: {total}") # 1 day, 12:30:00
print(f"Total seconds: {total.total_seconds()}") # 131400.0
# Subtraction
diff = td1 - td2
print(f"Difference: {diff}") # 0:12:00
# Multiplication
doubled = td1 * 2
print(f"Doubled: {doubled}") # 2 days
# Comparison
print(td1 > td2) # True
print(td1 < timedelta(days=2)) # True
# Convert to different units
td = timedelta(days=1, hours=2, minutes=30, seconds=15)
print(f"Days: {td.days}")
print(f"Hours: {td.seconds // 3600}")
print(f"Minutes: {(td.seconds % 3600) // 60}")
print(f"Seconds: {td.seconds % 60}")
print(f"Total hours: {td.total_seconds() / 3600}")
Timezones
from datetime import datetime, timezone, timedelta
# UTC
utc_now = datetime.now(timezone.utc)
print(utc_now)
# Specific timezone using timedelta
est = timezone(timedelta(hours=-5))
eastern_now = datetime.now(est)
print(eastern_now)
# Make a naive datetime timezone-aware
naive = datetime(2024, 6, 15, 12, 0)
aware = naive.replace(tzinfo=timezone.utc)
print(aware)
# Convert between timezones (Python 3.9+)
from zoneinfo import ZoneInfo
tokyo_tz = ZoneInfo("Asia/Tokyo")
tokyo_now = datetime.now(tokyo_tz)
print(f"Tokyo: {tokyo_now}")
ny_tz = ZoneInfo("America/New_York")
ny_now = tokyo_now.astimezone(ny_tz)
print(f"New York: {ny_now}")
# List available timezones
import zoneinfo
all_zones = sorted(zoneinfo.available_timezones())
print(f"Total timezones: {len(all_zones)}")
Timezone Conversion
from datetime import datetime
from zoneinfo import ZoneInfo
def convert_timezone(dt, from_tz, to_tz):
"""Convert datetime from one timezone to another."""
source_tz = ZoneInfo(from_tz)
target_tz = ZoneInfo(to_tz)
# Make timezone-aware if naive
if dt.tzinfo is None:
dt = dt.replace(tzinfo=source_tz)
# Convert
return dt.astimezone(target_tz)
# Usage
meeting = datetime(2024, 6, 15, 14, 0)
tokyo_time = convert_timezone(meeting, "America/New_York", "Asia/Tokyo")
print(f"Meeting in Tokyo: {tokyo_time.strftime('%Y-%m-%d %H:%M %Z')}")
Timezone Awareness
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# Naive datetime (no timezone info)
naive = datetime(2024, 6, 15, 12, 0)
print(naive.tzinfo) # None
# Timezone-aware datetime
aware = datetime(2024, 6, 15, 12, 0, tzinfo=timezone.utc)
print(aware.tzinfo) # UTC
# Convert naive to aware
eastern = ZoneInfo("America/New_York")
aware_eastern = naive.replace(tzinfo=eastern)
print(aware_eastern.tzinfo) # America/New_York
# Convert between timezones
utc_time = datetime.now(timezone.utc)
tokyo_time = utc_time.astimezone(ZoneInfo("Asia/Tokyo"))
print(f"UTC: {utc_time}")
print(f"Tokyo: {tokyo_time}")
# Naive vs aware comparison
# naive == aware # TypeError: can't compare naive and aware datetimes
Real-World: Scheduler
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
class EventScheduler:
def __init__(self, timezone="UTC"):
self.events = []
self.tz = ZoneInfo(timezone)
def add_event(self, title, start_time, duration_minutes=60, recurring=False):
event = {
"title": title,
"start": start_time,
"duration": timedelta(minutes=duration_minutes),
"recurring": recurring,
"id": len(self.events)
}
self.events.append(event)
return event["id"]
def get_upcoming(self, hours=24):
now = datetime.now(self.tz)
cutoff = now + timedelta(hours=hours)
return [e for e in self.events if now <= e["start"] <= cutoff]
def get_events_on_date(self, target_date):
return [
e for e in self.events
if e["start"].date() == target_date
]
def time_until_event(self, event_id):
now = datetime.now(self.tz)
event = self.events[event_id]
delta = event["start"] - now
if delta.total_seconds() < 0:
return "Event has passed"
hours, remainder = divmod(int(delta.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
return f"{hours}h {minutes}m {seconds}s"
# Usage
scheduler = EventScheduler("America/New_York")
now = datetime.now(ZoneInfo("America/New_York"))
scheduler.add_event("Team Meeting", now + timedelta(hours=2))
scheduler.add_event("Lunch", now + timedelta(hours=4, minutes=30))
scheduler.add_event("Code Review", now + timedelta(days=1))
print(f"Upcoming: {len(scheduler.get_upcoming(hours=12))} events")
print(f"Time until meeting: {scheduler.time_until_event(0)}")
Real-World: Age Calculator
from datetime import date
def calculate_age(birth_date):
"""Calculate precise age in years, months, and days."""
today = date.today()
years = today.year - birth_date.year
months = today.month - birth_date.month
days = today.day - birth_date.day
if days < 0:
months -= 1
# Get days in previous month
import calendar
prev_month = today.month - 1 or 12
prev_year = today.year if today.month > 1 else today.year - 1
days += calendar.monthrange(prev_year, prev_month)[1]
if months < 0:
years -= 1
months += 12
return years, months, days
# Usage
birthday = date(1990, 5, 15)
years, months, days = calculate_age(birthday)
print(f"Age: {years} years, {months} months, {days} days")
# Days until next birthday
def days_until_birthday(birth_date):
today = date.today()
next_birthday = birth_date.replace(year=today.year)
if next_birthday < today:
next_birthday = next_birthday.replace(year=today.year + 1)
return (next_birthday - today).days
print(f"Days until birthday: {days_until_birthday(birthday)}")
Common Mistakes
from datetime import datetime, timedelta, timezone
# Mistake 1: Naive vs aware datetime
naive = datetime.now()
aware = datetime.now(timezone.utc)
# naive.astimezone(aware.tzinfo) # Fails in Python 3.9+
aware.astimezone(timezone.utc) # Works
# Fix: always use timezone-aware datetimes
dt = datetime.now(timezone.utc)
# Mistake 2: Mutable datetime
dt = datetime(2024, 6, 15)
# dt.replace() returns new object, doesn't modify in place
new_dt = dt.replace(day=16)
# Mistake 3: Timedelta doesn't have months/years
# timedelta(days=30) != 1 month
# timedelta(days=365) != 1 year
# Fix: use dateutil.relativedelta for calendar-aware math
from dateutil.relativedelta import relativedelta
dt = datetime(2024, 1, 31)
dt + relativedelta(months=1) # 2024-02-29 (handles leap years)
# Mistake 4: Timezone conversion without awareness
from zoneinfo import ZoneInfo
dt_naive = datetime(2024, 6, 15, 12, 0)
# dt_naive.replace(tzinfo=ZoneInfo("US/Eastern")) # Wrong! Assumes local time
# Fix: localize properly
from datetime import timezone
dt_utc = datetime(2024, 6, 15, 12, 0, tzinfo=timezone.utc)
dt_eastern = dt_utc.astimezone(ZoneInfo("US/Eastern"))
# Mistake 5: Comparing naive and aware datetimes
# naive_dt == aware_dt # TypeError!
Key Takeaways
datefor calendar dates,timefor times,datetimefor bothstrftime()formats to string,strptime()parses from stringtimedeltahandles date arithmetic- Always use
timezone.utcfor timezone-aware datetimes - Use
zoneinfo(Python 3.9+) for timezone support - Use
dateutil.relativedeltafor calendar-aware arithmetic (months, years) - Pre-compile format strings for repeated parsing
- Use
fromisoformat()for ISO 8601 parsing - Naive datetimes cannot be compared with aware datetimes
- Always validate timezone conversions with real-world examples