What this page does
Introduces the datetime module and what you will learn.
Where this fits
This is the starting point. You should know basic Python before starting.
Explanation
The datetime module provides classes for manipulating dates and times. By the end of this guide, you will know how to:
- Create dates ā Specific days, today's date
- Create times ā Hours, minutes, seconds, microseconds
- Create datetimes ā Combined date and time
- Calculate durations ā Add/subtract time with timedelta
- Format output ā Convert to any string format
- Parse input ā Convert strings to datetime objects
- Handle timezones ā UTC and local time
The datetime module is built into Python. No installation required.
Why this matters
Every application deals with dates: birthdays, deadlines, schedules, logs, expiration dates, billing cycles. datetime is essential.
ā If something breaks here
Nothing to break yet. Move to Page 2.
What this page does
Sets up a Python file for practicing the datetime module.
Where this fits
Before writing code, create a workspace.
Code (this page)
mkdir ~/projects/datetime-practice
cd ~/projects/datetime-practice
touch datetime_basics.py
Explanation
These commands:
- Create a folder called
datetime-practice
- Enter that folder
- Create an empty Python file
From now on, you will write code in
datetime_basics.py and run it with
python3 datetime_basics.py.
Why this matters
Having a dedicated file lets you build up code incrementally and re-run to test changes.
ā If something breaks here
mkdir: File exists: The folder exists. Just cd into it
What this page does
Shows the different ways to import from the datetime module.
Where this fits
Your file is ready. Now understand the import options.
Code (this page)
# Option 1: Import the module
import datetime
print(datetime.date.today())# Option 2: Import specific classes (most common)
from datetime import date, time, datetime, timedelta
print(date.today())
# Note: 'datetime' is both a module AND a class inside it
# This can be confusing at first
Explanation
The datetime module contains several classes:
date ā just a date (year, month, day)
time ā just a time (hour, minute, second)
datetime ā date AND time combined
timedelta ā a duration (difference between times)
Most code uses
from datetime import ... to avoid writing
datetime.datetime.
Why this matters
Understanding the import clarifies the confusing datetime.datetime pattern you'll see in code.
ā If something breaks here
NameError: Make sure you imported the class you're using
What this page does
Creates a date object for a specific day.
Where this fits
Module imported. Now create your first date.
Code (this page)
from datetime import date# Create a specific date
birthday = date(2000, 6, 15) # Year, Month, Day
print("Birthday:", birthday)
# Access components
print(f"Year: {birthday.year}")
print(f"Month: {birthday.month}")
print(f"Day: {birthday.day}")
# Invalid dates raise errors
# date(2026, 2, 30) # ValueError: day is out of range for month
Explanation
Run the file:
Birthday: 2000-06-15
Year: 2000
Month: 6
Day: 15
date(year, month, day):
- Year: any valid year (1-9999)
- Month: 1-12
- Day: 1-31 (validated against the month)
Invalid dates (Feb 30, Month 13) raise
ValueError.
Why this matters
Dates are the foundation. Birthdays, deadlines, events ā all start with a date object.
ā If something breaks here
ValueError: Check that month is 1-12 and day is valid for that month
What this page does
Gets the current date.
Where this fits
You created specific dates. Now get today.
Code (this page)
from datetime import datetoday = date.today()
print("Today is:", today)
print(f"Year: {today.year}")
print(f"Month: {today.month}")
print(f"Day: {today.day}")
# Weekday (0=Monday, 6=Sunday)
print(f"Weekday number: {today.weekday()}")
# ISO weekday (1=Monday, 7=Sunday)
print(f"ISO weekday: {today.isoweekday()}")
Explanation
Run the file:
Today is: 2026-01-28
Year: 2026
Month: 1
Day: 28
Weekday number: 2
ISO weekday: 3
date.today():
- Returns current local date
.weekday() returns 0-6 (Monday=0)
.isoweekday() returns 1-7 (Monday=1)
Why this matters
"What's today?" is a common question. Most date logic starts from today.
ā If something breaks here
Nothing should break. Date is always available.
What this page does
Creates a time object for a specific time of day.
Where this fits
You know dates. Now learn times (without dates).
Code (this page)
from datetime import time# Create a specific time
meeting = time(14, 30) # 2:30 PM
print("Meeting time:", meeting)
# With seconds and microseconds
precise = time(14, 30, 45, 123456)
print("Precise time:", precise)
# Access components
print(f"Hour: {meeting.hour}")
print(f"Minute: {meeting.minute}")
print(f"Second: {meeting.second}")
print(f"Microsecond: {meeting.microsecond}")
Explanation
Run the file:
Meeting time: 14:30:00
Precise time: 14:30:45.123456
Hour: 14
Minute: 30
Second: 0
Microsecond: 0
time(hour, minute, second, microsecond):
- Hour: 0-23 (24-hour format)
- Minute: 0-59
- Second: 0-59
- Microsecond: 0-999999
All parameters except hour are optional (default to 0).
Why this matters
Some applications need time without date: schedules, alarms, business hours.
ā If something breaks here
ValueError: Hour must be 0-23, minute/second 0-59
What this page does
Creates a datetime object combining date and time.
Where this fits
You know date and time separately. Now combine them.
Code (this page)
from datetime import datetime# Create a specific datetime
event = datetime(2026, 12, 25, 18, 30, 0)
print("Event:", event)
# Access date components
print(f"Year: {event.year}")
print(f"Month: {event.month}")
print(f"Day: {event.day}")
# Access time components
print(f"Hour: {event.hour}")
print(f"Minute: {event.minute}")
print(f"Second: {event.second}")
Explanation
Run the file:
Event: 2026-12-25 18:30:00
Year: 2026
Month: 12
Day: 25
Hour: 18
Minute: 30
Second: 0
datetime(year, month, day, hour, minute, second, microsecond):
- First three (date) are required
- Last four (time) default to 0
Why this matters
Most real events have both date AND time: appointments, log entries, transactions.
ā If something breaks here
TypeError: Year, month, day are required
What this page does
Gets the current date and time.
Where this fits
You created specific datetimes. Now get the current moment.
Code (this page)
from datetime import datetime# Current local datetime
now = datetime.now()
print("Now:", now)
# Current UTC datetime
utc_now = datetime.utcnow()
print("UTC:", utc_now)
# Just the date part
print("Date part:", now.date())
# Just the time part
print("Time part:", now.time())
Explanation
Run the file:
Now: 2026-01-28 14:30:45.123456
UTC: 2026-01-28 22:30:45.123456
Date part: 2026-01-28
Time part: 14:30:45.123456
datetime.now():
- Returns current local datetime
.date() extracts just the date
.time() extracts just the time
datetime.utcnow():
- Returns current UTC datetime (no timezone info attached)
Why this matters
Logging, timestamps, "created at" fields ā all need the current datetime.
ā If something breaks here
Nothing should break. Now always works.
What this page does
Creates duration objects for time arithmetic.
Where this fits
You have datetimes. Now calculate differences.
Code (this page)
from datetime import timedelta# Create durations
one_day = timedelta(days=1)
one_week = timedelta(weeks=1)
two_hours = timedelta(hours=2)
thirty_minutes = timedelta(minutes=30)
print("One day:", one_day)
print("One week:", one_week)
print("Two hours:", two_hours)
print("30 minutes:", thirty_minutes)
# Combined
complex_duration = timedelta(days=2, hours=5, minutes=30)
print("2 days, 5 hours, 30 min:", complex_duration)
Explanation
Run the file:
One day: 1 day, 0:00:00
One week: 7 days, 0:00:00
Two hours: 2:00:00
30 minutes: 0:30:00
2 days, 5 hours, 30 min: 2 days, 5:30:00
timedelta(weeks, days, hours, minutes, seconds, milliseconds, microseconds):
- All parameters are optional
- Internally stored as days, seconds, and microseconds
- Weeks are converted to days (1 week = 7 days)
Why this matters
Timedelta is how you do date math: "What's 30 days from now?" "How long until deadline?"
ā If something breaks here
Nothing should break. All parameters are optional.
What this page does
Adds and subtracts time from dates.
Where this fits
You have dates and timedeltas. Now combine them.
Code (this page)
from datetime import date, timedeltatoday = date.today()
print("Today:", today)
# Add days
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
print("Tomorrow:", tomorrow)
print("Next week:", next_week)
# Subtract days
yesterday = today - timedelta(days=1)
last_month = today - timedelta(days=30)
print("Yesterday:", yesterday)
print("30 days ago:", last_month)
# Difference between dates
new_years = date(2027, 1, 1)
days_until = new_years - today
print(f"Days until 2027: {days_until.days}")
Explanation
Run the file:
Today: 2026-01-28
Tomorrow: 2026-01-29
Next week: 2026-02-04
Yesterday: 2026-01-27
30 days ago: 2025-12-29
Days until 2027: 338
Arithmetic rules:
date + timedelta = date
date - timedelta = date
date - date = timedelta
Why this matters
"Due in 30 days", "Expires in 1 year", "Days since signup" ā all date arithmetic.
ā If something breaks here
TypeError: Can only add/subtract timedelta to dates, not integers
What this page does
Adds and subtracts time from datetimes (including hours/minutes).
Where this fits
Date arithmetic works. Now add time precision.
Code (this page)
from datetime import datetime, timedeltanow = datetime.now()
print("Now:", now)
# Add hours and minutes
meeting_start = now + timedelta(hours=2)
meeting_end = meeting_start + timedelta(minutes=45)
print("Meeting starts:", meeting_start)
print("Meeting ends:", meeting_end)
# Subtract to find past times
three_hours_ago = now - timedelta(hours=3)
print("3 hours ago:", three_hours_ago)
# Difference between datetimes
deadline = datetime(2026, 2, 1, 9, 0, 0)
remaining = deadline - now
print(f"Time until deadline: {remaining}")
print(f"That's {remaining.days} days and {remaining.seconds // 3600} hours")
Explanation
Run the file:
Now: 2026-01-28 14:30:00.123456
Meeting starts: 2026-01-28 16:30:00.123456
Meeting ends: 2026-01-28 17:15:00.123456
3 hours ago: 2026-01-28 11:30:00.123456
Time until deadline: 3 days, 18:30:00
That's 3 days and 18 hours
Same rules as date:
datetime + timedelta = datetime
datetime - timedelta = datetime
datetime - datetime = timedelta
Why this matters
Scheduling, countdowns, session expiration ā all need datetime precision.
ā If something breaks here
Nothing should break. Same patterns as date arithmetic.
What this page does
Shows how to compare dates and datetimes.
Where this fits
You can calculate differences. Now compare directly.
Code (this page)
from datetime import date, datetime# Compare dates
today = date.today()
birthday = date(2026, 6, 15)
print(f"Today: {today}")
print(f"Birthday: {birthday}")
print(f"Birthday passed? {today > birthday}")
print(f"Birthday upcoming? {today < birthday}")
print(f"Is today birthday? {today == birthday}")
# Compare datetimes
now = datetime.now()
deadline = datetime(2026, 2, 1, 17, 0, 0)
if now < deadline:
print("\nDeadline is in the future")
elif now > deadline:
print("\nDeadline has passed")
else:
print("\nIt's exactly the deadline!")
Explanation
Run the file:
Today: 2026-01-28
Birthday: 2026-06-15
Birthday passed? False
Birthday upcoming? True
Is today birthday? FalseDeadline is in the future
Comparison operators work naturally:
< earlier than
> later than
== same moment
<=, >=, != also work
Why this matters
"Is this expired?", "Has the event started?", "Is the user's subscription active?" ā all comparisons.
ā If something breaks here
TypeError: Can only compare date with date, datetime with datetime
What this page does
Converts datetime to formatted strings.
Where this fits
You have datetime objects. Now display them nicely.
Code (this page)
from datetime import datetimenow = datetime.now()
# Common formats
print(now.strftime("%Y-%m-%d")) # 2026-01-28
print(now.strftime("%d/%m/%Y")) # 28/01/2026
print(now.strftime("%B %d, %Y")) # January 28, 2026
print(now.strftime("%H:%M:%S")) # 14:30:45
print(now.strftime("%I:%M %p")) # 02:30 PM
print(now.strftime("%A, %B %d, %Y")) # Wednesday, January 28, 2026
print(now.strftime("%Y-%m-%d %H:%M:%S")) # 2026-01-28 14:30:45
# Custom formats
print(now.strftime("Today is %A")) # Today is Wednesday
print(now.strftime("Time: %H:%M")) # Time: 14:30
Explanation
Run the file:
2026-01-28
28/01/2026
January 28, 2026
14:30:45
02:30 PM
Wednesday, January 28, 2026
2026-01-28 14:30:45
Today is Wednesday
Time: 14:30
strftime(format) replaces codes with values:
%Y = 4-digit year
%m = month (01-12)
%d = day (01-31)
%H = hour 24h, %I = hour 12h
%M = minute, %S = second
%A = weekday name, %B = month name
Why this matters
Users don't read 2026-01-28 14:30:00.123456. They read "January 28, 2026 at 2:30 PM".
ā If something breaks here
- Wrong output: Format codes are case-sensitive (
%m ā %M)
What this page does
Converts strings to datetime objects.
Where this fits
strftime outputs strings. strptime parses them back.
Code (this page)
from datetime import datetime# Parse different formats
dt1 = datetime.strptime("2026-01-28", "%Y-%m-%d")
print("Parsed:", dt1)
dt2 = datetime.strptime("28/01/2026", "%d/%m/%Y")
print("Parsed:", dt2)
dt3 = datetime.strptime("January 28, 2026", "%B %d, %Y")
print("Parsed:", dt3)
dt4 = datetime.strptime("2026-01-28 14:30:00", "%Y-%m-%d %H:%M:%S")
print("Parsed with time:", dt4)
# Parse from user input
date_string = "2026-12-25"
christmas = datetime.strptime(date_string, "%Y-%m-%d")
print(f"Christmas is on a {christmas.strftime('%A')}")
Explanation
Run the file:
Parsed: 2026-01-28 00:00:00
Parsed: 2026-01-28 00:00:00
Parsed: 2026-01-28 00:00:00
Parsed with time: 2026-01-28 14:30:00
Christmas is on a Friday
datetime.strptime(string, format):
- Parses string according to format
- Returns datetime object
- Time defaults to 00:00:00 if not in format
Why this matters
User input, CSV files, API responses ā all give you date strings that need parsing.
ā If something breaks here
ValueError: Format doesn't match the string. Check carefully.
What this page does
Shows the standard ISO 8601 date format.
Where this fits
Custom formats vary. ISO is the universal standard.
Code (this page)
from datetime import datetime, date# ISO format output
now = datetime.now()
print("ISO datetime:", now.isoformat())
today = date.today()
print("ISO date:", today.isoformat())
# With custom separator
print("Custom sep:", now.isoformat(sep=' '))
# Parse ISO format
iso_string = "2026-01-28T14:30:00"
parsed = datetime.fromisoformat(iso_string)
print("Parsed ISO:", parsed)
# Also works with date
date_string = "2026-01-28"
parsed_date = date.fromisoformat(date_string)
print("Parsed date:", parsed_date)
Explanation
Run the file:
ISO datetime: 2026-01-28T14:30:45.123456
ISO date: 2026-01-28
Custom sep: 2026-01-28 14:30:45.123456
Parsed ISO: 2026-01-28 14:30:00
Parsed date: 2026-01-28
ISO 8601 format: YYYY-MM-DDTHH:MM:SS
- Universal, unambiguous
- Used by APIs, databases, JSON
.isoformat() outputs it
.fromisoformat() parses it (Python 3.7+)
Why this matters
APIs and databases expect ISO format. It sorts correctly as text. It's the safest choice.
ā If something breaks here
fromisoformat not found: Requires Python 3.7+
What this page does
Converts between datetime and Unix timestamps.
Where this fits
Sometimes you need timestamps instead of datetime objects.
Code (this page)
from datetime import datetime# Datetime to timestamp
now = datetime.now()
timestamp = now.timestamp()
print(f"Datetime: {now}")
print(f"Timestamp: {timestamp}")
# Timestamp to datetime
ts = 1737849600 # A specific moment
dt = datetime.fromtimestamp(ts)
print(f"\nTimestamp {ts}")
print(f"As datetime: {dt}")
# UTC timestamp to datetime
dt_utc = datetime.utcfromtimestamp(ts)
print(f"As UTC: {dt_utc}")
# Current timestamp shortcut
import time
print(f"\ntime.time(): {time.time()}")
print(f"datetime.now().timestamp(): {datetime.now().timestamp()}")
Explanation
Run the file:
Datetime: 2026-01-28 14:30:45.123456
Timestamp: 1737849045.123456Timestamp 1737849600
As datetime: 2026-01-28 14:40:00
As UTC: 2026-01-28 22:40:00
time.time(): 1737849045.123456
datetime.now().timestamp(): 1737849045.123456
Conversion methods:
.timestamp() ā datetime to Unix timestamp
fromtimestamp(ts) ā timestamp to local datetime
utcfromtimestamp(ts) ā timestamp to UTC datetime
Why this matters
Databases often store timestamps. APIs return them. You need to convert both ways.
ā If something breaks here
OSError: Timestamp out of valid range
What this page does
Creates new datetime by changing specific parts.
Where this fits
Sometimes you need to modify just one component.
Code (this page)
from datetime import datetimenow = datetime.now()
print("Now:", now)
# Change the year
next_year = now.replace(year=2027)
print("Next year:", next_year)
# Change multiple parts
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
print("Today at midnight:", midnight)
# Set specific time
noon = now.replace(hour=12, minute=0, second=0, microsecond=0)
print("Today at noon:", noon)
# End of month
end_of_month = now.replace(day=31) # Might fail if month doesn't have 31 days
# Safer: use calendar or go to next month and subtract
Explanation
Run the file:
Now: 2026-01-28 14:30:45.123456
Next year: 2027-01-28 14:30:45.123456
Today at midnight: 2026-01-28 00:00:00
Today at noon: 2026-01-28 12:00:00
dt.replace(year=, month=, day=, hour=, minute=, second=, microsecond=):
- Returns a NEW datetime (original unchanged)
- Only specify the parts you want to change
- Invalid combinations raise
ValueError
Why this matters
"Same time tomorrow", "start of today", "change to midnight" ā all need replace().
ā If something breaks here
ValueError: day=31 doesn't work for all months
What this page does
Shows how to work with days of the week.
Where this fits
Many schedules depend on weekdays.
Code (this page)
from datetime import date, timedeltatoday = date.today()
print(f"Today: {today} ({today.strftime('%A')})")
print(f"Weekday: {today.weekday()} (0=Mon, 6=Sun)")
# Find next Monday
days_until_monday = (7 - today.weekday()) % 7
if days_until_monday == 0:
days_until_monday = 7 # If today is Monday, get next Monday
next_monday = today + timedelta(days=days_until_monday)
print(f"Next Monday: {next_monday}")
# Find last Friday
days_since_friday = (today.weekday() - 4) % 7
if days_since_friday == 0:
days_since_friday = 7 # If today is Friday, get last Friday
last_friday = today - timedelta(days=days_since_friday)
print(f"Last Friday: {last_friday}")
# Is it a weekend?
is_weekend = today.weekday() >= 5
print(f"Is weekend: {is_weekend}")
# Days until weekend
if today.weekday() < 5: # Weekday
days_to_weekend = 5 - today.weekday()
print(f"Days until weekend: {days_to_weekend}")
else:
print("It's the weekend!")
Explanation
Run the file (results depend on current day):
Today: 2026-01-28 (Wednesday)
Weekday: 2 (0=Mon, 6=Sun)
Next Monday: 2026-02-02
Last Friday: 2026-01-23
Is weekend: False
Days until weekend: 2
Weekday math:
- Monday=0, Sunday=6
- Use modulo
% 7 for wrapping
- Weekend = weekday >= 5
Why this matters
Business logic often depends on weekdays: "next business day", "last Friday", "skip weekends".
ā If something breaks here
- Off by one: Double-check your modulo math
What this page does
Introduces timezone-aware datetimes.
Where this fits
So far, all datetimes are "naive" (no timezone). Now add awareness.
Code (this page)
from datetime import datetime, timezone# Naive datetime (no timezone info)
naive = datetime.now()
print(f"Naive: {naive}")
print(f"Timezone info: {naive.tzinfo}")
# UTC timezone-aware datetime
utc_now = datetime.now(timezone.utc)
print(f"\nUTC aware: {utc_now}")
print(f"Timezone info: {utc_now.tzinfo}")
# Check if aware or naive
print(f"\nNaive has tz? {naive.tzinfo is not None}")
print(f"UTC has tz? {utc_now.tzinfo is not None}")
Explanation
Run the file:
Naive: 2026-01-28 14:30:45.123456
Timezone info: NoneUTC aware: 2026-01-28 22:30:45.123456+00:00
Timezone info: UTC
Naive has tz? False
UTC has tz? True
Two types of datetime:
- Naive: No timezone info (
.tzinfo is None)
- Aware: Has timezone info
timezone.utc is the built-in UTC timezone.
Why this matters
Global applications need timezone awareness. "3 PM" means different things in different places.
ā If something breaks here
Nothing should break. Timezone is built-in.
What this page does
Creates custom timezone offsets.
Where this fits
UTC is one timezone. Now handle others.
Code (this page)
from datetime import datetime, timezone, timedelta# Create timezone with offset
eastern = timezone(timedelta(hours=-5)) # EST (UTC-5)
pacific = timezone(timedelta(hours=-8)) # PST (UTC-8)
india = timezone(timedelta(hours=5, minutes=30)) # IST (UTC+5:30)
# Get current time in different zones
utc_now = datetime.now(timezone.utc)
print(f"UTC: {utc_now.strftime('%H:%M %Z')}")
eastern_now = utc_now.astimezone(eastern)
print(f"Eastern: {eastern_now.strftime('%H:%M %Z')}")
pacific_now = utc_now.astimezone(pacific)
print(f"Pacific: {pacific_now.strftime('%H:%M %Z')}")
india_now = utc_now.astimezone(india)
print(f"India: {india_now.strftime('%H:%M %Z')}")
Explanation
Run the file:
UTC: 22:30 UTC
Eastern: 17:30 UTC-05:00
Pacific: 14:30 UTC-08:00
India: 04:00 UTC+05:30
timezone(timedelta(...)):
- Creates a fixed offset timezone
astimezone(tz) converts between timezones
- Same moment in time, different local representation
Why this matters
Scheduling across timezones needs conversion. "Meeting at 3 PM Eastern" ā "What time is that for me?"
ā If something breaks here
- Wrong offset: Remember negative for west of UTC
What this page does
Shows the modern way to handle named timezones.
Where this fits
Fixed offsets don't handle daylight saving. Named zones do.
Code (this page)
from datetime import datetime
try:
from zoneinfo import ZoneInfo
except ImportError:
print("zoneinfo requires Python 3.9+")
exit()# Named timezones (handle DST automatically)
eastern = ZoneInfo("America/New_York")
pacific = ZoneInfo("America/Los_Angeles")
london = ZoneInfo("Europe/London")
tokyo = ZoneInfo("Asia/Tokyo")
# Current time in each zone
now_utc = datetime.now(ZoneInfo("UTC"))
print(f"UTC: {now_utc.strftime('%Y-%m-%d %H:%M %Z')}")
now_eastern = now_utc.astimezone(eastern)
print(f"New York: {now_eastern.strftime('%Y-%m-%d %H:%M %Z')}")
now_pacific = now_utc.astimezone(pacific)
print(f"LA: {now_pacific.strftime('%Y-%m-%d %H:%M %Z')}")
now_london = now_utc.astimezone(london)
print(f"London: {now_london.strftime('%Y-%m-%d %H:%M %Z')}")
now_tokyo = now_utc.astimezone(tokyo)
print(f"Tokyo: {now_tokyo.strftime('%Y-%m-%d %H:%M %Z')}")
Explanation
Run the file:
UTC: 2026-01-28 22:30 UTC
New York: 2026-01-28 17:30 EST
LA: 2026-01-28 14:30 PST
London: 2026-01-28 22:30 GMT
Tokyo: 2026-01-29 07:30 JST
ZoneInfo("timezone_name"):
- Uses IANA timezone database
- Handles daylight saving automatically
- Names like "America/New_York", "Europe/London"
Why this matters
Daylight saving changes offsets. Named zones handle this correctly.
ā If something breaks here
ModuleNotFoundError: Requires Python 3.9+
ZoneInfoNotFoundError: Invalid timezone name
What this page does
Adds timezone info to naive datetimes.
Where this fits
Legacy code often has naive datetimes. Convert them.
Code (this page)
from datetime import datetime, timezone, timedelta# Naive datetime
naive = datetime(2026, 6, 15, 14, 30)
print(f"Naive: {naive} (tzinfo={naive.tzinfo})")
# Add UTC timezone (assume it WAS UTC)
utc_aware = naive.replace(tzinfo=timezone.utc)
print(f"As UTC: {utc_aware}")
# Add custom timezone
eastern = timezone(timedelta(hours=-5))
eastern_aware = naive.replace(tzinfo=eastern)
print(f"As Eastern: {eastern_aware}")
# IMPORTANT: replace doesn't convert, it labels!
# These represent DIFFERENT moments in time:
print(f"\nUTC timestamp: {utc_aware.timestamp()}")
print(f"Eastern timestamp: {eastern_aware.timestamp()}")
print("(Different because they're different actual times)")
Explanation
Run the file:
Naive: 2026-06-15 14:30:00 (tzinfo=None)
As UTC: 2026-06-15 14:30:00+00:00
As Eastern: 2026-06-15 14:30:00-05:00UTC timestamp: 1750000200.0
Eastern timestamp: 1750018200.0
(Different because they're different actual times)
replace(tzinfo=tz):
- LABELS the datetime with a timezone
- Does NOT convert the time
- Use when you know what timezone the naive datetime was in
Why this matters
Converting naive to aware is common when reading from databases or files that don't store timezone.
ā If something breaks here
- Can't mix aware and naive in comparisons
What this page does
Shows frequently needed date calculations.
Where this fits
Build practical skills with real scenarios.
Code (this page)
from datetime import date, datetime, timedeltatoday = date.today()
# First day of current month
first_of_month = today.replace(day=1)
print(f"First of month: {first_of_month}")
# Last day of current month
next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
last_of_month = next_month - timedelta(days=1)
print(f"Last of month: {last_of_month}")
# First day of current year
first_of_year = today.replace(month=1, day=1)
print(f"First of year: {first_of_year}")
# Days remaining in year
dec_31 = date(today.year, 12, 31)
days_remaining = (dec_31 - today).days
print(f"Days left in year: {days_remaining}")
# Age calculation
birthdate = date(1990, 6, 15)
age = today.year - birthdate.year
if (today.month, today.day) < (birthdate.month, birthdate.day):
age -= 1 # Birthday hasn't happened yet this year
print(f"Age: {age} years")
Explanation
Run the file:
First of month: 2026-01-01
Last of month: 2026-01-31
First of year: 2026-01-01
Days left in year: 337
Age: 35 years
Common patterns:
- First of month:
replace(day=1)
- Last of month: go to next month, subtract 1 day
- Age: subtract years, adjust if birthday hasn't passed
Why this matters
These calculations appear constantly in business applications.
ā If something breaks here
- Last of month logic: handles months with 28-31 days correctly
What this page does
Creates sequences of dates.
Where this fits
Reports and calendars need date ranges.
Code (this page)
from datetime import date, timedeltadef date_range(start, end):
"""Generate dates from start to end (inclusive)."""
current = start
while current <= end:
yield current
current += timedelta(days=1)
# Generate a week of dates
start = date(2026, 1, 26)
end = date(2026, 2, 1)
print("Week of dates:")
for d in date_range(start, end):
print(f" {d} ({d.strftime('%A')})")
# Generate working days only
print("\nWorking days only:")
for d in date_range(start, end):
if d.weekday() < 5: # Monday-Friday
print(f" {d}")
# Count days
total = sum(1 for d in date_range(start, end))
work_days = sum(1 for d in date_range(start, end) if d.weekday() < 5)
print(f"\nTotal: {total} days, {work_days} working days")
Explanation
Run the file:
Week of dates:
2026-01-26 (Monday)
2026-01-27 (Tuesday)
2026-01-28 (Wednesday)
2026-01-29 (Thursday)
2026-01-30 (Friday)
2026-01-31 (Saturday)
2026-02-01 (Sunday)Working days only:
2026-01-26
2026-01-27
2026-01-28
2026-01-29
2026-01-30
Total: 7 days, 5 working days
Generator pattern:
- Use
yield for memory efficiency
- Filter with conditions (weekday < 5)
- Works with any date range
Why this matters
Calendar views, billing periods, scheduling ā all need date ranges.
ā If something breaks here
Nothing should break. Adjust start/end as needed.
What this page does
Handles dates in various formats from different sources.
Where this fits
Real data comes in many formats. Parse them all.
Code (this page)
from datetime import datetimedef parse_date_flexible(date_string):
"""Try multiple formats to parse a date string."""
formats = [
"%Y-%m-%d", # 2026-01-28
"%d/%m/%Y", # 28/01/2026
"%m/%d/%Y", # 01/28/2026
"%B %d, %Y", # January 28, 2026
"%d %B %Y", # 28 January 2026
"%Y-%m-%d %H:%M:%S", # 2026-01-28 14:30:00
"%d/%m/%Y %H:%M", # 28/01/2026 14:30
]
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except ValueError:
continue
raise ValueError(f"Cannot parse date: {date_string}")
# Test various formats
test_dates = [
"2026-01-28",
"28/01/2026",
"January 28, 2026",
"2026-01-28 14:30:00",
]
for date_str in test_dates:
parsed = parse_date_flexible(date_str)
print(f"'{date_str}' ā {parsed}")
Explanation
Run the file:
'2026-01-28' ā 2026-01-28 00:00:00
'28/01/2026' ā 2026-01-28 00:00:00
'January 28, 2026' ā 2026-01-28 00:00:00
'2026-01-28 14:30:00' ā 2026-01-28 14:30:00
The pattern:
- Try each format in order
- Return on first success
- Raise error if none match
Why this matters
Users enter dates differently. Files from different systems use different formats. Be flexible.
ā If something breaks here
- Ambiguous dates: "01/02/2026" could be Jan 2 or Feb 1. Order your formats by priority.
What this page does
Converts timedelta to human-readable strings.
Where this fits
Raw timedelta output isn't user-friendly.
Code (this page)
from datetime import timedeltadef format_duration(td):
"""Convert timedelta to human-readable string."""
total_seconds = int(td.total_seconds())
if total_seconds < 0:
return "in the past"
days, remainder = divmod(total_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days:
parts.append(f"{days} day{'s' if days != 1 else ''}")
if hours:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
if seconds and not days: # Skip seconds for long durations
parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
return ", ".join(parts) if parts else "0 seconds"
# Test various durations
durations = [
timedelta(seconds=45),
timedelta(minutes=5, seconds=30),
timedelta(hours=2, minutes=15),
timedelta(days=1, hours=5),
timedelta(days=30),
]
for td in durations:
print(f"{td} ā {format_duration(td)}")
Explanation
Run the file:
0:00:45 ā 45 seconds
0:05:30 ā 5 minutes, 30 seconds
2:15:00 ā 2 hours, 15 minutes
1 day, 5:00:00 ā 1 day, 5 hours
30 days, 0:00:00 ā 30 days
Key techniques:
total_seconds() gives total as float
divmod extracts each unit
- Conditional plural handling
Why this matters
"3 days, 2 hours" is readable. "3 days, 2:00:00" is not.
ā If something breaks here
Nothing should break. Adjust formatting to your needs.
What this page does
Builds a countdown to a future event.
Where this fits
Apply datetime skills to a practical tool.
Code (this page)
from datetime import datetime, timedeltadef countdown_to(event_name, event_datetime):
"""Print countdown to an event."""
now = datetime.now()
remaining = event_datetime - now
if remaining.total_seconds() <= 0:
print(f"{event_name} has already happened!")
return
days = remaining.days
hours, remainder = divmod(remaining.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
print(f"Countdown to {event_name}:")
print(f" {days} days, {hours} hours, {minutes} minutes, {seconds} seconds")
# Milestones
if days == 0:
print(" š„ It's TODAY!")
elif days <= 7:
print(" ā” Less than a week!")
elif days <= 30:
print(" š
Less than a month!")
# Countdown to various events
new_year = datetime(2027, 1, 1, 0, 0, 0)
countdown_to("New Year 2027", new_year)
print()
# Countdown to a meeting
meeting = datetime.now() + timedelta(hours=3, minutes=30)
countdown_to("Team Meeting", meeting)
Explanation
Run the file:
Countdown to New Year 2027:
337 days, 9 hours, 29 minutes, 15 seconds
š
Less than a month!Countdown to Team Meeting:
0 days, 3 hours, 29 minutes, 59 seconds
š„ It's TODAY!
Pattern:
- Calculate remaining time as timedelta
- Extract components for display
- Add contextual messages based on duration
Why this matters
Countdowns create anticipation and urgency. Events, deadlines, launches.
ā If something breaks here
Nothing should break. Event in past shows message.
What this page does
Manages events with datetime comparisons.
Where this fits
Real applications manage multiple events.
Code (this page)
from datetime import datetime, timedeltaclass Event:
def __init__(self, name, dt):
self.name = name
self.datetime = dt
def __repr__(self):
return f"{self.name} @ {self.datetime.strftime('%Y-%m-%d %H:%M')}"
class Scheduler:
def __init__(self):
self.events = []
def add(self, name, dt):
self.events.append(Event(name, dt))
self.events.sort(key=lambda e: e.datetime)
def upcoming(self, n=5):
"""Get next n upcoming events."""
now = datetime.now()
future = [e for e in self.events if e.datetime > now]
return future[:n]
def today(self):
"""Get today's events."""
now = datetime.now()
return [e for e in self.events
if e.datetime.date() == now.date()]
# Demo
scheduler = Scheduler()
now = datetime.now()
scheduler.add("Team standup", now + timedelta(hours=1))
scheduler.add("Lunch", now + timedelta(hours=3))
scheduler.add("Code review", now + timedelta(hours=5))
scheduler.add("Tomorrow meeting", now + timedelta(days=1, hours=2))
scheduler.add("Past event", now - timedelta(hours=2))
print("All events:", scheduler.events)
print("\nUpcoming:", scheduler.upcoming(3))
print("\nToday:", scheduler.today())
Explanation
Run the file:
All events: [Past event @ ..., Team standup @ ..., Lunch @ ..., ...]Upcoming: [Team standup @ ..., Lunch @ ..., Code review @ ...]
Today: [Team standup @ ..., Lunch @ ..., Code review @ ...]
Scheduler features:
- Events sorted by datetime
- Filter by upcoming/past
- Filter by date
Why this matters
Calendar apps, reminder systems, booking systems ā all schedule events.
ā If something breaks here
Nothing should break. Adjust event times as needed.
What this page does
Generates repeating events (daily, weekly, monthly).
Where this fits
Real schedules have recurring events.
Code (this page)
from datetime import datetime, timedelta, datedef daily_occurrences(start_date, count):
"""Generate daily occurrences."""
for i in range(count):
yield start_date + timedelta(days=i)
def weekly_occurrences(start_date, count, weekday=None):
"""Generate weekly occurrences on same weekday."""
if weekday is None:
weekday = start_date.weekday()
# Find first occurrence on target weekday
days_ahead = weekday - start_date.weekday()
if days_ahead < 0:
days_ahead += 7
first = start_date + timedelta(days=days_ahead)
for i in range(count):
yield first + timedelta(weeks=i)
def monthly_occurrences(start_date, count):
"""Generate monthly occurrences (same day of month)."""
current = start_date
for _ in range(count):
yield current
# Move to next month
if current.month == 12:
current = current.replace(year=current.year + 1, month=1)
else:
# Handle months with fewer days
next_month = current.month + 1
try:
current = current.replace(month=next_month)
except ValueError:
# Day doesn't exist in next month, use last day
current = current.replace(month=next_month, day=28)
# Demo
start = date(2026, 1, 28)
print("Daily (5 occurrences):")
for d in daily_occurrences(start, 5):
print(f" {d}")
print("\nWeekly on Monday (4 occurrences):")
for d in weekly_occurrences(start, 4, weekday=0):
print(f" {d} ({d.strftime('%A')})")
print("\nMonthly (6 occurrences):")
for d in monthly_occurrences(start, 6):
print(f" {d}")
Explanation
Run the file:
Daily (5 occurrences):
2026-01-28
2026-01-29
2026-01-30
2026-01-31
2026-02-01Weekly on Monday (4 occurrences):
2026-02-02 (Monday)
2026-02-09 (Monday)
2026-02-16 (Monday)
2026-02-23 (Monday)
Monthly (6 occurrences):
2026-01-28
2026-02-28
2026-03-28
2026-04-28
2026-05-28
2026-06-28
Patterns:
- Daily: just add days
- Weekly: calculate to target weekday, then add weeks
- Monthly: replace month, handle edge cases
Why this matters
Recurring meetings, subscriptions, reminders ā all need recurrence patterns.
ā If something breaks here
- Monthly on 31st: some months don't have 31 days
What this page does
Calculates with business days (excluding weekends).
Where this fits
Business deadlines skip weekends.
Code (this page)
from datetime import date, timedeltadef add_business_days(start_date, num_days):
"""Add business days (Mon-Fri) to a date."""
current = start_date
days_added = 0
while days_added < num_days:
current += timedelta(days=1)
if current.weekday() < 5: # Monday-Friday
days_added += 1
return current
def business_days_between(start_date, end_date):
"""Count business days between two dates."""
count = 0
current = start_date
while current < end_date:
current += timedelta(days=1)
if current.weekday() < 5:
count += 1
return count
# Demo
today = date.today()
print(f"Today: {today} ({today.strftime('%A')})")
# Add 5 business days
deadline = add_business_days(today, 5)
print(f"5 business days later: {deadline} ({deadline.strftime('%A')})")
# Add 10 business days
deadline2 = add_business_days(today, 10)
print(f"10 business days later: {deadline2} ({deadline2.strftime('%A')})")
# Count business days in January
jan_start = date(2026, 1, 1)
jan_end = date(2026, 1, 31)
work_days = business_days_between(jan_start, jan_end)
print(f"\nBusiness days in January 2026: {work_days}")
Explanation
Run the file:
Today: 2026-01-28 (Wednesday)
5 business days later: 2026-02-04 (Wednesday)
10 business days later: 2026-02-11 (Wednesday)Business days in January 2026: 22
Key points:
- Skip weekends (weekday >= 5)
- Could extend to skip holidays (with a holiday list)
- Different from calendar days
Why this matters
"Delivery in 5 business days", "Payment due in 30 business days" ā real deadlines.
ā If something breaks here
Nothing should break. Could add holiday handling.
What this page does
Shows advanced timedelta operations.
Where this fits
Timedeltas can be combined and manipulated.
Code (this page)
from datetime import timedelta# Basic arithmetic
hour = timedelta(hours=1)
day = timedelta(days=1)
print(f"1 day + 2 hours: {day + 2*hour}")
print(f"1 day - 6 hours: {day - 6*hour}")
print(f"3 days * 2: {3*day}")
print(f"1 week / 2: {timedelta(weeks=1) / 2}")
# Total seconds
week = timedelta(weeks=1)
print(f"\n1 week = {week.total_seconds()} seconds")
print(f"1 week = {week.total_seconds() / 3600} hours")
print(f"1 week = {week.days} days")
# Comparisons
short = timedelta(hours=1)
long = timedelta(days=1)
print(f"\n1 hour < 1 day: {short < long}")
print(f"1 hour == 60 minutes: {short == timedelta(minutes=60)}")
# Absolute value
negative = timedelta(days=-5)
print(f"\nNegative duration: {negative}")
print(f"Absolute value: {abs(negative)}")
Explanation
Run the file:
1 day + 2 hours: 1 day, 2:00:00
1 day - 6 hours: 18:00:00
3 days * 2: 6 days, 0:00:00
1 week / 2: 3 days, 12:00:001 week = 604800.0 seconds
1 week = 168.0 hours
1 week = 7 days
1 hour < 1 day: True
1 hour == 60 minutes: True
Negative duration: -5 days, 0:00:00
Absolute value: 5 days, 0:00:00
Timedelta supports:
- Addition, subtraction
- Multiplication, division by numbers
- Comparison operators
abs() for absolute value
Why this matters
Complex duration calculations need these operations.
ā If something breaks here
- Division: Only divide by numbers, not other timedeltas
What this page does
Uses the calendar module with datetime.
Where this fits
Calendar provides month/year views that complement datetime.
Code (this page)
import calendar
from datetime import date# Print a month calendar
print("January 2026:")
print(calendar.month(2026, 1))
# Days in a month
days_in_feb = calendar.monthrange(2026, 2)[1]
print(f"Days in Feb 2026: {days_in_feb}")
# Is leap year?
print(f"2024 is leap year: {calendar.isleap(2024)}")
print(f"2026 is leap year: {calendar.isleap(2026)}")
# First and last day of month
today = date.today()
first_weekday, num_days = calendar.monthrange(today.year, today.month)
first_day = today.replace(day=1)
last_day = today.replace(day=num_days)
print(f"\nThis month: {first_day} to {last_day}")
# All Sundays in a month
print("\nSundays in January 2026:")
cal = calendar.Calendar()
for day in cal.itermonthdays2(2026, 1):
date_num, weekday = day
if date_num != 0 and weekday == 6: # 6 = Sunday
print(f" January {date_num}")
Explanation
Run the file:
January 2026:
January 2026
Mo Tu We Th Fr Sa Su
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31Days in Feb 2026: 28
2024 is leap year: True
2026 is leap year: False
This month: 2026-01-01 to 2026-01-31
Sundays in January 2026:
January 4
January 11
January 18
January 25
Calendar module helps with:
- Month displays
- Days in month
- Leap year checking
- Iterating specific weekdays
Why this matters
Calendar views, scheduling, and month-based logic benefit from the calendar module.
ā If something breaks here
Nothing should break. Calendar is built-in.
What this page does
Validates date strings and values.
Where this fits
User input needs validation before use.
Code (this page)
from datetime import datetime, datedef is_valid_date(year, month, day):
"""Check if a date is valid."""
try:
date(year, month, day)
return True
except ValueError:
return False
def parse_date_safe(date_string, format="%Y-%m-%d"):
"""Parse date, return None if invalid."""
try:
return datetime.strptime(date_string, format)
except ValueError:
return None
def validate_date_range(start, end):
"""Ensure start is before end."""
if start > end:
raise ValueError("Start date must be before end date")
return True
# Test validation
print("Date validation:")
print(f" 2026-02-28: {is_valid_date(2026, 2, 28)}") # Valid
print(f" 2026-02-29: {is_valid_date(2026, 2, 29)}") # Invalid (not leap year)
print(f" 2024-02-29: {is_valid_date(2024, 2, 29)}") # Valid (leap year)
print(f" 2026-13-01: {is_valid_date(2026, 13, 1)}") # Invalid month
print("\nParsing:")
print(f" '2026-01-28': {parse_date_safe('2026-01-28')}")
print(f" 'invalid': {parse_date_safe('invalid')}")
print(f" '28/01/2026': {parse_date_safe('28/01/2026')}") # Wrong format
print("\nRange validation:")
try:
validate_date_range(date(2026, 1, 1), date(2026, 12, 31))
print(" Valid range ā")
except ValueError as e:
print(f" Error: {e}")
Explanation
Run the file:
Date validation:
2026-02-28: True
2026-02-29: False
2024-02-29: True
2026-13-01: FalseParsing:
'2026-01-28': 2026-01-28 00:00:00
'invalid': None
'28/01/2026': None
Range validation:
Valid range ā
Validation strategies:
- Try to create the date (catch ValueError)
- Return None on parse failure
- Check logical constraints (start < end)
Why this matters
Invalid dates crash apps. Always validate user input.
ā If something breaks here
Nothing should break. These functions handle errors.
What this page does
Quick reference for all datetime classes and methods.
Where this fits
Lookup table after completing the guide.
Explanation
### Classes
| Class | Purpose | Example |
date | Date only | date(2026, 1, 28) |
time | Time only | time(14, 30) |
datetime | Date + time | datetime(2026, 1, 28, 14, 30) |
timedelta | Duration | timedelta(days=1) |
timezone | Timezone offset | timezone.utc |
### date Methods
| Method | Returns |
date.today() | Current date |
.year, .month, .day | Components |
.weekday() | 0-6 (Mon-Sun) |
.isoformat() | "YYYY-MM-DD" |
.strftime(fmt) | Formatted string |
### datetime Methods
| Method | Returns |
datetime.now() | Current datetime |
datetime.utcnow() | Current UTC |
.date() | Date part |
.time() | Time part |
.timestamp() | Unix timestamp |
.strftime(fmt) | Formatted string |
.replace(...) | New with changes |
### Parsing
| Method | Input | Output |
strptime(s, fmt) | String | datetime |
fromisoformat(s) | ISO string | datetime |
fromtimestamp(ts) | Unix time | datetime |
### Common Format Codes
| Code | Meaning |
%Y | Year 4-digit |
%m | Month 01-12 |
%d | Day 01-31 |
%H | Hour 00-23 |
%M | Minute 00-59 |
%S | Second 00-59 |
%A | Weekday name |
%B | Month name |
Why this matters
ā If something breaks here
Nothing to break. Reference only.
What this page does
Reusable code patterns for common tasks.
Where this fits
Copy-paste solutions for frequent needs.
Code (this page)
from datetime import datetime, date, timedelta, timezone# Pattern 1: Today at midnight
today_midnight = datetime.combine(date.today(), datetime.min.time())
# Pattern 2: End of today
today_end = datetime.combine(date.today(), datetime.max.time())
# Pattern 3: First day of month
first_of_month = date.today().replace(day=1)
# Pattern 4: Days until date
target = date(2026, 12, 25)
days_until = (target - date.today()).days
# Pattern 5: Is date in past?
is_past = date(2020, 1, 1) < date.today()
# Pattern 6: Format for display
display = datetime.now().strftime("%B %d, %Y at %I:%M %p")
# Pattern 7: Parse ISO date
dt = datetime.fromisoformat("2026-01-28T14:30:00")
# Pattern 8: Current UTC timestamp
utc_now = datetime.now(timezone.utc)
# Pattern 9: Add months (approximate)
def add_months(dt, months):
month = dt.month + months
year = dt.year + (month - 1) // 12
month = (month - 1) % 12 + 1
day = min(dt.day, [31,28,31,30,31,30,31,31,30,31,30,31][month-1])
return dt.replace(year=year, month=month, day=day)
# Pattern 10: Time since event
event_time = datetime(2026, 1, 1, 0, 0, 0)
time_since = datetime.now() - event_time
print("Patterns demonstrated successfully!")
print(f"Today midnight: {today_midnight}")
print(f"Days until Christmas: {days_until}")
print(f"Display format: {display}")
Explanation
These patterns solve common problems:
- Midnight and end-of-day boundaries
- Month boundaries
- Date comparisons
- Formatting for users
- Safe parsing
- Adding months (tricky!)
Why this matters
ā If something breaks here
Nothing should break. These are proven patterns.
What this page does
Confirms you have mastered all skills in this guide.
Where this fits
This is the end. Verify everything works together.
Explanation
Complete this final test in your datetime_basics.py:
from datetime import datetime, date, timedelta, timezoneprint("=== DateTime Module Final Test ===\n")
# 1. Current date and time
now = datetime.now()
print(f"1. Now: {now.strftime('%Y-%m-%d %H:%M:%S')}")
# 2. Create specific date
birthday = date(1990, 6, 15)
age = date.today().year - birthday.year
print(f"2. Born {birthday}, age ~{age}")
# 3. Date arithmetic
deadline = date.today() + timedelta(days=30)
print(f"3. 30 days from now: {deadline}")
# 4. Parse and format
date_str = "December 25, 2026"
christmas = datetime.strptime(date_str, "%B %d, %Y")
print(f"4. Parsed: '{date_str}' ā {christmas.date()}")
# 5. Time until event
remaining = christmas - datetime.now()
print(f"5. Days until Christmas: {remaining.days}")
# 6. Timezone aware
utc_now = datetime.now(timezone.utc)
print(f"6. UTC time: {utc_now.strftime('%H:%M:%S %Z')}")
# 7. Compare dates
if date.today() < christmas.date():
print("7. Christmas is coming!")
else:
print("7. Christmas has passed")
# 8. Weekday
print(f"8. Christmas 2026 is on a {christmas.strftime('%A')}")
print("\nā DateTime Module Fundamentals Complete!")
Run it. If you understand every line, you've completed the guide.
Why this matters
You now master dates and times in Python: creating, formatting, parsing, calculating, and comparing.