🐍 Python

Python datetime Module — Complete Guide

A complete, professional guide to Python's datetime module — date, time, datetime, and timedelta classes, strftime/strptime formatting, timezone-aware vs naive datetimes, and real date-arithmetic patterns.

📅

Last Updated

March 2026

⏱️

Read Time

28 min

🎯

Level

Intermediate

What is the datetime Module in Python?

The datetime module is Python's built-in standard library toolkit for working with dates, times, and the intervals between them. It provides a small set of specialized classes — date, time, datetime, timedelta, and tzinfo — that together cover nearly everything a program needs to represent, compare, format, parse, and calculate with calendar dates and clock times.

Working with dates correctly is deceptively hard: months have different numbers of days, years are sometimes leap years, time zones shift by non-whole-hour amounts in some regions, daylight saving time creates hours that repeat or skip entirely, and string date formats vary wildly by country and convention. The datetime module exists precisely to handle this complexity correctly and consistently, so application code doesn't have to reinvent calendar arithmetic — and getting even a small operation wrong (like naively adding 86400 seconds to represent "one day later") can silently produce incorrect results across a daylight-saving boundary.

Every value produced by the datetime module is immutable — once a date or datetime object is created, it cannot be changed in place. Operations that appear to "modify" a date, like adding a number of days, always return a brand-new object rather than altering the original, which makes datetime objects safe to pass around and reuse without fear of unexpected side effects.

The Five Core Classes of the datetime Module

The datetime module is built around five distinct classes, each representing a different kind of temporal value. Understanding which one to reach for is the first step to using the module correctly.

ClassRepresentsExampleTypical Use
dateA calendar date, no time componentdate(2026, 3, 16)Birthdays, deadlines, calendar-only data
timeA clock time, no date componenttime(14, 30, 0)Store opening hours, daily schedules
datetimeA combined date AND timedatetime(2026, 3, 16, 14, 30)Timestamps, log entries, event scheduling
timedeltaA duration — the difference between two points in timetimedelta(days=7)Date arithmetic, deadlines, expiry calculations
tzinfo / timezoneTime zone offset and rules informationtimezone.utcMaking a datetime timezone-aware
🐍 Pythoncore_classes_overview.py
from datetime import date, time, datetime, timedelta, timezone

d = date(2026, 3, 16)
t = time(14, 30, 0)
dt = datetime(2026, 3, 16, 14, 30, 0)
delta = timedelta(days=7, hours=3)

print(d)       # 2026-03-16
print(t)       # 14:30:00
print(dt)      # 2026-03-16 14:30:00
print(delta)   # 7 days, 3:00:00

# datetime combines date and time — you can extract either half:
print(dt.date())   # 2026-03-16
print(dt.time())   # 14:30:00

Creating Date and Time Objects

There are several ways to construct date and datetime objects, depending on whether you want the current moment, a specific known date, or a date derived from a Unix timestamp.

Getting the Current Date and Time

🐍 Pythoncurrent_date_time.py
from datetime import date, datetime

today = date.today()
print(today)                # e.g. 2026-07-24

now = datetime.now()
print(now)                  # e.g. 2026-07-24 15:42:10.123456  (local time, naive)

utc_now = datetime.now(datetime_timezone_utc := __import__('datetime').timezone.utc)
print(utc_now)              # e.g. 2026-07-24 10:12:10.123456+00:00  (timezone-aware)

Constructing a Specific Date

🐍 Pythonspecific_date.py
from datetime import date, datetime

independence_day = date(1947, 8, 15)
print(independence_day)          # 1947-08-15
print(independence_day.year)     # 1947
print(independence_day.month)    # 8
print(independence_day.day)      # 15
print(independence_day.weekday())    # 4  (Monday=0 ... Sunday=6)
print(independence_day.strftime('%A'))  # Friday

Creating a Date from a Unix Timestamp

🐍 Pythonfrom_timestamp.py
from datetime import datetime

unix_timestamp = 1750000000
dt = datetime.fromtimestamp(unix_timestamp)
print(dt)   # local time equivalent of that Unix timestamp

# Converting a datetime back to a Unix timestamp:
print(dt.timestamp())   # 1750000000.0

