Working with Dates and Times
The datetime module in depth — creating and formatting dates, naive vs timezone-aware datetimes, zoneinfo, and a real scheduling example.
The Module Every Application Needs, and Everyone Gets Wrong Once
Dates and times feel simple until they are not. Nearly every production application touches them somewhere — a timestamp on a database row, a "remind me in 3 days" feature, a report that needs to say "this happened at 9am Eastern" correctly regardless of where the server physically runs. And nearly every engineer, at some point, ships a date/time bug that only shows up for users in a specific timezone, or only around a Daylight Saving Time transition, or only when a server's clock is set to UTC instead of local time. This module exists to get you past that first bug before it happens in production, not after.
Python's standard library ships a genuinely solid toolkit for this: the datetime module for representing points in time and durations, and — since Python 3.9 — the zoneinfo module for correct, IANA-database-backed timezone handling. This module builds both up from first principles, spends real time on the naive-vs-aware distinction (because it is the single biggest source of real bugs), and ends with a worked scheduling example across US timezones.
from datetime import date, time, datetime, timedelta
today = date.today() # just a calendar date, no time component
now = datetime.now() # date AND time, but "naive" — see Part 04
one_week_later = today + timedelta(days=7) # date arithmetic, covered in Part 06
print(today) # 2026-08-13
print(now) # 2026-08-13 09:41:02.118273
print(one_week_later) # 2026-08-20date, time, datetime, and timedelta
The datetime module provides four classes that cover almost everything you will need. It is worth being precise about what each one represents, since mixing them up (for example, trying to add a time object directly to a date object) is a common early mistake.
from datetime import date
launch_day = date(2026, 8, 13) # year, month, day
print(launch_day) # 2026-08-13
print(launch_day.year, launch_day.month, launch_day.day) # 2026 8 13
print(launch_day.weekday()) # 3 — Monday is 0, so 3 is Thursdayfrom datetime import time
standup = time(9, 30) # hour, minute (seconds and microseconds default to 0)
print(standup) # 09:30:00
print(standup.hour, standup.minute) # 9 30from datetime import datetime
meeting = datetime(2026, 8, 13, 9, 30, 0) # year, month, day, hour, minute, second
print(meeting) # 2026-08-13 09:30:00
# datetime.now() and datetime.today() both give the CURRENT date and time
print(datetime.now())from datetime import timedelta
one_week = timedelta(days=7)
ninety_minutes = timedelta(hours=1, minutes=30)
print(one_week) # 7 days, 0:00:00
print(ninety_minutes) # 1:30:00The distinction to hold onto: date, time, and datetime represent a specific point — a place on the calendar or clock. timedelta represents a span — an amount of elapsed time, with no fixed starting point of its own. You will combine them constantly: a point plus a span gives you another point, covered fully in Part 06.
strftime and strptime — Genuinely Fiddly, Worth Doing Properly
Turning a datetime object into a specific text format, and turning text back into a datetime object, are two of the most common date/time operations in real code — and also where most people reach for the documentation every single time, because the format codes are dense and easy to mix up. Two methods to know: strftime ("string format time" — object to string) and strptime ("string parse time" — string to object).
from datetime import datetime
now = datetime(2026, 8, 13, 14, 5, 9)
print(now.strftime("%Y-%m-%d")) # "2026-08-13"
print(now.strftime("%m/%d/%Y")) # "08/13/2026"
print(now.strftime("%B %d, %Y")) # "August 13, 2026"
print(now.strftime("%A, %B %d")) # "Thursday, August 13"
print(now.strftime("%I:%M %p")) # "02:05 PM"
print(now.strftime("%Y-%m-%d %H:%M:%S")) # "2026-08-13 14:05:09"%Y 4-digit year 2026
%y 2-digit year 26
%m month, zero-padded 08
%d day of month 13
%B full month name August
%b abbreviated month Aug
%A full weekday name Thursday
%a abbreviated weekday Thu
%H hour, 24-hour clock 14
%I hour, 12-hour clock 02
%M minute, zero-padded 05
%S second, zero-padded 09
%p AM or PM PMfrom datetime import datetime
# The format string must match the input text's shape EXACTLY, code for code
parsed = datetime.strptime("2026-08-13 14:05:09", "%Y-%m-%d %H:%M:%S")
print(parsed) # datetime.datetime(2026, 8, 13, 14, 5, 9)
print(type(parsed)) # <class 'datetime.datetime'>
# A common real case — parsing a date users typed in a form
form_date = datetime.strptime("08/13/2026", "%m/%d/%Y")
print(form_date.date()) # 2026-08-13datetime.strptime("2026-08-13", "%m/%d/%Y") raises ValueError: time data '2026-08-13' does not match format '%m/%d/%Y' — the separators (dashes vs slashes) and field order both have to line up precisely with what is actually in the string, not what you assume it looks like. When parsing data from an external source, always confirm the exact format first rather than guessing.A useful shortcut for the common ISO 8601 format specifically (YYYY-MM-DDTHH:MM:SS, the standard format used by most APIs and databases): datetime.fromisoformat() and datetime.isoformat() handle it without needing to spell out a format string at all.
now = datetime(2026, 8, 13, 14, 5, 9)
print(now.isoformat()) # "2026-08-13T14:05:09"
parsed = datetime.fromisoformat("2026-08-13T14:05:09")
print(parsed) # datetime.datetime(2026, 8, 13, 14, 5, 9)The Distinction Behind the Majority of Real Production Date Bugs
A naive datetime has no timezone information attached — it is just a collection of numbers (year, month, day, hour, minute, second) with no notion of where on Earth or relative to what reference point those numbers apply. A timezone-aware datetime carries that information explicitly. datetime.now(), used casually in the first example of this module, returns a naive datetime — which is exactly why it deserves a section of its own here.
from datetime import datetime
naive = datetime.now()
print(naive) # 2026-08-13 09:41:02.118273
print(naive.tzinfo) # None — no timezone attached at all
# This "9:41" could be Eastern time, Pacific time, UTC, or anything else —
# the datetime object itself contains no information that says which.Here is why this becomes a real bug, not just a technicality. Imagine a scheduling system storing "reminder due at 2026-08-13 09:00:00" as a naive datetime, on a server configured to run in UTC. A user in Denver (Mountain time, UTC−6 during Daylight Saving Time) expects their 9am reminder to fire at 9am their local time — but the naive datetime has no way to express that distinction. Depending on how the comparison is written elsewhere in the code, the reminder can silently fire six hours early or late, and nothing about the code itself signals that anything is wrong — it runs without error, it just produces the wrong answer.
from datetime import datetime, timezone
aware_utc = datetime.now(timezone.utc)
print(aware_utc) # 2026-08-13 15:41:02.118273+00:00
print(aware_utc.tzinfo) # UTC — no longer Nonenaive_dt - aware_dt raises TypeError: can't subtract offset-naive and offset-aware datetimes. Python refuses to guess what timezone the naive one is supposed to represent — which is a genuine safety feature, not an inconvenience, since silently guessing wrong is exactly the class of bug described above.The practical rule most production codebases adopt: store and compute with timezone-aware datetimes internally, in UTC, and only convert to a specific local timezone at the moment you display something to a user or accept input from one. Part 05 covers exactly how to do that conversion correctly.
Working With Actual Timezones — The Modern Standard-Library Way
Since Python 3.9, the standard library includes zoneinfo, which gives you access to the full IANA time zone database — the same authoritative source used across most modern software. Before 3.9, this required the third-party pytz package, which you will still see in a lot of legacy code and older tutorials; zoneinfo is the modern replacement and the one to reach for in new code.
from datetime import datetime
from zoneinfo import ZoneInfo
denver_time = datetime(2026, 8, 13, 9, 0, 0, tzinfo=ZoneInfo("America/Denver"))
print(denver_time) # 2026-08-13 09:00:00-06:00
print(denver_time.tzinfo) # America/DenverAmerica/New_York Eastern (handles EST/EDT switching automatically)
America/Chicago Central
America/Denver Mountain
America/Los_Angeles Pacific
America/Anchorage Alaska
Pacific/Honolulu Hawaii (no Daylight Saving Time observed)Notice these are named after cities and regions, not fixed offsets like "UTC-5" — this is deliberate and important. A fixed offset cannot correctly represent Daylight Saving Time transitions, but America/New_York automatically knows to be UTC−5 in the winter and UTC−4 in the summer, because the IANA database encodes the actual historical and current rules for that region, including exactly when the transitions happen each year.
from datetime import datetime
from zoneinfo import ZoneInfo
# A meeting scheduled in Eastern time
meeting_eastern = datetime(2026, 8, 13, 13, 0, tzinfo=ZoneInfo("America/New_York"))
# What time is that for a Denver-based attendee?
meeting_denver = meeting_eastern.astimezone(ZoneInfo("America/Denver"))
print(meeting_denver) # 2026-08-13 11:00:00-06:00 — 11am Mountain
# And in UTC, for storing in a database?
meeting_utc = meeting_eastern.astimezone(ZoneInfo("UTC"))
print(meeting_utc) # 2026-08-13 17:00:00+00:00pytz in an existing codebase, the biggest gotcha is that pytz timezones generally should not be passed directly to a datetime constructor's tzinfo argument — they require a separate .localize() call to attach correctly, an easy trap for anyone used to zoneinfo's simpler API. For new code, zoneinfo avoids this entirely and needs no external dependency, since it ships in the standard library from Python 3.9 onward.timedelta — Adding, Subtracting, and Measuring Elapsed Time
You met timedelta briefly in Part 02. It supports the arithmetic operators directly, which is what makes date math in Python genuinely pleasant rather than a manual calendar-counting exercise.
from datetime import date, datetime, timedelta
today = date(2026, 8, 13)
print(today + timedelta(days=10)) # 2026-08-23
print(today - timedelta(weeks=2)) # 2026-07-30
deadline = datetime(2026, 8, 13, 17, 0)
print(deadline + timedelta(hours=6, minutes=30)) # 2026-08-13 23:30:00start = datetime(2026, 8, 13, 9, 0)
end = datetime(2026, 8, 13, 17, 30)
elapsed = end - start
print(elapsed) # 8:30:00
print(elapsed.total_seconds()) # 30600.0 — useful for logging or comparisons
print(type(elapsed)) # <class 'datetime.timedelta'>Calculating business days — a genuinely common real task
Adding a fixed number of calendar days is straightforward, but "5 business days from now" is a common real requirement that the standard library does not provide directly — it is worth seeing how naturally it builds on what you already have.
from datetime import date, timedelta
def add_business_days(start_date, business_days):
current = start_date
added = 0
while added < business_days:
current += timedelta(days=1)
if current.weekday() < 5: # Monday=0 ... Friday=4; Saturday=5, Sunday=6
added += 1
return current
order_date = date(2026, 8, 13) # a Thursday
ship_by = add_business_days(order_date, 5)
print(ship_by) # 2026-08-20 — skips the weekend correctlyConverting Between Unix Timestamps and datetime Objects
A Unix timestamp (or "epoch time") is a single number: the count of seconds elapsed since January 1, 1970, 00:00:00 UTC. It shows up constantly in APIs, logs, and databases, precisely because a single number is unambiguous and easy to store, sort, and compare — no format-string parsing required.
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
ts = now.timestamp()
print(ts) # 1786721234.118273 (an example value)
# Converting a timestamp back to a datetime
restored = datetime.fromtimestamp(ts, tz=timezone.utc)
print(restored) # matches "now" abovetz=timezone.utc (or another explicit zone) unless you specifically intend local- system-timezone behaviour.Scheduling Logic for a US-Wide Service
Here is a realistic worked example pulling together naive-vs-aware, zoneinfo, and formatting: a notification scheduler for a fictional nationwide service that needs to send a "your appointment is tomorrow at 9am" reminder correctly, regardless of which US timezone the recipient is in.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
def schedule_reminder(appointment_local_time, user_timezone, hours_before=24):
"""
appointment_local_time: a naive datetime representing the appointment,
in the USER's own local time
user_timezone: an IANA zone string, e.g. "America/Denver"
"""
# Step 1 — attach the user's actual timezone to their local appointment time
appointment_aware = appointment_local_time.replace(tzinfo=ZoneInfo(user_timezone))
# Step 2 — convert to UTC immediately, for internal storage and comparison
appointment_utc = appointment_aware.astimezone(ZoneInfo("UTC"))
# Step 3 — compute when the reminder should fire, still in UTC
reminder_utc = appointment_utc - timedelta(hours=hours_before)
return {
"appointment_utc": appointment_utc,
"reminder_utc": reminder_utc,
"reminder_local_display": reminder_utc.astimezone(ZoneInfo(user_timezone)),
}
# A user in Denver with a 9am appointment on 2026-08-14
result = schedule_reminder(
datetime(2026, 8, 14, 9, 0),
"America/Denver",
)
print(result["appointment_utc"]) # 2026-08-14 15:00:00+00:00
print(result["reminder_utc"]) # 2026-08-13 15:00:00+00:00
print(result["reminder_local_display"]) # 2026-08-13 09:00:00-06:00 — 9am the day before, Denver timeNotice the pattern the function follows: attach the correct local timezone as early as possible, immediately convert to UTC for any internal storage or arithmetic, and only convert back to a local timezone at the very last step, for display. This "convert to UTC at the boundary, work in UTC internally" pattern is exactly the practical rule from the end of Part 04, applied to a real feature.
An Austin Scheduling Startup's Daylight Saving Time Bug
A scheduling startup lets small medical and dental offices manage patient appointments. An early version of their reminder system stores each appointment's local time as a naive datetime, plus a separate UTC offset column captured at the moment the appointment was booked — a design decision made under deadline pressure, reasoning that "we'll just apply the saved offset later."
What breaks, the weekend Daylight Saving Time ends
An appointment booked in July for a date in November was saved with July's UTC offset — Central Daylight Time, UTC−5. By the time November arrives, Central Standard Time (UTC−6) is in effect, but the stored offset never updated, because a fixed offset captured once is not the same thing as a timezone. Every reminder for an appointment spanning the Daylight Saving Time transition fires exactly one hour off — patients start calling asking why their reminder said 9am but the front desk says their appointment is at 10am.
The fix
The team replaces the fixed-offset column entirely with an IANA zone name — exactly the "America/Chicago"-style string from Part 05 — stored alongside a UTC timestamp. Because zoneinfo encodes the actual Daylight Saving Time transition rules for that region, converting America/Chicago at any future date automatically applies the correct offset for that specific date, without the application needing to track transition dates itself. This is precisely why Part 05 emphasized using named zones like America/Denver rather than a fixed numeric offset — a fixed offset is only ever correct for the exact moment it was captured.
The broader lesson, echoed across most real date/time incidents: a UTC offset is a snapshot, valid for one instant; a timezone name is a rule, valid indefinitely into the future. Storing the snapshot when you needed the rule is a subtle mistake that will not surface until the next Daylight Saving Time transition proves it wrong.
Four Misconceptions About Dates and Times
5 Interview Questions — With Complete Answers
Date/Time Mistakes Beginners Make Constantly
Errors You Will Hit With Dates and Times — And Exactly Why
🎯 Key Takeaways
- ✓date, time, and datetime represent POINTS on the calendar/clock. timedelta represents a SPAN of elapsed time, with no fixed starting point of its own.
- ✓strftime formats a datetime object into a string; strptime parses a string into a datetime object using a format string that must match the input exactly.
- ✓A naive datetime has no timezone attached; a timezone-aware datetime does. datetime.now() returns naive by default — a common source of real production bugs.
- ✓Naive and aware datetimes cannot be compared or subtracted directly — Python raises a TypeError rather than guessing.
- ✓zoneinfo (standard library since Python 3.9) is the modern way to work with real IANA timezones like "America/Denver" — prefer it over the older third-party pytz for new code.
- ✓A timezone name is a RULE that correctly accounts for Daylight Saving Time transitions; a captured UTC offset is only valid for the instant it was recorded. Store the name, not just the offset.
- ✓The standard production pattern: store and compute with timezone-aware datetimes in UTC internally, and convert to a local timezone only at the display/input boundary.
- ✓Unix timestamps (seconds since 1970-01-01 UTC) are a common, unambiguous way to represent a point in time in APIs and databases — convert with .timestamp() and datetime.fromtimestamp(ts, tz=...).
- ✓timedelta supports direct arithmetic with date and datetime objects, and subtracting two datetimes yields a timedelta — this is how you build things like business-day calculations.
What comes next
Module 34 covers multithreading and multiprocessing — the Global Interpreter Lock, when threads genuinely help despite it, and when you need real parallelism with separate processes instead.
Module 34 → Multithreading and Multiprocessing BasicsDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.