Skip to content
aviral gupta

// I4.4 · ~30 min · Intermediate

Dates and times with datetime

After this lesson you can read and write dates, calculate with them using timedelta, and handle times in UTC without mixing naive and aware datetimes.

Lesson 4 of 6 in I4 The standard library for real programs

You will be able to

  • Create dates and datetimes, read them with fromisoformat and format them with strftime
  • Calculate with timedelta: add days and weeks, subtract dates, compare them
  • Use aware datetimes in UTC with timezone.utc, and tell them apart from naive ones
  1. Warm-up · Activity 1 of 7

    Warm-up from the regular expressions lesson: what does this print?

    import re
    
    print(re.search(r"\d{4}-\d{2}-\d{2}", "Invoice from 2026-09-29, due soon").group())
  2. Predict · Activity 2 of 7

    Predict before you read on: what does this print?

    from datetime import date, timedelta
    
    print(date(2026, 9, 29) + timedelta(days=3))
  3. Practice · Activity 3 of 7

    Fill in the code for the year with four digits, so that the date prints as 29.09.2026.

    text = d.strftime("%d.%m.____")
    d.strftime("%d.%m.")
  4. Practice · Activity 4 of 7

    How many days is it from 29 September to Christmas Eve? What does this print?

    from datetime import date
    
    print((date(2026, 12, 24) - date(2026, 9, 29)).days)
  5. Practice · Activity 5 of 7

    Match each expression to what it gives.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. A timedelta of 36 hours. What does this print?

    from datetime import timedelta
    
    duration = timedelta(hours=36)
    print(duration.days, duration.seconds)
  7. Apply · Activity 7 of 7

    Mini-task. Write a program that prints how many days are left until the next birthday, like 166 days until 14.03.2027. Use a fixed today = date(2026, 9, 29) so you can check the result, and a birthday of 14 March 1990. If this year's birthday is already past, use next year's.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Deadlines and log times

The program reads deadlines in ISO format, sorts them, and says how many days are left from a fixed today (a real program would use date.today()). Then it reads two log times in UTC, one ending in Z and one in +00:00, and works out how long the job ran.

main.py

from datetime import date, datetime, timedelta

today = date(2026, 9, 29)  # in a real program: date.today()
deadlines = {"tax return": "2026-10-31", "passport": "2026-09-15", "report": "2026-10-02"}

for task, text in sorted(deadlines.items(), key=lambda item: item[1]):
    due = date.fromisoformat(text)
    left = (due - today).days
    status = "overdue" if left < 0 else f"{left} days left"
    print(f"{task:<11}{due.strftime('%a %d.%m.%Y')}  {status}")

# Two times from a server log, in UTC.
start = datetime.fromisoformat("2026-09-29T08:15:00Z")
end = datetime.fromisoformat("2026-09-29T10:05:30+00:00")
print(end - start, (end - start).total_seconds())
print(start.tzinfo, start.isoformat())
print((start + timedelta(weeks=2)).date())

Run it with

python main.py

Output

passport   Tue 15.09.2026  overdue
report     Fri 02.10.2026  3 days left
tax return Sat 31.10.2026  32 days left
1:50:30 6630.0
UTC 2026-09-29T08:15:00+00:00
2026-10-13
  • ISO dates sort correctly as text, because the year comes first.
  • A negative .days means the date is in the past.
  • Z and +00:00 both mean UTC, so the two times can be subtracted.
  • fromisoformat made both times aware, so their difference is an exact duration.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Library loans

Dates come as ISO strings. Write due_date(borrowed, weeks=3), which returns the return date as an ISO string; days_overdue(due, returned), which returns how many days late a book came back, or 0 when it was on time; and german(iso), which formats a date as 05.10.2026.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    date.fromisoformat turns the text into a date, and .isoformat() turns a date back into text.

  2. Hint 2

    Add timedelta(weeks=weeks). For days_overdue, subtract the due date from the return date and take .days.

  3. Hint 3

    max(late, 0) turns a negative number of days into 0. strftime("%d.%m.%Y") writes the German form.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from datetime import date, timedelta


