Every Python project I’ve ever written that touches real-world data eventually needs to deal with dates and times, and the datetime module is where that always starts. I want to lay out the fundamentals here in a way I wish someone had laid out for me early on — not just “here’s the syntax,” but why the module is structured the way it is, and how the pieces fit together.
The Core Classes
The datetime module gives you four main classes I use constantly, plus one supporting type:
date— a calendar date (year, month, day), with no time-of-day componenttime— a time of day, with no date componentdatetime— a combination of bothtimedelta— a duration, used for arithmetictzinfo(and its concrete implementationstimezone/ZoneInfo) — timezone information
from datetime import date, time, datetime, timedelta
d = date(2026, 7, 30)
t = time(14, 30, 0)
dt = datetime(2026, 7, 30, 14, 30, 0)
print(d)
print(t)
print(dt)
Output:
2026-07-30
14:30:00
2026-07-30 14:30:00
Creating a Datetime from the Current Moment
The most common entry point I use is datetime.now(), which gives the current local date and time:
now = datetime.now()
print(now)
For the current UTC time, I use:
utc_now = datetime.now(timezone.utc)
print(utc_now)
I always prefer the second form for anything that gets stored or compared across systems, since it’s explicit and unambiguous about which timezone it represents — a habit that saves a lot of debugging later.
Accessing Components
Every datetime object exposes its components as simple attributes:
dt = datetime(2026, 7, 30, 14, 30, 45, 123456)
print(dt.year) # 2026
print(dt.month) # 7
print(dt.day) # 30
print(dt.hour) # 14
print(dt.minute) # 30
print(dt.second) # 45
print(dt.microsecond) # 123456
print(dt.weekday()) # 3 (Monday=0 ... Sunday=6)
print(dt.isoweekday()) # 4 (Monday=1 ... Sunday=7)
I use .weekday() far more than I expected to — it’s the backbone of almost any “is this a business day” or “which day of the week” logic.
Formatting Datetimes as Strings: strftime
To turn a datetime object into a specific string format, strftime (string format time) is the tool:
dt = datetime(2026, 7, 30, 14, 30, 0)
print(dt.strftime("%Y-%m-%d")) # 2026-07-30
print(dt.strftime("%B %d, %Y")) # July 30, 2026
print(dt.strftime("%A, %I:%M %p")) # Thursday, 02:30 PM
print(dt.strftime("%Y-%m-%dT%H:%M:%S")) # 2026-07-30T14:30:00
The directive codes (%Y, %m, %d, and so on) follow the C standard library’s strftime conventions, which is why they’re consistent across many programming languages, not just Python.
Parsing Strings into Datetimes: strptime
Going the other direction — turning a string into a datetime object — uses strptime (string parse time), and requires you to specify the exact format the string is in:
raw = "2026-07-30 14:30:00"
dt = datetime.strptime(raw, "%Y-%m-%d %H:%M:%S")
print(dt)
print(type(dt))
Output:
2026-07-30 14:30:00
<class 'datetime.datetime'>
If the format string doesn’t match the input exactly, Python raises a ValueError with a clear message about where the mismatch occurred — I’ve come to appreciate how specific these error messages are when debugging malformed input.
The Modern Shortcut: ISO Format Parsing
For the extremely common case of ISO 8601-formatted strings (YYYY-MM-DDTHH:MM:SS), you don’t need strptime at all — datetime.fromisoformat() handles it directly, and as of Python 3.11, it’s become significantly more flexible, supporting the Z suffix and various offset formats:
dt = datetime.fromisoformat("2026-07-30T14:30:00+05:00")
print(dt)
print(dt.tzinfo)
Output:
2026-07-30 14:30:00+05:00
UTC+05:00
This is my default choice whenever I’m parsing dates from JSON APIs, since ISO 8601 is by far the most common serialization format for dates in modern web services.
Immutability
datetime, date, and time objects are all immutable — once created, you can’t change their internal state. Any “modification” actually creates a brand-new object. This is why .replace() exists — it returns a new object with specified fields changed, rather than mutating in place:
dt = datetime(2026, 7, 30, 14, 30, 0)
new_dt = dt.replace(year=2027, hour=9)
print(dt) # unchanged: 2026-07-30 14:30:00
print(new_dt) # 2027-07-30 09:30:00
This immutability is actually a feature, not a limitation — it makes datetime objects safe to pass around and share across your codebase without worrying about one part of your program unexpectedly mutating a value that another part depends on. It’s also what makes datetime objects hashable, so you can use them as dictionary keys or put them in sets.
Comparing Datetimes
Because datetime objects support the standard comparison operators directly, sorting and comparison logic reads naturally:
d1 = datetime(2026, 1, 1)
d2 = datetime(2026, 6, 15)
print(d1 < d2) # True
print(max(d1, d2)) # 2026-06-15 00:00:00
events = [datetime(2026, 5, 1), datetime(2026, 1, 1), datetime(2026, 12, 1)]
print(sorted(events))
The only rule to remember: you can’t compare a naive datetime to an aware one (it raises TypeError), for the same reason you can’t subtract them — Python won’t guess whether the naive value is meant to be in the same zone.
Internal Working: The Proleptic Gregorian Calendar
Under the hood, Python’s date class represents dates using the proleptic Gregorian calendar — meaning the Gregorian calendar rules are extended backward indefinitely, even to dates before the calendar was historically adopted. Internally, a date is stored as a single ordinal number of days since 0001-01-01, which is why date.toordinal() and date.fromordinal() exist as efficient conversion points:
d = date(2026, 7, 30)
print(d.toordinal())
print(date.fromordinal(d.toordinal()))
This ordinal representation is also why date arithmetic is so fast — comparing or subtracting two dates is really just comparing or subtracting two integers under the hood.
Basic Arithmetic with timedelta
Beyond just subtracting two datetimes to get a duration, I add and subtract timedelta objects directly to shift a datetime forward or backward — this is the fundamental building block for “N days from now,” reminders, expiry dates, and scheduling logic:
from datetime import datetime, timedelta
now = datetime(2026, 7, 30, 9, 0, 0)
one_week_later = now + timedelta(weeks=1)
three_days_earlier = now - timedelta(days=3)
print(one_week_later)
print(three_days_earlier)
Output:
2026-08-06 09:00:00
2026-07-27 09:00:00
timedelta accepts weeks, days, hours, minutes, seconds, milliseconds, and microseconds as keyword arguments, and you can combine any number of them in a single call — they’re all normalized into the same internal representation regardless of how you specify them.
The min, max, and resolution Class Attributes
Each of the core classes exposes useful boundary values I’ve occasionally needed — for instance, when initializing a “latest seen” variable that should be overwritten by any real date:
print(datetime.min) # 0001-01-01 00:00:00
print(datetime.max) # 9999-12-31 23:59:59.999999
print(datetime.resolution) # 0:00:00.000001 (microsecond precision)
earliest_seen = datetime.max
for candidate in [datetime(2026, 1, 1), datetime(2025, 6, 1)]:
if candidate < earliest_seen:
earliest_seen = candidate
print(earliest_seen)
datetime.resolution confirms the smallest time unit the class can represent — one microsecond — which is worth knowing if you’re ever tempted to work with nanosecond-level precision; the standard datetime module simply doesn’t support finer granularity than that (numpy.datetime64 or pandas.Timestamp are the tools to reach for if nanosecond precision genuinely matters).
Common Mistakes
- Confusing
date,time, anddatetimeand trying to mix them directly. You can’t subtract atimefrom adatetime, for instance — combine them properly first withdatetime.combine(). - Forgetting that
datetimeobjects are immutable, and expecting.replace()or similar methods to mutate in place rather than return a new object. - Mismatched
strftime/strptimeformat strings, especially around 12-hour vs. 24-hour time (%Ivs%H) and needing%pfor AM/PM. - Using
datetime.now()whendatetime.now(timezone.utc)was actually needed for consistency across systems.
Building Dates from ISO Calendar Values
Occasionally I need to construct a date not from a year/month/day, but from an ISO week number — common in scheduling systems that organize by week rather than by month. Python supports this directly through date.fromisocalendar():
d = date.fromisocalendar(2026, 31, 4) # ISO year, ISO week, ISO weekday
print(d)
print(d.isocalendar())
Output:
2026-07-30
datetime.IsoCalendarYear(year=2026, week=31, weekday=4)
This pairs naturally with .isocalendar(), which does the reverse conversion — going from a date object to its ISO year, week number, and weekday. I’ve used this specifically for generating “week 31 report” style filenames and labels in scheduling and reporting tools where the business operates on a weekly cadence rather than a monthly one.
Debugging Tips
When strptime raises a ValueError, read the error message carefully — it typically tells you exactly which part of the string didn’t match the format, which is usually enough to spot a stray space, wrong separator, or missing zero-padding.
FAQs
Q: What’s the difference between datetime.now() and datetime.today()? They’re nearly identical for practical purposes; today() doesn’t accept a timezone argument the way now() does, so now() is generally the more flexible and recommended choice.
Q: How do I get just today’s date without the time? date.today() for a date object, or datetime.now().date() if you already have a datetime.
Q: Can I create a datetime from separate date and time objects? Yes — datetime.combine(my_date, my_time).
Summary
The datetime module’s core classes — date, time, datetime, and timedelta — form the foundation for essentially all date and time work in Python. Understanding that these objects are immutable, that naive and aware datetimes don’t mix, and knowing when to reach for strftime/strptime versus fromisoformat() covers the vast majority of real-world usage.
References
- Python official docs:
datetime— Basic date and time types - Python official docs:
strftime()andstrptime()Format Codes - PEP 8 – Style Guide for Python Code