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.

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 duration

Getting 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 part

Reading 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 1970

Formatting: 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
CodeMeansExample
%Y / %yYear, 4 digit / 2 digit2026 / 26
%m / %B / %bMonth number / full name / short08 / August / Aug
%dDay of month22
%A / %aWeekday full / shortSaturday / Sat
%H / %IHour 24 / 1214 / 02
%M / %SMinute / second30 / 45
%pAM or PMPM
%jDay of the year234
%%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")    # ValueError
print(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 wrap strptime on external data in a try block, because it raises ValueError on 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:00
def 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 ago

timedelta 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-22

A 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))    # TypeError

Time 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 UTC
The 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"))
FunctionUse 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 year
import 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 strftime and strptime.
  • Measuring elapsed time with time.time().
  • Assuming weekday() starts on Sunday. It starts on Monday, as 0.
  • Trying to build a timedelta in months or years.
  • Comparing a date with a datetime.
  • Parsing user supplied dates without a try block.

Best practices

  • Store timestamps in UTC and convert only for display.
  • Use fromisoformat and isoformat for machine readable dates.
  • Use perf_counter for measuring and time() for timestamps.
  • Pass a today parameter into date logic so it can be tested with a fixed date.
  • Handle month arithmetic explicitly, choosing your own end of month rule.

Practice

  1. Calculate a person's exact age in years, months and days from their date of birth.
  2. Write a function that returns the next occurrence of a given weekday.
  3. Parse dates in three different formats, rejecting anything that matches none.
  4. Convert a meeting time between three time zones and print all three.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

itertools and functools

itertools builds lazy iterators for combining and slicing sequences. functools transforms functions themselves - caching, partial application and redu...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.