def due_date(borrowed: str, weeks: int = 3) -> str:
    return (date.fromisoformat(borrowed) + timedelta(weeks=weeks)).isoformat()


def days_overdue(due: str, returned: str) -> int:
    late = (date.fromisoformat(returned) - date.fromisoformat(due)).days
    return max(late, 0)


def german(iso: str) -> str:
    return date.fromisoformat(iso).strftime("%d.%m.%Y")


if __name__ == "__main__":
    print(due_date("2026-09-29"), days_overdue("2026-10-20", "2026-10-23"), german("2026-10-05"))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from datetime import date, timedelta


def due_date(borrowed: str, weeks: int = 3) -> str:
    return borrowed


def days_overdue(due: str, returned: str) -> int:
    return 0


def german(iso: str) -> str:
    return iso


if __name__ == "__main__":
    print(due_date("2026-09-29"), days_overdue("2026-10-20", "2026-10-23"), german("2026-10-05"))

test_main.py

from main import days_overdue, due_date, german


def test_due_date():
    """Three weeks after 2026-09-29 is 2026-10-20"""
    got = due_date("2026-09-29")
    assert got == "2026-10-20", f"due_date returned {got!r}"


def test_due_date_new_year():
    """Two weeks after 2026-12-20 is in the next year"""
    got = due_date("2026-12-20", 2)
    assert got == "2027-01-03", f"due_date('2026-12-20', 2) returned {got!r}"


def test_overdue():
    """Returned 3 days late gives 3, returned early gives 0"""
    got = days_overdue("2026-10-20", "2026-10-23"), days_overdue("2026-10-20", "2026-10-01")
    assert got == (3, 0), f"days_overdue returned {got!r}, expected (3, 0)"


def test_german():
    """2026-10-05 is written 05.10.2026"""
    got = german("2026-10-05")
    assert got == "05.10.2026", f"german returned {got!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Times in UTC

Log times arrive in mixed form: some with an offset such as Z or +02:00, some without, which the logs mean as UTC. Write parse_utc(text), which returns an aware datetime in UTC for both kinds. Then write minutes_between(start, end), which returns the whole minutes between two such texts. Mixed forms must not raise a TypeError.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    After fromisoformat, check moment.tzinfo: it is None for a text without an offset.

  2. Hint 2

    A naive time that means UTC gets its zone with moment.replace(tzinfo=timezone.utc).

  3. Hint 3

    An aware time in another offset is converted with moment.astimezone(timezone.utc). Then any two results can be subtracted.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from datetime import datetime, timezone


def parse_utc(text: str) -> datetime:
    moment = datetime.fromisoformat(text)
    if moment.tzinfo is None:
        return moment.replace(tzinfo=timezone.utc)
    return moment.astimezone(timezone.utc)