Formatting and Parsing Dates — strftime and strptime

Converting between datetime objects and human-readable strings is one of the most common tasks in real applications. Python uses two complementary methods for this: strftime() ("string format time") converts a datetime object into a formatted string, and strptime() ("string parse time") converts a string into a datetime object, given a matching format pattern.

🐍 Pythonstrftime_strptime.py
from datetime import datetime

dt = datetime(2026, 3, 16, 14, 30, 0)

# Formatting: datetime object -> string
print(dt.strftime('%Y-%m-%d'))            # 2026-03-16
print(dt.strftime('%d/%m/%Y'))            # 16/03/2026
print(dt.strftime('%A, %B %d, %Y'))       # Monday, March 16, 2026
print(dt.strftime('%I:%M %p'))            # 02:30 PM

# Parsing: string -> datetime object
parsed = datetime.strptime('16-03-2026', '%d-%m-%Y')
print(parsed)             # 2026-03-16 00:00:00
print(type(parsed))       # <class 'datetime.datetime'>
Format CodeMeaningExample Output
%YFour-digit year2026
%yTwo-digit year26
%mMonth as zero-padded number03
%BFull month nameMarch
%bAbbreviated month nameMar
%dDay of the month, zero-padded16
%AFull weekday nameMonday
%aAbbreviated weekday nameMon
%HHour, 24-hour clock, zero-padded14
%IHour, 12-hour clock, zero-padded02
%MMinute, zero-padded30
%SSecond, zero-padded00
%pAM or PMPM
%fMicrosecond, zero-padded to 6 digits000000
%zUTC offset (empty if naive)+0000
%ZTime zone name (empty if naive)UTC
%jDay of the year (001-366)075

The single most important rule when using strptime(): the format string passed as the second argument must exactly match the structure of the input string, character for character, including all separators (dashes, slashes, spaces). A mismatch — even something as small as expecting a dash where the string uses a slash — raises a ValueError with the message "time data ... does not match format".

Naive vs Timezone-Aware Datetime — Decision Flowchart

One of the most important — and most commonly mishandled — decisions when working with datetimes is whether to use a naive datetime (no time zone information at all) or a timezone-aware one (carries explicit UTC offset information). This flowchart shows how to decide, and what happens when the two are mixed.

▶️ Creating a datetimeWhere does this value get used?
consider
❓ Crosses time zones or stored long-term?Server logs, APIs, databases
yes
✅ Use timezone-awaredatetime.now(timezone.utc)
later
⚠️ Naive datetime acceptablePurely local, single-timezone context
later
🔗 Comparing two datetimesAre both aware, or both naive?
consistent
✅ Comparison works correctlySame 'kind' on both sides
success
🚫 TypeError raisedCan't compare naive and aware directly
must fix
🏁 Consistent datetime handlingBugs from timezone mixing avoided

Code Execution Flow — from source to output

The general industry guideline: store and compute in UTC using timezone-aware datetimes everywhere internally (databases, APIs, logs), and only convert to a specific local time zone at the very last moment — when actually displaying a value to a human user.

Date Arithmetic with timedelta

The timedelta class represents a duration — the span between two points in time — and is the correct tool for adding or subtracting time from a date or datetime, rather than manually calculating seconds or days yourself.

🐍 Pythontimedelta_arithmetic.py
from datetime import date, datetime, timedelta

today = date.today()
one_week_later = today + timedelta(weeks=1)
print(one_week_later)

deadline = datetime(2026, 12, 31, 23, 59, 59)
reminder = deadline - timedelta(days=3)
print(reminder)              # 2026-12-28 23:59:59

# Subtracting two datetimes gives you a timedelta:
start = datetime(2026, 1, 1)
end = datetime(2026, 3, 16)
difference = end - start
print(difference)            # 74 days, 0:00:00
print(difference.days)       # 74
print(difference.total_seconds())  # 6393600.0
  • timedelta(days=..., seconds=..., microseconds=..., weeks=...) — Accepts multiple unit keywords at once; internally, everything is normalized down to days, seconds, and microseconds.

  • date/datetime + timedelta — Returns a new date/datetime shifted forward by that duration.

  • date/datetime - timedelta — Returns a new date/datetime shifted backward by that duration.

  • datetime - datetime — Returns a timedelta representing the exact difference between the two points in time.

  • timedelta.days, timedelta.seconds, timedelta.total_seconds() — Ways to extract the duration's magnitude in different units for further calculation.

