Dates and Times: datetime, time and calendar
datetime handles calendar dates and clock times, timedelta handles durations, strftime and strptime convert to and from text, and time zones are where the real difficulty is.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
The four datetime types
from datetime import date, time, datetime, timedelta
print(date(2026, 8, 22)) # 2026-08-22 a calendar date
print(time(14, 30, 0)) # 14:30:00 a clock time
print(datetime(2026, 8, 22, 14, 30)) # 2026-08-22 14:30:00 both
print(timedelta(days=7, hours=3)) # 7 days, 3:00:00 a durationGetting the current moment
from datetime import date, datetime, timezone
print(date.today()) # 2026-08-22
print(datetime.now()) # local time, no time zone attached
print(datetime.now(timezone.utc)) # UTC, time zone attached
print(datetime.now().date()) # just the date part
print(datetime.now().time()) # just the time partReading the parts
moment = datetime(2026, 8, 22, 14, 30, 45)
print(moment.year, moment.month, moment.day) # 2026 8 22
print(moment.hour, moment.minute, moment.second) # 14 30 45
print(moment.weekday()) # 5 - Monday is 0
print(moment.isoweekday()) # 6 - Monday is 1
print(moment.isoformat()) # 2026-08-22T14:30:45
print(moment.timestamp()) # seconds since 1970Formatting: strftime
moment = datetime(2026, 8, 22, 14, 30, 45)
print(moment.strftime("%Y-%m-%d")) # 2026-08-22
print(moment.strftime("%d/%m/%Y")) # 22/08/2026
print(moment.strftime("%B %d, %Y")) # August 22, 2026
print(moment.strftime("%A")) # Saturday
print(moment.strftime("%H:%M:%S")) # 14:30:45
print(moment.strftime("%I:%M %p")) # 02:30 PM
print(moment.strftime("%d %b %Y at %H:%M")) # 22 Aug 2026 at 14:30
print(f"{moment:%Y-%m-%d %H:%M}") # f-strings work directly| Code | Means | Example |
|---|---|---|
%Y / %y | Year, 4 digit / 2 digit | 2026 / 26 |
%m / %B / %b | Month number / full name / short | 08 / August / Aug |
%d | Day of month | 22 |
%A / %a | Weekday full / short | Saturday / Sat |
%H / %I | Hour 24 / 12 | 14 / 02 |
%M / %S | Minute / second | 30 / 45 |
%p | AM or PM | PM |
%j | Day of the year | 234 |
%% | A literal percent sign | % |
Parsing: strptime
from datetime import datetime
print(datetime.strptime("2026-08-22", "%Y-%m-%d"))
print(datetime.strptime("22/08/2026 14:30", "%d/%m/%Y %H:%M"))
print(datetime.strptime("August 22, 2026", "%B %d, %Y"))
# The format must match EXACTLY
# datetime.strptime("2026-8-22", "%Y-%m-%d") # works, single digits are accepted
# datetime.strptime("22-08-2026", "%Y-%m-%d") # ValueErrorprint(datetime.fromisoformat("2026-08-22T14:30:45")) # no format string needed
print(date.fromisoformat("2026-08-22"))Remember which is which: format produces a string (strftime), parse reads one (strptime). Always wrapstrptimeon external data in atryblock, because it raisesValueErroron anything unexpected.
Arithmetic with timedelta
from datetime import date, datetime, timedelta
today = date(2026, 8, 22)
print(today + timedelta(days=7)) # 2026-08-29
print(today - timedelta(days=30)) # 2026-07-23
print(today + timedelta(weeks=2)) # 2026-09-05
start = datetime(2026, 1, 1)
end = datetime(2026, 8, 22, 14, 30)
gap = end - start
print(type(gap)) # <class 'datetime.timedelta'>
print(gap.days) # 233
print(gap.total_seconds()) # the whole gap in seconds
print(gap) # 233 days, 14:30:00def describe_age(gap):
"""Turn a timedelta into a readable phrase."""
seconds = int(gap.total_seconds())
if seconds < 60:
return f"{seconds} seconds ago"
minutes, seconds = divmod(seconds, 60)
if minutes < 60:
return f"{minutes} minutes ago"
hours, minutes = divmod(minutes, 60)
if hours < 24:
return f"{hours} hours ago"
return f"{hours // 24} days ago"
print(describe_age(timedelta(seconds=4000))) # 1 hours agotimedelta has no months or years
# timedelta(months=1) # TypeError - months vary in length
def add_months(d, months):
"""Add whole months, clamping the day to the end of the target month."""
import calendar
month = d.month - 1 + months
year = d.year + month // 12
month = month % 12 + 1
day = min(d.day, calendar.monthrange(year, month)[1])
return d.replace(year=year, month=month, day=day)
print(add_months(date(2026, 1, 31), 1)) # 2026-02-28
print(add_months(date(2026, 8, 22), 6)) # 2027-02-22A month is not a fixed duration, so timedelta deliberately refuses to model one. Adding a month to 31 January has no single correct answer, which is why you must choose the rule yourself.
Comparing and sorting
from datetime import date
dates = [date(2026, 8, 22), date(2026, 1, 5), date(2026, 12, 31)]
print(sorted(dates))
print(min(dates), max(dates))
print(date(2026, 8, 22) > date(2026, 1, 5)) # True
# But never compare a date with a datetime
# print(date(2026, 8, 22) < datetime(2026, 8, 22)) # TypeErrorTime zones
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
naive = datetime(2026, 8, 22, 14, 30) # no zone: ambiguous
aware = datetime(2026, 8, 22, 14, 30, tzinfo=timezone.utc) # unambiguous
print(naive.tzinfo) # None
print(aware.tzinfo) # UTC
kolkata = ZoneInfo("Asia/Kolkata")
london = ZoneInfo("Europe/London")
local = datetime(2026, 8, 22, 20, 0, tzinfo=kolkata)
print(local) # 20:00 +05:30
print(local.astimezone(london)) # the same moment in London
print(local.astimezone(timezone.utc)) # the same moment in UTCThe rule that avoids nearly every date bug: store and compute in UTC, convert to a local zone only when displaying. A naive datetime has no zone attached, so comparing one with an aware datetime raises TypeError - which is Python protecting you from a wrong answer.time
import time
print(time.time()) # seconds since 1 January 1970, as a float
print(time.perf_counter()) # a high resolution counter for measuring
start = time.perf_counter()
total = sum(range(1_000_000))
print(f"{time.perf_counter() - start:.4f}s")
time.sleep(0.5) # pause for half a second
print(time.strftime("%Y-%m-%d %H:%M:%S"))| Function | Use for |
|---|---|
time.time() | A timestamp to store or compare |
time.perf_counter() | Measuring how long something took |
time.monotonic() | Timeouts; never goes backwards |
time.sleep(s) | Pausing |
Never measure a duration with time.time(). The system clock can be adjusted while your code runs, so the difference can be wrong or even negative. perf_counter cannot.
calendar
import calendar
print(calendar.isleap(2026), calendar.isleap(2024)) # False True
print(calendar.monthrange(2026, 2)) # (6, 28) weekday of the 1st, length
print(calendar.month_name[8]) # August
print(calendar.day_name[0]) # Monday
print(calendar.weekday(2026, 8, 22)) # 5
print(calendar.month(2026, 8)) # a printable month
print(calendar.calendar(2026)[:200]) # the whole yearimport calendar
from datetime import date
def working_days(year, month):
"""Count the weekdays in a month."""
_, length = calendar.monthrange(year, month)
return sum(
1 for day in range(1, length + 1)
if date(year, month, day).weekday() < 5
)
print(working_days(2026, 8))A worked example
from datetime import datetime, date, timedelta
def parse_deadline(text):
for pattern in ("%Y-%m-%d", "%d/%m/%Y", "%d %b %Y"):
try:
return datetime.strptime(text, pattern).date()
except ValueError:
continue
raise ValueError(f"unrecognised date: {text!r}")
def status(deadline, today=None):
today = today or date.today()
gap = (deadline - today).days
if gap < 0:
return f"overdue by {-gap} days"
if gap == 0:
return "due today"
if gap <= 7:
return f"due in {gap} days"
return f"due on {deadline:%d %b %Y}"
for text in ["2026-08-25", "01/09/2026", "20 Aug 2026"]:
deadline = parse_deadline(text)
print(f"{text:<14}{status(deadline, date(2026, 8, 22))}")Common mistakes
- Mixing naive and aware datetimes and meeting
TypeError. - Confusing
strftimeandstrptime. - Measuring elapsed time with
time.time(). - Assuming
weekday()starts on Sunday. It starts on Monday, as 0. - Trying to build a
timedeltain months or years. - Comparing a
datewith adatetime. - Parsing user supplied dates without a
tryblock.
Best practices
- Store timestamps in UTC and convert only for display.
- Use
fromisoformatandisoformatfor machine readable dates. - Use
perf_counterfor measuring andtime()for timestamps. - Pass a
todayparameter into date logic so it can be tested with a fixed date. - Handle month arithmetic explicitly, choosing your own end of month rule.
Practice
- Calculate a person's exact age in years, months and days from their date of birth.
- Write a function that returns the next occurrence of a given weekday.
- Parse dates in three different formats, rejecting anything that matches none.
- Convert a meeting time between three time zones and print all three.
- Count the Fridays that fall on the 13th in a given year.
Conclusion
date, time, datetime and timedelta cover calendar work; strftime and strptime cover text. Keep everything in UTC internally, use perf_counter for measurement, and remember that months are not durations.