def minutes_between(start: str, end: str) -> int:
    return int((parse_utc(end) - parse_utc(start)).total_seconds() // 60)


if __name__ == "__main__":
    print(parse_utc("2026-09-29T10:00:00+02:00"))
    print(minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05"))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from datetime import datetime, timezone


def parse_utc(text: str) -> datetime:
    return datetime.fromisoformat(text)


def minutes_between(start: str, end: str) -> int:
    return int((parse_utc(end) - parse_utc(start)).total_seconds() // 60)


if __name__ == "__main__":
    print(parse_utc("2026-09-29T10:00:00+02:00"))
    print(minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05"))

test_main.py

from datetime import datetime, timezone

from main import minutes_between, parse_utc


def test_naive_becomes_utc():
    """A time without an offset is read as UTC"""
    got = parse_utc("2026-09-29 08:15")
    assert got == datetime(2026, 9, 29, 8, 15, tzinfo=timezone.utc), f"parse_utc returned {got!r}"
    assert got.tzinfo == timezone.utc, f"the result has tzinfo {got.tzinfo!r}, expected timezone.utc"


def test_offset_converted():
    """10:00 at +02:00 is 08:00 in UTC"""
    got = parse_utc("2026-09-29T10:00:00+02:00")
    assert (got.hour, got.tzinfo) == (8, timezone.utc), f"parse_utc returned {got!r}, expected 08:00 in UTC"


def test_mixed_minutes():
    """08:15Z to 10:05 without an offset is 110 minutes"""
    got = minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05")
    assert got == 110, f"minutes_between returned {got!r}"


def test_over_midnight():
    """23:30 to 00:10 the next day is 40 minutes"""
    got = minutes_between("2026-09-29T23:30:00+00:00", "2026-09-30T00:10:00+00:00")
    assert got == 40, f"minutes_between returned {got!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Comparing naive and aware datetimes

from datetime import datetime, timezone

local = datetime(2026, 9, 29, 12, 0)
server = datetime(2026, 9, 29, 12, 0, tzinfo=timezone.utc)
print(local < server)

What Python prints

TypeError: can't compare offset-naive and offset-aware datetimes

Why, and the fix

local has no time zone, so Python cannot tell which moment it means, and refuses to order it against an exact moment. Decide what the naive value means. If it is UTC, write local.replace(tzinfo=timezone.utc); better, create aware values from the start with tzinfo=timezone.utc.

Reading a German date with fromisoformat

from datetime import date

birthday = date.fromisoformat("29.09.2026")

What Python prints

ValueError: Invalid isoformat string: '29.09.2026'

Why, and the fix

fromisoformat only reads ISO 8601, year first: 2026-09-29. For other layouts, give the format: datetime.strptime("29.09.2026", "%d.%m.%Y").date(). Store and exchange dates in ISO form, and use strftime only for display.

Adding a number to a date

from datetime import date

tomorrow = date(2026, 9, 29) + 1

What Python prints

TypeError: unsupported operand type(s) for +: 'datetime.date' and 'int'

Why, and the fix

A plain 1 does not say whether you mean a day, an hour or a week. Add a timedelta instead: date(2026, 9, 29) + timedelta(days=1).

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

date, datetime and text

date(2026, 9, 29) is a calendar day; datetime(2026, 9, 29, 14, 30) adds a time. Python checks them: date(2026, 2, 30) raises ValueError. Read ISO 8601 text with date.fromisoformat("2026-09-29") or datetime.fromisoformat("2026-09-29T14:30"), and write it with isoformat(). print shows a date the same way, but puts a space instead of the T in a datetime. For other layouts, strftime formats with codes: %d day, %m month, %Y four-digit year, %H:%M hours and minutes, so "%d.%m.%Y" gives 29.09.2026. Use date.today() or datetime.now() for the current moment.

Calculating with timedelta

A timedelta is a duration: timedelta(days=3), timedelta(weeks=2, hours=6). Adding it to a date gives a new date, across month and year ends. Subtracting two dates gives a timedelta, and .days is the number of days between them. Dates compare with < and >, and sort in time order. A timedelta stores only days, seconds and microseconds, so timedelta(hours=36) is 1 day and 43200 seconds; total_seconds() gives the whole duration. There is no months= argument: months differ in length.

Naive and aware, and UTC

A datetime without time-zone information is naive: 14:30, but where? One with tzinfo is aware and names one exact moment. For timestamps you store or compare, use UTC: datetime.now(timezone.utc), or datetime(..., tzinfo=timezone.utc). fromisoformat reads offsets such as Z or +02:00, and astimezone(timezone.utc) converts to UTC. Naive and aware datetimes cannot be ordered with < and > or subtracted: Python raises TypeError. == does not raise; it gives False. If you know a naive value is UTC, attach the zone with replace(tzinfo=timezone.utc).

Sources

Last reviewed September 29, 2026