Notably, timedelta has no concept of months or years, because those units don't have a fixed, consistent length (a month can be 28-31 days, a year can be 365 or 366). Adding "one month" correctly (e.g. handling February and month-end edge cases) requires either manual calendar-aware logic or a third-party library like python-dateutil's relativedelta.

Comparing and Sorting Dates

date and datetime objects support all standard comparison operators directly — <, <=, >, >=, ==, and != — making it straightforward to check ordering, sort collections of dates, or filter for a specific range.

🐍 Pythoncomparing_dates.py
from datetime import date

start = date(2026, 1, 1)
end = date(2026, 12, 31)
checkpoint = date(2026, 6, 15)

print(start < checkpoint < end)    # True — chained comparisons work too

events = [date(2026, 5, 1), date(2026, 1, 15), date(2026, 9, 20)]
events.sort()
print(events)   # sorted chronologically: [2026-01-15, 2026-05-01, 2026-09-20]

print(max(events))   # 2026-09-20 — the latest date
print(min(events))   # 2026-01-15 — the earliest date

A critical caveat: Python cannot compare a naive datetime with a timezone-aware one — attempting to do so raises TypeError: can't compare offset-naive and offset-aware datetimes. Both datetimes being compared must be consistently either naive or aware; mixing the two is one of the most common real-world datetime bugs.

Timezone Handling with zoneinfo

Since Python 3.9, the standard library includes the zoneinfo module, providing access to the IANA time zone database (the same authoritative source used across most modern operating systems) without requiring any third-party package. This replaced the earlier common practice of relying on the external pytz library for anything beyond simple UTC handling.

🐍 Pythonzoneinfo_example.py
from datetime import datetime
from zoneinfo import ZoneInfo

# Create a timezone-aware datetime directly in a specific zone:
ny_time = datetime(2026, 3, 16, 9, 0, tzinfo=ZoneInfo('America/New_York'))
print(ny_time)   # 2026-03-16 09:00:00-04:00

# Convert that same moment into a different time zone:
kolkata_time = ny_time.astimezone(ZoneInfo('Asia/Kolkata'))
print(kolkata_time)   # 2026-03-16 18:30:00+05:30

# Both represent the exact SAME instant in time, just displayed differently:
print(ny_time == kolkata_time)   # True
ApproachIncluded with Python?Handles DST automatically?Recommended Since
datetime.timezone.utc✅ Built-in, no import needed beyond datetimeN/A — UTC has no DSTAlways
zoneinfo (ZoneInfo)✅ Built-in since Python 3.9✅ Yes, using the IANA databasePython 3.9+
pytz (third-party)❌ Requires pip install pytz✅ Yes, but with a quirky, error-prone APILegacy code / pre-3.9 projects
Manual fixed UTC offset✅ Built-in via timezone(timedelta(...))❌ No — fixed offset never adjusts for DSTOnly for zones without DST

For any new project targeting Python 3.9 or later, zoneinfo is the recommended approach for full timezone-aware handling — it's built into the standard library, correctly accounts for daylight saving transitions using the same authoritative data your operating system uses, and avoids the historically confusing, easy-to-misuse localize()/normalize() API that pytz required.

now() vs utcnow() vs today() — Choosing the Right Function

The datetime module offers several similar-looking functions for getting the current moment, each with an important distinction worth understanding clearly.

FunctionReturnsTimezone-Aware?Notes
date.today()Current calendar date, local time zoneN/A (date has no time zone concept)Use for date-only values like 'today's date'
datetime.now()Current date and time, local time zone❌ NaiveMost common, but ambiguous about which zone 'local' means when shared across systems
datetime.now(tz)Current date and time, converted to given tz✅ AwareCorrect way to get an explicit, unambiguous current moment
datetime.utcnow()Current UTC date and time❌ Naive (deprecated in 3.12+)Deprecated — produces a naive datetime that LOOKS like UTC but isn't marked as such
datetime.now(timezone.utc)Current UTC date and time✅ AwareThe modern, recommended replacement for utcnow()

As of Python 3.12, datetime.utcnow() is officially deprecated precisely because of this naive-vs-aware ambiguity — it returns a naive datetime whose values happen to represent UTC, but with nothing in the object itself indicating that; that missing context has historically caused numerous production bugs when such a value was later compared or combined with a different timezone-aware or local-time value. The recommended modern replacement is always datetime.now(timezone.utc), which returns the equivalent value but explicitly, correctly marked as timezone-aware UTC.

How the datetime Module's Pieces Fit Together

The datetime module's classes build on each other in layers — from raw calendar/clock values, through formatting and arithmetic, up to full timezone awareness.

Raw Value Classes
date — calendar date onlytime — clock time onlydatetime — combined date + time
Duration & Arithmetic
timedelta — spans between two points in timeSupports +, -, comparison operators
Formatting & Parsing
strftime() — object to stringstrptime() — string to objectisoformat() / fromisoformat()
Timezone Awareness
tzinfo — abstract basetimezone — fixed UTC offsetszoneinfo.ZoneInfo — full IANA database, DST-aware

Architecture Diagram

Every layer builds on the one beneath it: you cannot format or parse a value that doesn't exist yet, you cannot meaningfully compute a duration without two raw values, and a value only becomes fully timezone-aware once a tzinfo subclass (like timezone or ZoneInfo) is explicitly attached to it.

Practice: A Countdown and Age Calculator

This example combines date arithmetic, formatting, and comparisons into two small, realistic utility functions.

🐍 Pythondate_utilities.py
from datetime import date

def calculate_age(birth_date):
    today = date.today()
    age = today.year - birth_date.year
    had_birthday_this_year = (today.month, today.day) >= (birth_date.month, birth_date.day)
    if not had_birthday_this_year:
        age -= 1
    return age

def days_until(target_date):
    today = date.today()
    if target_date < today:
        return f"That date was {(today - target_date).days} days ago."
    remaining = (target_date - today).days
    return f"{remaining} days remaining until {target_date.strftime('%B %d, %Y')}."

print(calculate_age(date(2000, 6, 15)))
print(days_until(date(2026, 12, 25)))

Output

26 154 days remaining until December 25, 2026.

Practice This Code — Live Editor

datetime Module Best Practices

These practices help avoid the most common, and most subtle, bugs when working with dates and times in production code.

  • Store and compute in UTC, display in local time — Use timezone-aware UTC datetimes internally (databases, APIs, logs), converting to a specific local zone only at the final point of display to a user.

  • Prefer datetime.now(timezone.utc) over the deprecated utcnow() — utcnow() returns a naive datetime with no timezone marker at all, a common source of comparison bugs; the aware version is unambiguous.

  • Use zoneinfo instead of pytz for new Python 3.9+ projects — zoneinfo is built into the standard library, uses the same IANA database, and has a simpler, less error-prone API than pytz's localize()/normalize() pattern.

  • Never mix naive and timezone-aware datetimes — Comparing or subtracting one of each raises a TypeError; keep all datetimes in a given code path consistently one or the other.

  • Use timedelta for day/week/hour arithmetic, not manual second math — Manually computing seconds (like adding 86400 for 'one day') is error-prone around DST transitions; timedelta handles this correctly.

  • Remember timedelta has no month or year units — For calendar-aware month/year arithmetic (handling variable month lengths correctly), use the third-party dateutil.relativedelta instead.

  • Match strptime() format strings exactly to the input — Every separator character and field order must match precisely, or Python raises a ValueError describing the mismatch.

  • Use isoformat()/fromisoformat() for machine-to-machine date exchange — ISO 8601 is the unambiguous, internationally standardized format ideal for APIs, logs, and file storage — avoid locale-dependent formats there.

Advantages and Disadvantages of the datetime Module

The datetime module handles the vast majority of real-world date and time needs directly, but it has some genuine rough edges worth understanding.

✅ Advantages
No Installation RequiredPart of the standard library, immediately available with a single import, covering the vast majority of common date/time needs out of the box.
Immutable, Predictable Objectsdate and datetime objects can't be modified in place, making them safe to share and pass around without unexpected side effects.
Rich Comparison and Arithmetic SupportStandard operators (<, >, ==, +, -) work directly and intuitively on dates, times, and durations.
Flexible Formatting via strftime/strptimeA wide, well-documented set of format codes covers virtually every common date string representation.
Built-in Timezone Database (zoneinfo)Since Python 3.9, full IANA timezone data with correct DST handling is available with no external dependency.
❌ Disadvantages
No Native Month/Year Arithmetictimedelta only understands days, weeks, and smaller units — calendar-correct month/year math requires a third-party library like dateutil.
Naive vs Aware Mixing Is a Common TrapComparing a naive and an aware datetime raises a runtime TypeError, and it's easy to accidentally end up with a mix in larger codebases.
utcnow() Historically Confusing (Now Deprecated)A naive datetime that represents UTC but doesn't say so caused real production bugs for years, prompting its deprecation in Python 3.12.
strptime() Format Strings Are Strict and UnforgivingAny mismatch between the format string and the actual input string raises an error rather than attempting a flexible, best-effort parse.
Pre-3.9 Projects Still Need pytz for Full Timezone SupportProjects targeting older Python versions can't use zoneinfo and must rely on the more error-prone pytz library instead.

Python datetime Module — Interview Questions

Frequently asked interview questions about the datetime module, timezone handling, and date arithmetic.

Practice Questions — Test Your Understanding

Work through these practice questions on the datetime module. Try to reason through each one before checking the answer.

1. What does date(2026, 3, 16) - date(2026, 3, 1) return, and what is its value?

Easy

2. Why might adding timedelta(days=1) to a datetime near a daylight-saving transition NOT change the clock time by exactly 24 hours in wall-clock terms for a timezone-aware datetime?

Hard

3. What is the output of datetime(2026, 3, 16).strftime('%A')?

Easy

4. Why does comparing datetime.now() with datetime.now(timezone.utc) raise a TypeError?

Medium

5. If today is date(2026, 2, 27), what does today + timedelta(days=3) return, and why is this significant for February?

Medium

6. What is wrong with using datetime.strptime('2026-13-01', '%Y-%m-%d')?

Medium

7. How would you get the exact same moment in time expressed in two different time zones using zoneinfo?

Hard

8. Why is a manual formula like 'age = this_year - birth_year' potentially incorrect for calculating someone's current age?

Hard

Conclusion — Handling Dates and Times Correctly

The datetime module packs an enormous amount of real-world calendar and clock complexity behind a compact, well-designed set of classes. Its true value shows up not in the simple cases — printing today's date is trivial — but in the harder ones it quietly gets right: leap years, variable month lengths, daylight saving transitions, and the crucial distinction between a naive value and one that's genuinely, unambiguously anchored to a specific point in absolute time.

The habits worth carrying forward: work in UTC internally and convert to local time only for display, always use timezone-aware datetimes for anything that might be compared, stored, or shared across systems, reach for zoneinfo rather than manual offsets or the older pytz library on modern Python, and use timedelta for date arithmetic rather than manually computing seconds or days yourself.

SituationRecommended Approach
Storing timestamps in a database or API✅ Timezone-aware UTC (datetime.now(timezone.utc))
Displaying a time to a specific user✅ Convert to their local zone with .astimezone(ZoneInfo(...))
Calendar-only data (birthdays, deadlines)✅ Use date, not datetime
Adding/subtracting days, weeks, hours✅ timedelta
Adding/subtracting months or years correctly✅ dateutil.relativedelta (third-party)
Exchanging dates between systems/APIs✅ ISO 8601 via isoformat()/fromisoformat()
Getting 'current UTC time' in new code✅ datetime.now(timezone.utc), not utcnow()

From here, natural next topics include the third-party dateutil library for calendar-aware relative date arithmetic, the time module for lower-level, performance-focused timing needs, and working with dates in pandas for anyone handling time-series data at scale.

Frequently Asked Questions (FAQ)