Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 6 – Modules, packages and the standard library

Dates, times and time zones (IST included)

Create, format and parse dates in Python, calculate with timedelta, and convert aware datetimes between time zones with zoneinfo, IST included.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create dates and datetimes, and format and parse them with strftime() and strptime()
  • Calculate durations, deadlines and month steps with timedelta and calendar
  • Distinguish naive from aware datetimes, and attach or convert a time zone with replace() and astimezone()
  • Convert times between zones with zoneinfo, and handle daylight-saving gaps and repeated hours with UTC and fold
  • Store and exchange times as UTC in ISO 8601

Before you start

What you’ll learn · Before you start

What you will learn

  • Create dates and datetimes, and format and parse them with strftime() and strptime()
  • Calculate durations, deadlines and month steps with timedelta and calendar
  • Distinguish naive from aware datetimes, and attach or convert a time zone with replace() and astimezone()
  • Convert times between zones with zoneinfo, and handle daylight-saving gaps and repeated hours with UTC and fold
  • Store and exchange times as UTC in ISO 8601

Before you start

On this page

A weekly call at 19:30 in India is at 14:00 in London and 09:00 in New York, until March: then it moves to 10:00 in New York, and two or three weeks later to 15:00 in London. Python’s datetime module and its companion zoneinfo get such times right, as long as every time says which clock it belongs to.

datetime has four types for calendar work: date for a day, time for a clock reading, datetime for both and timedelta for a length of time. A datetime is naive datetime: A datetime with no time zone: its tzinfo is None, so it is a clock reading that could belong to any place. When Python must convert one, it assumes the computer's local time. when it has no time zone and aware datetime: A datetime that carries a tzinfo giving its offset from UTC, so it names one exact moment. datetime.now(UTC) and a datetime made with tzinfo=ZoneInfo("Asia/Kolkata") are aware. when it has one, and only an aware value names one moment that the whole world agrees on. One habit prevents most bugs: make datetimes aware as soon as they enter the program, store and calculate in UTC, and convert to a local zone only to show a time to a person.

date, time and datetime check their values and never change

Each of the four types holds one kind of value, and the program below makes the first three of them:

Type Holds Made in this lesson as
date a day: the year, the month and the day date(2027, 3, 15)
time a clock reading: hours, minutes, seconds time(9, 30)
datetime a day and a clock reading, with a time zone or without datetime.combine(exam, start)
timedelta a length of time exam - today, in the next section
Making dates and reading their parts Python · date_basics.py
from datetime import date, datetime, time

exam = date(2027, 3, 15)
print(exam, exam.year, exam.month, exam.day)
print(exam.strftime("%A"), exam.weekday(), exam.isoweekday())
print(exam.isocalendar())

start = time(9, 30)
slot = datetime.combine(exam, start)
print(slot, "|", slot.isoformat())
print(slot.replace(hour=14, minute=0))
print(date.fromisoformat("2027-03-15") == exam)

Output

2027-03-15 2027 3 15
Monday 0 1
datetime.IsoCalendarDate(year=2027, week=11, weekday=1)
2027-03-15 09:30:00 | 2027-03-15T09:30:00
2027-03-15 14:00:00
True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 date_basics.py

  • date(2027, 3, 15) checks its arguments, so a month 13 or a 31st day in April raises ValueError. Its parts are attributes; weekday() counts Monday as 0 and isoweekday() as 1, and isocalendar() gives the ISO 8601 week, week 11 here.
  • datetime.combine() joins a date and a time. Dates and times are immutable: Unable to change once made. Strings, numbers and tuples are immutable: an operation that seems to change one, such as upper(), returns a new object instead. More in “Strings: literals, indexing and slicing”: they never change once made, so replace() returns a copy with some fields changed.
  • isoformat() and fromisoformat() write and read the international form, year first, which also sorts correctly as text.

Real programs ask date.today() or datetime.now(zone) for the current day and time. The examples here fix their dates instead, so that their output never changes.

Ask for a day that does not exist

Press Edit, change the exam to date(2027, 4, 31) and run the program. It stops on that line with ValueError, because April has 30 days, before anything is printed.

Checkpoint

Check: date, time and datetime

  1. Question 1 of 2 date(2027, 3, 15) is a Monday. What do its weekday() and isoweekday() return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0 and 1

    weekday() numbers the days from Monday 0 to Sunday 6, and isoweekday() from Monday 1 to Sunday 7, which is what date_basics.py prints for the exam day: Monday 0 1.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    from datetime import date
    d = date(2027, 3, 15)
    d.replace(day=20)
    print(d.day)
    Show the answer: Question 2 of 2

    Answer: 15

    A date is immutable, so replace() returns a new one. Write d = d.replace(day=20) to keep it; the original d still has day 15.

A timedelta counts days and seconds, never months

Subtracting one date from another gives a timedelta, and adding one moves a date. This program counts the days to an exam, adds up study sessions and steps through months:

Days until an exam, study hours and month steps Python · days_until.py
import calendar
from datetime import date, timedelta

today = date(2027, 1, 20)  # a fixed day, so the output never changes
exam = date(2027, 3, 15)
left = exam - today
print(left, "|", left.days, "days")
print(left.days // 7, "weeks and", left.days % 7, "days")
print("revision starts", exam - timedelta(weeks=6))

study = timedelta(hours=2, minutes=45)
print(study * 30, "|", (study * 30) / timedelta(hours=1), "hours")
print(timedelta(hours=-1), "|", timedelta(days=1, minutes=90).total_seconds())


def add_months(day, months):
    """Move by whole months, keeping the day if the month has it, else its last day."""
    index = day.month - 1 + months
    year, month = day.year + index // 12, index % 12 + 1
    last = calendar.monthrange(year, month)[1]
    return day.replace(year=year, month=month, day=min(day.day, last))


print(add_months(date(2027, 1, 31), 1), add_months(date(2028, 1, 31), 1), add_months(date(2027, 11, 30), 3))
try:
    date(2027, 1, 31).replace(month=2)
except ValueError as error:
    print("ValueError:", error)

Output

54 days, 0:00:00 | 54 days
7 weeks and 5 days
revision starts 2027-02-01
3 days, 10:30:00 | 82.5 hours
-1 day, 23:00:00 | 91800.0
2027-02-28 2028-02-29 2028-02-29
ValueError: day 31 must be in range 1..28 for month 2 in year 2027

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 days_until.py

  • 54 days are left, and six weeks before the exam is 2027-02-01.
  • A timedelta keeps only days, seconds and microseconds; weeks, hours and minutes are converted when you make one. total_seconds() gives the whole length in seconds, and dividing by another timedelta asks “how many of these”: 30 sessions of 2 hours 45 minutes come to 82.5 hours.
  • Negative durations look odd: timedelta(hours=-1) prints as -1 day, 23:00:00, minus one day plus 23 hours, because only the days may be negative.
  • A timedelta has no months, because a month has 28 to 31 days. add_months() above moves the month number, keeps the day if the new month has it and otherwise takes its last day, which calendar.monthrange() gives. replace(month=2) alone fails on date(2027, 1, 31), and the error says which days that February has.

The month arithmetic of add_months() is the one step a timedelta cannot do for you. Here it is at the top level of a program, so that every line can be stepped through, for 1 and 13 months after the last day of January:

Interactive · line by line One month and thirteen months after the last day of January Step through the two passes. Watch index turn a count of months into a year and a month, and min() pull the day back when the new month is shorter.

Program · month_steps.py

from calendar import monthrange
from datetime import date

day = date(2027, 1, 31)
for months in [1, 13]:
    index = day.month - 1 + months
    year = day.year + index // 12
    month = index % 12 + 1
    last = monthrange(year, month)[1]
    keep = min(day.day, last)
    print(date(year, month, keep))

Variables

After line 5: monthrange is monthrange(year, month); date is date; day is datetime.date(2027, 1, 31); months is 13; index is 13; year is 2028; month is 2; last is 29; keep is 29; the loop's own list from [1, 13] is (1, 13) (the loop stopped at position 2).monthrangemonthrange(year, month)datedatedaydatetime.date(2027, 1, 31)months13index13year2028month2last29keep29the loop reads [1, 13]101312end
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

2027-02-28
2028-02-29

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 19 steps are listed below.

Line run 5
Next line end
Loop pass 2
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. from calendar import monthrange loads monthrange.
  3. Line 2. from datetime import date loads date.
  4. Line 4. day = date(2027, 1, 31) sets day to datetime.date(2027, 1, 31).
  5. Line 5. for months in [1, 13]: takes the item at position 0 of [1, 13]: months is 1.
  6. Line 6. index = day.month - 1 + months sets index to 1. index counts months from January of the start year, with January as 0, so January plus 1 month is 1, February.
  7. Line 7. year = day.year + index // 12 sets year to 2027.
  8. Line 8. month = index % 12 + 1 sets month to 2.
  9. Line 9. last = monthrange(year, month)[1] sets last to 28. monthrange() gives the weekday of the first day and the number of days in the month; [1] takes the number of days. This February has 28.
  10. Line 10. keep = min(day.day, last) sets keep to 28. Key point The day must exist in the new month, so min() takes 28 instead of 31. A timedelta cannot do this step, because it has no months.
  11. Line 11. print(date(year, month, keep)) prints 2027-02-28.
  12. Line 5. for months in [1, 13]: takes the item at position 1 of [1, 13]: months is 13.
  13. Line 6. index = day.month - 1 + months sets index to 13 (it was 1).
  14. Line 7. year = day.year + index // 12 sets year to 2028 (it was 2027). 13 months on, index is 13, so the year goes up by 13 // 12 = 1, to 2028.
  15. Line 8. month = index % 12 + 1 month keeps its value, 2. 13 % 12 + 1 = 2, so the month is February again, 12 months later than in the first pass.
  16. Line 9. last = monthrange(year, month)[1] sets last to 29 (it was 28).
  17. Line 10. keep = min(day.day, last) sets keep to 29 (it was 28).
  18. Line 11. print(date(year, month, keep)) prints 2028-02-29.
  19. Line 5. for months in [1, 13]: finds no item at position 2 of [1, 13] (it has 2 items), so the loop ends. That was the last line, so the program ends. Both answers are the last day of February, the 28th in a common year and the 29th in a leap year. date(2027, 1, 31).replace(month=2) would have raised ValueError instead.

Both passes land on the last day of February, 28 in 2027 and 29 in the leap year 2028, where replace(month=2) would have raised ValueError.

Checkpoint

Check: Durations with timedelta

  1. Question 1 of 2 How many days are there from date(2027, 1, 20) to date(2027, 2, 3)?

    Type a number.

    A hint

    Subtract the two dates and read the timedelta's days.

    Show the answer: Question 1 of 2

    Answer: 14 days

    date(2027, 2, 3) − date(2027, 1, 20) is a timedelta of 14 days: the 11 days of January after the 20th, and the first 3 days of February.

  2. Question 2 of 2 How does timedelta(hours=-1) print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: -1 day, 23:00:00

    A timedelta stores days, seconds and microseconds, and only the days may be negative. One hour back is −1 day plus 82,800 seconds, which prints as -1 day, 23:00:00, as days_until.py shows.

Date Calculator Count the days, weeks and business days between two dates, or add days and months, to check your own code.

The format, not the text, decides what a date means

strftime() and f-strings turn a date into text with strftime code: A code such as %d, %B or %H that stands for one part of a date or a time. It goes after the colon when an f-string formats a datetime, or into strftime(). More in “f-strings and the format mini-language”, and strptime() reads text with the same codes:

Reading day-first dates with strptime() Python · parse_dates.py
import warnings
from datetime import date, datetime

text = "04-10-2026"
day_first = datetime.strptime(text, "%d-%m-%Y").date()
month_first = datetime.strptime(text, "%m-%d-%Y").date()
print(day_first, "|", month_first)
print(date.strptime(text, "%d-%m-%Y"))  # new in Python 3.14
print(f"{day_first:%d %B %Y} | {day_first:%a %d/%m/%y}")

print(datetime.strptime("4-10-2026 9:05", "%d-%m-%Y %H:%M"))  # one-digit fields are accepted
for bad in ["31-04-2026", "2026-10-04"]:
    try:
        datetime.strptime(bad, "%d-%m-%Y")
    except ValueError as error:
        print("ValueError:", error)

# A day and month without a year: the default year, 1900, is not a leap year
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    try:
        datetime.strptime("29-02", "%d-%m")
    except ValueError as error:
        print("ValueError:", error)
print(caught[0].category.__name__)

print(datetime.fromisoformat("2026-10-04T09:30:00+05:30"))
print(datetime.fromisoformat("2026-10-04T04:00:00Z"))

Output

2026-10-04 | 2026-04-10
2026-10-04
04 October 2026 | Sun 04/10/26
2026-10-04 09:05:00
ValueError: day 31 must be in range 1..30 for month 4 in year 2026
ValueError: time data '2026-10-04' does not match format '%d-%m-%Y'
ValueError: day 29 must be in range 1..28 for month 2 in year 1900
DeprecationWarning
2026-10-04 09:30:00+05:30
2026-10-04 04:00:00+00:00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 parse_dates.py

The same text can mean two days, and some texts mean none. The program’s own inputs, side by side:

Text Format What strptime() makes of it
"04-10-2026" "%d-%m-%Y" 2026-10-04, a day in October
"04-10-2026" "%m-%d-%Y" 2026-04-10, a day in April
"31-04-2026" "%d-%m-%Y" ValueError: April has 30 days
"2026-10-04" "%d-%m-%Y" ValueError: the text does not match the format
"29-02" "%d-%m" ValueError: without a year it assumes 1900, not a leap year
  • %d stands for the day, %m for the month’s number, %Y for the four-digit year, %B for the month’s name, %a for a short weekday and %H:%M for a 24-hour time. The codes, not the text, decide what each number means.
  • date.strptime(), new in Python 3.14, returns a date straight away, and time.strptime() a time; on older versions, call datetime.strptime() and then .date().
  • Parsing is strict about separators and about real dates, but it accepts days, months and hours written with one digit.
  • Since Python 3.13 a format with a day but no year also raises DeprecationWarning, and the documentation warns that it may become an error: always parse a year.
  • fromisoformat() reads ISO 8601: The international standard for writing dates and times year first, then month, day and time, with an offset such as +05:30 or Z for UTC. isoformat() writes it and fromisoformat() reads it. text with its offset, such as +05:30, and since Python 3.11 also the Z that means UTC.

Checkpoint

Check: Formatting and parsing text

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    from datetime import datetime
    print(datetime.strptime("03-04-2027", "%d-%m-%Y").month)
    Show the answer: Question 1 of 2

    Answer: 4

    The codes, not the text, decide what each number means. With %d-%m-%Y the text is the 3rd day of the 4th month; with %m-%d-%Y the same text would be read month first, as the 3rd month.

  2. Question 2 of 2 Why does datetime.strptime("29-02", "%d-%m") raise ValueError?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Without a year, strptime() assumes 1900, which was not a leap year

    A format with no year gets 1900, which was not a leap year, so 29-02 cannot be read. Since Python 3.13 such a format also warns, and the documentation says it may become an error: always parse a year.

Only an aware datetime names one moment

A naive datetime is a clock reading; an aware one adds the zone, and with it the moment. This program attaches a zone, converts, makes the most common mistake on purpose and compares the results:

Attaching a zone, converting, comparing Python · aware_naive.py
import warnings
from datetime import UTC, datetime, timedelta, timezone

naive = datetime(2026, 10, 4, 9, 30)
print(naive, "|", naive.tzinfo, naive.utcoffset())

ist = timezone(timedelta(hours=5, minutes=30), "IST")
aware = naive.replace(tzinfo=ist)  # attach a zone: same wall time
print(aware, "|", aware.tzname(), aware.utcoffset())
print(aware.astimezone(UTC))       # convert: same moment, other clock
wrong = aware.replace(tzinfo=UTC)  # the bug: same wall time, another moment
print(wrong, "|", wrong.astimezone(ist))

print(naive == aware, aware == aware.astimezone(UTC))  # never equal; the same moment
try:
    naive < aware
except TypeError as error:
    print("TypeError:", error)

# The current time: datetime.now(UTC), not the deprecated utcnow()
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    old = datetime.utcnow()
print(old.tzinfo, "|", caught[0].category.__name__)
print(datetime.now(UTC).tzinfo, datetime.now(ist).utcoffset())

Output

2026-10-04 09:30:00 | None None
2026-10-04 09:30:00+05:30 | IST 5:30:00
2026-10-04 04:00:00+00:00
2026-10-04 09:30:00+00:00 | 2026-10-04 15:00:00+05:30
False True
TypeError: can't compare offset-naive and offset-aware datetimes
None | DeprecationWarning
UTC 5:30:00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aware_naive.py

Step through the first four lines of the output: what each call keeps, and what it changes.

Interactive · step by step Attach a zone, or convert to one Step through one clock reading. replace() keeps the reading and changes the moment it names; astimezone() keeps the moment and changes the reading.
Attach a zone, or convert to one, step 6 of 6. Showing 09:30, no zone, 09:30+05:30, 04:00+00:00, 09:30+00:00, 15:00+05:30. In focus: 09:30+05:30, 04:00+00:00.09:30, no zonenaive: any clock09:30+05:30replace(tzinfo=ist)04:00+00:00astimezone(UTC)09:30+00:00replace(tzinfo=UTC)15:00+05:30astimezone(ist)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A naive reading. A naive datetime is a clock reading with no zone. 09:30 could be on a clock in Kolkata, in London or anywhere else, and Python cannot tell which.
  2. Attach India's zone. replace(tzinfo=ist) says which clock the reading belongs to and keeps it as it was. The value is now aware, 09:30 in India, one moment for the whole world.
  3. Convert to UTC. astimezone(UTC) keeps the moment and changes the clock, so 09:30 in India is 04:00 in UTC. Compared with ==, the two values are equal.
  4. The common bug. replace(tzinfo=UTC) on the Indian time keeps the reading, 09:30, but now claims it is UTC. The reading looks the same; the moment is five and a half hours later.
  5. Seen in India. Converted back to India's clock, the wrong value reads 15:00. A call booked for 09:30 would now appear in the afternoon.
  6. The rule. Use replace(tzinfo=…) once, to say which clock a naive reading came from, and astimezone() for every conversion after that.

  • A naive datetime has tzinfo set to None: 09:30 could be on any clock in the world. When Python converts a naive value, in timestamp() or astimezone(), it assumes the computer’s local time, so the same code gives different answers on a laptop in India and on a server in Europe.
  • replace(tzinfo=…) attaches a zone and keeps the clock reading; astimezone() converts to another zone and keeps the moment. Using one where the other was meant is the most common time-zone bug, the fourth line of the output.
  • A fixed offset, timezone(timedelta(hours=5, minutes=30), "IST"), is all India needs for current dates. Zones with daylight saving time need zoneinfo, in the next section.
  • A naive and an aware datetime are never equal, and comparing them with < raises TypeError, as does subtracting one from the other. Two aware values compare by their moment: 09:30 in India equals 04:00 in UTC.
  • datetime.utcnow() returns the current UTC time as a naive value, and it has been deprecated since Python 3.12. Use datetime.now(UTC) (UTC is an alias of timezone.utc, since 3.11), or datetime.now(ZoneInfo("Asia/Kolkata")) for the time in India.

Calling a time UTC instead of converting it

aware.replace(tzinfo=UTC) on a time in India keeps 09:30 and claims it is UTC, which is 15:00 in India. Attach the zone the reading came from once, naive.replace(tzinfo=ist), and convert with astimezone(UTC), which gives 04:00.

Checkpoint

Check: Naive and aware datetimes

  1. Question 1 of 2 naive is a clock reading typed in India, 09:30, with no zone. Which expression gives the same moment in UTC?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: naive.replace(tzinfo=ist).astimezone(UTC)

    Attach the zone the reading came from with replace(), once, then convert with astimezone(). aware_naive.py shows the right value, 04:00+00:00, and the bug, 09:30+00:00, which is 15:00 in India.

  2. Question 2 of 2 What happens when a naive and an aware datetime are compared with <?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It raises TypeError

    A naive and an aware datetime are never equal, and ordering them with < raises TypeError, as does subtracting one from the other: can't compare offset-naive and offset-aware datetimes.

zoneinfo knows when each region changes its clocks

zoneinfo.ZoneInfo(key) loads a zone from the IANA time zone database: The public record of the clock rules of every region, past and present, with keys such as Asia/Kolkata and Europe/London. zoneinfo reads it from the operating system or from the tzdata package., which records the offsets and the daylight-saving rules of places around the world, past and present. Keys name regions, never abbreviations: Asia/Kolkata, Europe/London, America/New_York. Here the weekly call at 19:30 in India meets two zones that change their clocks:

One meeting time on three clocks Python · meeting_times.py
from datetime import datetime
from zoneinfo import ZoneInfo

KOLKATA = ZoneInfo("Asia/Kolkata")
OTHERS = {"London": ZoneInfo("Europe/London"), "New York": ZoneInfo("America/New_York")}

# A weekly call at 19:30 in India, on five Mondays around the clock changes
for day in (2, 9, 16, 23, 30):
    call = datetime(2026, 3, day, 19, 30, tzinfo=KOLKATA)
    local = [f"{city} {call.astimezone(zone):%H:%M %Z}" for city, zone in OTHERS.items()]
    print(f"{call:%a %d %b %H:%M %Z} | " + " | ".join(local))

# India keeps one offset all year, with no daylight saving time
for month in (1, 7):
    moment = datetime(2026, month, 15, 12, tzinfo=KOLKATA)
    print(f"{moment:%b}", moment.utcoffset(), moment.dst(), moment.tzname())

# A short name is not a zone: "IST" means three different offsets
for key, month in (("Asia/Kolkata", 1), ("Europe/Dublin", 7), ("Asia/Jerusalem", 1)):
    moment = datetime(2026, month, 15, 12, tzinfo=ZoneInfo(key))
    print(f"{key:15} {moment.tzname()} {moment.utcoffset()}")

Output

Mon 02 Mar 19:30 IST | London 14:00 GMT | New York 09:00 EST
Mon 09 Mar 19:30 IST | London 14:00 GMT | New York 10:00 EDT
Mon 16 Mar 19:30 IST | London 14:00 GMT | New York 10:00 EDT
Mon 23 Mar 19:30 IST | London 14:00 GMT | New York 10:00 EDT
Mon 30 Mar 19:30 IST | London 15:00 BST | New York 10:00 EDT
Jan 5:30:00 0:00:00 IST
Jul 5:30:00 0:00:00 IST
Asia/Kolkata    IST 5:30:00
Europe/Dublin   IST 1:00:00
Asia/Jerusalem  IST 2:00:00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 meeting_times.py

Where the zone data comes from

zoneinfo reads the operating system’s copy of the database. Windows has none, and neither has Python in the browser, where ZoneInfo() raises ZoneInfoNotFoundError; installing the tzdata package from PyPI fixes both, and projects that must run everywhere should depend on it. That is also why the examples that use zoneinfo show their recorded output here instead of a Run button.

The United States starts its daylight saving time: Moving the clocks forward by an hour in spring and back in autumn, as the United Kingdom and the United States do. India does not use it, so Asia/Kolkata keeps +05:30 all year., moving its clocks forward an hour, on the second Sunday of March, and the United Kingdom on the last Sunday, so for two or three weeks, depending on the year, New York is four hours behind London instead of five. In 2026, the year of the program, it is three. Step through the five Mondays on the three clocks:

Interactive · step by step A call at 19:30 in India, on three clocks Step through the five Mondays of March in 2026, the year of the program. The call never moves in Kolkata; watch when it moves in New York and in London.
A call at 19:30 in India, on three clocks, step 5 of 5. Showing Kolkata, London (15:00 BST, +01:00), New York (10:00 EDT, −04:00). In focus: Kolkata.Kolkata19:30 IST, +05:30London15:00 BST, +01:00New York10:00 EDT, −04:00

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first Monday. The call is fixed at 19:30 on the Indian clock. On the first Monday of March, London is on GMT and New York on EST, so the call is at 14:00 and at 09:00 there.
  2. New York moves first. The United States moves its clocks forward on the second Sunday of March. On the next Monday New York is on EDT, four hours behind UTC, and the call is at 10:00 there.
  3. Three weeks apart. For the next three Mondays New York is four hours behind London instead of five. Nothing has changed in Kolkata or in London.
  4. London moves. The United Kingdom moves its clocks forward on the last Sunday of March. On the last Monday London is on BST, one hour ahead of UTC, and the call is at 15:00 there.
  5. India never moved. In Kolkata the call stayed at 19:30 on all five Mondays, because Asia/Kolkata is +05:30 all year, with no daylight saving time. Only the other two clocks moved.

The call moves an hour later in New York first and in London three weeks after, while in India it never moves: Asia/Kolkata shows +05:30 and no daylight-saving offset in January and in July alike. The last lines of the program show why you store the key and not the short name: IST stands for India Standard Time, for Irish Standard Time (Dublin in summer) and for Israel Standard Time, three different UTC offset: How far a clock is ahead of or behind UTC, such as +05:30 for India Standard Time. A zone with daylight saving time has two offsets a year: Europe/London is +00:00 in winter and +01:00 in summer..

India Standard Time

IST, the one time zone of all of India, is UTC+05:30 all year, with no daylight saving time. That makes conversions simple and easy to get wrong by half an hour: 14:00 UTC is 19:30 in India, not 19:00, and Nepal’s +05:45 shows that offsets need not be whole hours either. Use the key Asia/Kolkata; the older name Asia/Calcutta still works. Dates are usually written day first, so parse them with %d-%m-%Y and never guess the order. For old data, let the database decide rather than a fixed offset: for a date in 1943, Asia/Kolkata gives +06:30, an offset India used for a few years in the 1940s, as the last line of store_utc.py, at the end of this lesson, shows.

Checkpoint

Check: Time zones with zoneinfo

  1. Question 1 of 2 Why store the zone key Asia/Kolkata rather than the abbreviation IST?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: IST also stands for Irish Standard Time and Israel Standard Time, so it names three offsets

    Keys name regions, never abbreviations. IST is used for India, for Ireland in summer and for Israel in winter, so only a key such as Asia/Kolkata tells zoneinfo which rules to use.

  2. Question 2 of 2 On the Mondays between the US clock change and the UK clock change, how many hours is New York behind London?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4 hours

    New York is on EDT, UTC−4, while London is still on GMT, UTC+0, so it is 4 hours behind instead of 5: the call at 14:00 in London is at 10:00 in New York.

Time Zone Converter Compare a time across cities and zones, daylight saving included, to plan a meeting.

Adding a day across a clock change is not 24 hours

London’s clocks go forward on the last Sunday of March and back on the last Sunday of October. This program meets both nights:

A missing hour, a repeated hour, and 24 hours that are not a day Python · dst_traps.py
from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo

LONDON = ZoneInfo("Europe/London")

# 1. Adding a day to an aware datetime keeps the wall clock, not 24 hours
before = datetime(2026, 3, 28, 12, 0, tzinfo=LONDON)
after = before + timedelta(days=1)
print(after, "|", after - before, "|", after.astimezone(UTC) - before.astimezone(UTC))

# 2. For elapsed time, do the arithmetic in UTC
print((before.astimezone(UTC) + timedelta(hours=24)).astimezone(LONDON))

# 3. In autumn, 01:30 happens twice: fold picks the first or the second
first = datetime(2026, 10, 25, 1, 30, tzinfo=LONDON)
second = first.replace(fold=1)
print(first, first.tzname(), "|", second, second.tzname())
print(second.astimezone(UTC) - first.astimezone(UTC))

# 4. In spring, 01:30 never happens: the round trip moves it
ghost = datetime(2026, 3, 29, 1, 30, tzinfo=LONDON)
print(ghost, "|", ghost.astimezone(UTC).astimezone(LONDON))

Output

2026-03-29 12:00:00+01:00 | 1 day, 0:00:00 | 23:00:00
2026-03-29 13:00:00+01:00
2026-10-25 01:30:00+01:00 BST | 2026-10-25 01:30:00+00:00 GMT
1:00:00
2026-03-29 01:30:00+00:00 | 2026-03-29 02:30:00+01:00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dst_traps.py

Step through the spring night on a London clock and in UTC side by side; each row of the figure is one moment:

Interactive · step by step The night the clocks go forward in London Step through the weekend. The left column is a London clock, the right one UTC; each row is one moment.
The night the clocks go forward in London, step 5 of 5. Showing Sat 12:00 GMT, 12:00 UTC, 01:00 → 02:00 BST, 01:00 UTC, Sun 12:00 BST, 11:00 UTC, Sun 13:00 BST, 12:00 UTC. In focus: 01:00 → 02:00 BST.same momentSat 12:00 GMTbefore, +00:0012:00 UTC01:00 → 02:00 BSTthe clocks go forward01:00 UTCSun 12:00 BSTbefore + 1 day11:00 UTC23 hours laterSun 13:00 BSTUTC + 24 hours12:00 UTC24 hours later01:30 falls in the gap

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Saturday noon. Noon in London on the Saturday before the clocks go forward is 12:00 GMT, the same moment as 12:00 UTC, because in winter London's offset is zero.
  2. The clocks jump. At 01:00 GMT on Sunday the clocks jump to 02:00 BST, one hour ahead of UTC. On a London clock the hour from 01:00 to 02:00 never happens.
  3. Adding a day. before + timedelta(days=1) moves the wall clock to Sunday noon, 12:00 BST, which is 11:00 UTC. Subtracting the two says 1 day, but only 23 hours passed.
  4. 24 real hours. For 24 real hours, convert to UTC, add timedelta(hours=24) and convert back. That gives 12:00 UTC on Sunday, which a London clock shows as 13:00 BST.
  5. A time that never happens. 01:30 on that Sunday falls in the gap. zoneinfo accepts it anyway, with the offset from before the gap, and a round trip through UTC turns it into 02:30 BST.

  1. Adding timedelta(days=1) to an aware datetime moves the wall clock: noon on the day before the clocks go forward becomes noon the next day, and after - before says one day, but only 23 hours passed, as the subtraction in UTC shows. Python adjusts nothing for the time zone when it adds a timedelta, or when it subtracts two datetimes that share a tzinfo.
  2. For elapsed time (“in 24 hours”, a token that lives for a day, a timeout), convert to UTC, add, and convert back: 13:00 in London, an hour later on the clock than noon.
  3. When the clocks go back, 01:30 happens twice. The fold (datetime): The datetime attribute that picks one of two moments when a clock reading happens twice, as when the clocks go back: fold=0, the default, is the first, and fold=1 the second. attribute picks one: fold=0, the default, is the first, still in summer time (BST, +01:00), and fold=1 the second (GMT, +00:00), an hour later.
  4. When the clocks go forward, 01:30 never happens. zoneinfo does not complain; it uses the offset from before the gap, and a round trip through UTC turns the time into 02:30 BST. Decide what such input should mean, and test it.

Key idea

Adding a timedelta to an aware datetime keeps the wall clock; for elapsed time, add in UTC and convert back, and pick a repeated hour with fold.

Checkpoint

Check: Daylight-saving traps

  1. Question 1 of 2 A token is issued at 12:00 London time on the Saturday before the clocks go forward and must expire exactly 24 hours later. What does a London clock show when it expires?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 13:00 BST on Sunday

    For elapsed time, convert to UTC, add, and convert back: dst_traps.py prints 13:00+01:00. Adding a day to the London time keeps the wall clock and gives only 23 hours.

  2. Question 2 of 2 In London, 01:30 happens twice on the night the clocks go back. Which setting picks the second 01:30, on GMT?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fold=1

    When the clocks go back, one reading names two moments. fold picks one: 0 for the first, BST, and 1 for the second, GMT, as dst_traps.py shows, and the two are 1:00:00 apart.

When Do the Clocks Change? (Daylight Saving Time Dates) See when the clocks change in any country and year, with the local times and UTC offsets.

Store UTC, show local

A booking made in India is stored once and read in many places. This program stores it in UTC, reads it back and shows it on two clocks:

Store UTC, show local Python · store_utc.py
from datetime import UTC, datetime
from zoneinfo import ZoneInfo

KOLKATA = ZoneInfo("Asia/Kolkata")

booked = datetime(2026, 10, 4, 9, 30, tzinfo=KOLKATA)
stored = booked.astimezone(UTC).isoformat()  # what goes into the database
print(stored)

back = datetime.fromisoformat(stored)
print(back == booked, "|", back.astimezone(KOLKATA), "|", back.astimezone(ZoneInfo("Europe/London")))

stamp = booked.timestamp()  # seconds since 1970-01-01 00:00 UTC
print(stamp, "|", datetime.fromtimestamp(stamp, UTC))

# Old data: the database knows that India's offset was not always +05:30
print(datetime(1943, 6, 1, 12, tzinfo=KOLKATA).utcoffset())

Output

2026-10-04T04:00:00+00:00
True | 2026-10-04 09:30:00+05:30 | 2026-10-04 05:00:00+01:00
1791086400.0 | 2026-10-04 04:00:00+00:00
6:30:00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 store_utc.py

The whole path of a time typed in India, from the text to each reader’s clock:

Text typed in India is parsed, given the Asia/Kolkata zone, converted to UTC, stored as ISO 8601 and shown in each reader zone.Text typed in IndiaDD-MM-YYYY HH:MMNaive datetimeno time zone yetAware datetimeAsia/Kolkata, +05:30Aware datetimein UTC, +00:00Stored ISO 8601 text…T04:00:00+00:00Shown on eachreader's own clockstrptime()replace(tzinfo=…)astimezone(UTC)isoformat()fromisoformat(),then astimezone(zone)

Parse, attach the zone, store UTC, convert to show

Text description of the diagram

The diagram follows one time typed in India from top to bottom.

  1. The text, in the form DD-MM-YYYY HH:MM, is parsed with datetime.strptime(), which gives a naive datetime: a date and a clock reading without a time zone.
  2. replace(tzinfo=ZoneInfo("Asia/Kolkata")) says which clock the reading belongs to, without changing it. The datetime is now aware, at +05:30.
  3. astimezone(UTC) converts it to the same moment in UTC, at +00:00; a reading of 09:30 in India becomes 04:00 in UTC.
  4. isoformat() writes it as ISO 8601 text ending in +00:00, which is what the database keeps.
  5. To show it, datetime.fromisoformat() reads the text back, and astimezone() converts it to each reader's own zone.
  • Convert to UTC before storing, and write ISO 8601 with its offset: 2026-10-04T04:00:00+00:00 sorts correctly as text, names exactly one moment and reads back with fromisoformat() into an aware datetime equal to the original. Each reader then sees it on their own clock, 09:30 in India or 05:00 in London.
  • A POSIX timestamp, the number of seconds since the start of 1970 in UTC, is the other common form, and datetime.fromtimestamp(stamp, UTC) turns it back into an aware datetime. Without the zone argument, fromtimestamp() returns the computer’s local time as a naive value.
  • To measure how long something takes, use time.monotonic() or time.perf_counter() rather than two readings of datetime.now(): the wall clock can jump when the computer sets it, and a monotonic clock never goes backwards.

Checkpoint

Check: Storing and exchanging times

  1. Question 1 of 2 A booking is made for 09:30 in India. Which text should the database keep?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ISO 8601 text in UTC, ending in T04:00:00+00:00

    Convert to UTC before storing and write ISO 8601 with its offset, as store_utc.py does. Each reader then sees the moment on their own clock: 09:30 in India, 05:00 in London.

  2. Question 2 of 2 Which clock should a program read to measure how long a download took?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: time.monotonic(), read before and after

    To measure a duration, use time.monotonic() or time.perf_counter(): they only count up, while the wall clock can be set back or forward in the middle of the measurement.

Timestamp Converter Turn Unix timestamps into dates in any time zone, IST included, and dates back into timestamps.

Practise: IST to UTC and working days

Convert times typed in India to UTC, then count the working days between two dates with a list of holidays, across the ends of months and years.

Exercise · Medium · Python

Convert IST times to UTC and count working days

Write two functions in when.py.

ist_to_utc(text) reads a date and time typed in India in the form DD-MM-YYYY HH:MM, such as "04-10-2026 09:30", and returns the same moment in UTC as ISO 8601 text, "2026-10-04T04:00:00+00:00". Text in any other form must raise ValueError, which strptime() does for you. India Standard Time is UTC+05:30 all year, with no daylight saving time, so IST in the starter code, a fixed offset of 5 hours 30 minutes, is right for any date after 1945. Use it rather than ZoneInfo("Asia/Kolkata"): Python in the browser has no time-zone database, so zoneinfo raises ZoneInfoNotFoundError there.

business_days_between(start, end, holidays) counts the working days from start up to, but not including, end: the days from Monday to Friday that are not in holidays, a collection of date objects. When end is not after start, the answer is 0.

  • ist_to_utc("01-01-2027 03:00") is "2026-12-31T21:30:00+00:00": the moment falls in the previous year in UTC
  • business_days_between(date(2026, 10, 1), date(2026, 10, 8), []) is 5
  • the same week with date(2026, 10, 2) as a holiday has 4 working days

Starter code · when.py

from datetime import UTC, date, datetime, timedelta, timezone

IST = timezone(timedelta(hours=5, minutes=30), "IST")  # India Standard Time: UTC+05:30 all year


def ist_to_utc(text):
    """Turn 'DD-MM-YYYY HH:MM' in Indian time into ISO 8601 text in UTC."""
    # Replace this line with your code.
    return text


def business_days_between(start, end, holidays):
    """Count Monday-to-Friday days from start up to end (not included) that are not holidays."""
    # Replace this line with your code.
    return 0
The sample tests · test_when.py
from datetime import date

from when import business_days_between, ist_to_utc


def test_morning_in_india():
    """converts a morning in India to UTC"""
    assert ist_to_utc("04-10-2026 09:30") == "2026-10-04T04:00:00+00:00"


def test_previous_day_in_utc():
    """moves times before 05:30 in India to the previous day, month and year in UTC"""
    assert ist_to_utc("01-01-2027 03:00") == "2026-12-31T21:30:00+00:00"
    assert ist_to_utc("01-03-2028 05:29") == "2028-02-29T23:59:00+00:00"


def test_late_evening():
    """keeps a late evening on the same day in UTC"""
    assert ist_to_utc("31-03-2026 23:45") == "2026-03-31T18:15:00+00:00"


def test_rejects_other_forms():
    """raises ValueError for text that is not DD-MM-YYYY HH:MM"""
    for text in ["2026-10-04 09:30", "31-04-2026 10:00", "04-10-2026"]:
        try:
            ist_to_utc(text)
        except ValueError:
            continue
        assert False, f"expected a ValueError for {text!r}"


def test_one_week():
    """counts five working days in a week from Thursday to Thursday"""
    assert business_days_between(date(2026, 10, 1), date(2026, 10, 8), []) == 5


def test_holidays():
    """skips holidays, and holidays at weekends change nothing"""
    week = (date(2026, 10, 1), date(2026, 10, 8))
    assert business_days_between(*week, [date(2026, 10, 2)]) == 4
    assert business_days_between(*week, {date(2026, 10, 3), date(2026, 10, 4)}) == 5


def test_across_month_and_year_ends():
    """counts across the end of a month and of a year"""
    assert business_days_between(date(2026, 1, 30), date(2026, 2, 3), []) == 2
    assert business_days_between(date(2026, 12, 24), date(2027, 1, 4), [date(2026, 12, 25), date(2027, 1, 1)]) == 5


def test_empty_ranges():
    """returns 0 when the end is not after the start"""
    assert business_days_between(date(2026, 10, 5), date(2026, 10, 5), []) == 0
    assert business_days_between(date(2026, 10, 9), date(2026, 10, 5), []) == 0
A hint

datetime.strptime(text, "%d-%m-%Y %H:%M") gives a naive datetime. .replace(tzinfo=IST) says it is Indian time without changing the clock reading, .astimezone(UTC) converts it, and .isoformat() writes the text. For the working days, start with day = start, step with day += timedelta(days=1) while day < end, and count the days whose weekday() is below 5 (Monday is 0, Saturday 5) and that are not in holidays.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test yourself on the seven ideas of this lesson. The first question comes from the idea you have answered least well so far, a right answer brings a harder one, and every choice is explained the moment you pick it:

Practice that adapts to you

Practise this lesson

19 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 19 What does date(2027, 2, 29) do?

    Choose one answer.

    Show the answer: Question 1 of 19

    Answer: It raises ValueError, because 2027 is not a leap year

    A date is checked when it is made: date(2027, 2, 29) raises ValueError with the message that day 29 must be in range 1..28 for month 2 in year 2027.

  2. Question 2 of 19 A list of dates kept as text must sort into time order with plain sorted(). Which form of text does that?

    Choose one answer.

    Show the answer: Question 2 of 19

    Answer: Year first with fixed widths, as isoformat() writes it

    isoformat() writes the year first and pads every part to a fixed width, so sorting the text sorts the dates. It also reads back with fromisoformat().

  3. Question 3 of 19 Why does timedelta have no months argument?

    Choose one answer.

    Show the answer: Question 3 of 19

    Answer: A month has 28 to 31 days, so it is not one length of time

    A timedelta keeps only days, seconds and microseconds. Stepping by months means changing the month number and choosing a day that exists, as add_months() does with calendar.monthrange().

  4. Question 4 of 19 What does (timedelta(hours=2, minutes=45) * 30) / timedelta(hours=1) give?

    Type a number.

    A hint

    Dividing a timedelta by another one asks how many of the second fit into the first.

    Show the answer: Question 4 of 19

    Answer: 82.5 hours

    30 sessions of 2 hours 45 minutes are 82 hours 30 minutes, printed as 3 days, 10:30:00, and dividing by one hour gives 82.5, as days_until.py prints.

  5. Question 5 of 19 What does this print?

    Read the code, then choose one answer.

    from datetime import datetime
    print(datetime.strptime("04-10-2026", "%m-%d-%Y").date())
    Show the answer: Question 5 of 19

    Answer: 2026-04-10

    %m-%d-%Y reads the first number as the month, so the text means the tenth of April, and .date() prints in ISO order, year first. Nothing in the text itself says which order was meant, so the format must say it: %d-%m-%Y for the day-first form used in India and much of the world.

  6. Question 6 of 19 Which call, new in Python 3.14, reads day-first text straight into a date?

    Choose one answer.

    Show the answer: Question 6 of 19

    Answer: date.strptime(text, "%d-%m-%Y")

    Python 3.14 added date.strptime(), which returns a date, and time.strptime() in the datetime module, which returns a time. parse_dates.py uses date.strptime(text, "%d-%m-%Y").

  7. Question 7 of 19 Since Python 3.11, which ending does datetime.fromisoformat() accept that older versions refused?

    Choose one answer.

    Show the answer: Question 7 of 19

    Answer: Z, which means UTC

    fromisoformat() reads ISO 8601 text with its offset, such as +05:30, and since Python 3.11 also the Z that means UTC, so 04:00:00Z reads as 04:00:00+00:00.

  8. Question 8 of 19 Which of these is an aware datetime?

    Choose one answer.

    Show the answer: Question 8 of 19

    Answer: datetime(2026, 10, 4, 9, 30, tzinfo=ZoneInfo("Asia/Kolkata"))

    A datetime is aware when it carries a time zone that gives an offset from UTC. A plain constructor call, utcnow() and a format without a zone code all return naive datetimes; utcnow() reads the time in UTC but does not say so, which is why it is deprecated.

  9. Question 9 of 19 Which call replaces the deprecated datetime.utcnow()?

    Choose one answer.

    Show the answer: Question 9 of 19

    Answer: datetime.now(UTC)

    utcnow() has been deprecated since Python 3.12. datetime.now(UTC), with UTC the alias of timezone.utc since 3.11, gives an aware value; datetime.now(ZoneInfo("Asia/Kolkata")) gives the time in India.

  10. Question 10 of 19 Code that calls timestamp() on naive datetimes gives right answers on a laptop in India and wrong ones on a server in Europe. Why?

    Choose one answer.

    Show the answer: Question 10 of 19

    Answer: timestamp() treats a naive value as the computer's local time, which differs between the two machines

    When Python must convert a naive value, in timestamp() or astimezone(), it assumes the computer's local time. Make values aware as they enter the program, and the answer no longer depends on the machine.

  11. Question 11 of 19 Which of these is a key that zoneinfo.ZoneInfo() accepts?

    Choose one answer.

    Show the answer: Question 11 of 19

    Answer: "Asia/Kolkata"

    ZoneInfo() loads a zone by its IANA key. The older name Asia/Calcutta still works for India, but abbreviations such as IST and bare offsets are not keys.

  12. Question 12 of 19 A call is at 14:00 UTC. What is the hour on the clock in India (Asia/Kolkata)? Give the time as a decimal number of hours, so that 19:15 would be 19.25.

    Type a number.

    Show the answer: Question 12 of 19

    Answer: 19.5 hours

    India Standard Time is UTC+05:30 all year, so 14:00 UTC is 19:30 in India: 19.5 hours on the clock. Forgetting the half hour, a common mistake, gives 19:00.

  13. Question 13 of 19 Nepal is UTC+05:45. What is 14:00 UTC on a clock in Kathmandu? Give it as a decimal number of hours, so that 19:15 would be 19.25.

    Type a number.

    Show the answer: Question 13 of 19

    Answer: 19.75 hours

    14:00 plus 5 hours 45 minutes is 19:45, which is 19.75 hours. Offsets need not be whole hours, or even half hours.

  14. Question 14 of 19 before is noon in Europe/London on the day before the clocks go forward. How much real time passes between before and before + timedelta(days=1)?

    Choose one answer.

    Show the answer: Question 14 of 19

    Answer: 23 hours

    Adding a timedelta to an aware datetime keeps its tzinfo and moves the wall clock, so the result is noon the next day. Clocks went forward an hour overnight, so only 23 hours passed, which you see by subtracting the two after converting them to UTC.

  15. Question 15 of 19 A timeout must fire exactly 24 hours after t, an aware datetime in Europe/London. Which expression is right on every day of the year?

    Choose one answer.

    Show the answer: Question 15 of 19

    Answer: (t.astimezone(UTC) + timedelta(hours=24)).astimezone(t.tzinfo)

    Python adjusts nothing for the zone when it adds a timedelta to an aware datetime. For elapsed time, convert to UTC, add and convert back: dst_traps.py gives 13:00 BST, 24 hours after Saturday noon.

  16. Question 16 of 19 What does zoneinfo do with 01:30 in London on the night the clocks go forward, a time that never happens there?

    Choose one answer.

    Show the answer: Question 16 of 19

    Answer: It accepts it with the offset from before the gap, and a round trip through UTC turns it into 02:30 BST

    A time in the gap is accepted silently with the offset from before the gap. Because nothing warns you, decide what such input should mean in your program and write a test for it.

  17. Question 17 of 19 A booking system serves users in India, the UK and the US. What should it store for each booking's time?

    Choose one answer.

    Show the answer: Question 17 of 19

    Answer: An aware datetime in UTC, for example ISO 8601 text ending in +00:00

    A UTC value names one moment that every other zone can be computed from. A naive time says nothing about its zone, and three-letter names are ambiguous (IST is also used for Irish and Israel time). Keep the user's zone name, such as Asia/Kolkata, separately if you need it for display.

  18. Question 18 of 19 What does datetime.fromtimestamp(stamp) return when no zone is passed?

    Choose one answer.

    Show the answer: Question 18 of 19

    Answer: The computer's local time, as a naive datetime

    A POSIX timestamp counts seconds since the start of 1970 in UTC. datetime.fromtimestamp(stamp, UTC) turns it back into an aware datetime; without the zone argument the result is naive local time.

  19. Question 19 of 19 A report converts old times recorded in India with a fixed offset of +05:30. Why can it be wrong for a date in 1943?

    Choose one answer.

    Show the answer: Question 19 of 19

    Answer: India used +06:30 for a few years in the 1940s, which the time-zone database records under Asia/Kolkata

    Let the database decide the offset for old data, rather than a fixed number. ZoneInfo("Asia/Kolkata") knows the history of India's clocks, which a fixed timezone(timedelta(hours=5, minutes=30)) does not.

Interview questions

Fresher · What is the difference between a naive and an aware datetime in Python?

A naive datetime has no time zone: its tzinfo is None, so 09:30 could be 09:30 in Kolkata, London or anywhere else, and only the program knows which. An aware datetime carries a tzinfo that gives its offset from UTC, so it names one exact moment. Aware values convert with astimezone() and compare correctly across zones; a naive and an aware value are never equal, and ordering or subtracting them raises TypeError. When Python must convert a naive value, as in timestamp(), it assumes the computer’s local time, which is how bugs appear when code moves to a server elsewhere. Make values aware at the edge of the program, with datetime.now(UTC) or by attaching the right ZoneInfo, and keep them aware.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A reminder app stores the next reminder as a UTC datetime and adds timedelta(hours=24) after sending it. London users who chose 09:00 get it at 10:00 from the last Sunday of March; users in India never complain. Explain both, fix the scheduling, and say what to do when a chosen time does not exist.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A shop in India stores the time of every order in UTC. Its daily sales report runs at 00:05 UTC and adds up the orders of the previous UTC day, and its totals never match the shop's till. Which orders land on the wrong day, how much of each day is affected, and how do you fix the query?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • date, time and datetime check their values when they are made and never change; replace() returns a changed copy.
  • A timedelta holds days, seconds and microseconds and no months, so step months with calendar.monthrange() and min().
  • The format decides what a text means: parse day-first text with %d-%m-%Y, always with a year, and use date.strptime() on Python 3.14.
  • Naive datetimes have no zone: replace(tzinfo=…) attaches one, astimezone() converts, and datetime.now(UTC) replaces the deprecated utcnow().
  • Use ZoneInfo("Asia/Kolkata")-style keys, never abbreviations such as IST, which name three offsets, and install tzdata where the system has no database; adding a timedelta keeps the wall clock, so do elapsed-time arithmetic in UTC and pick repeated hours with fold.
  • Store UTC as ISO 8601 with its offset, show local times, and measure durations with time.monotonic().

Terms in this lesson

aware datetime
A datetime that carries a tzinfo giving its offset from UTC, so it names one exact moment. datetime.now(UTC) and a datetime made with tzinfo=ZoneInfo("Asia/Kolkata") are aware.
daylight saving time
Moving the clocks forward by an hour in spring and back in autumn, as the United Kingdom and the United States do. India does not use it, so Asia/Kolkata keeps +05:30 all year.
fold (datetime)
The datetime attribute that picks one of two moments when a clock reading happens twice, as when the clocks go back: fold=0, the default, is the first, and fold=1 the second.
IANA time zone database
The public record of the clock rules of every region, past and present, with keys such as Asia/Kolkata and Europe/London. zoneinfo reads it from the operating system or from the tzdata package.
ISO 8601
The international standard for writing dates and times year first, then month, day and time, with an offset such as +05:30 or Z for UTC. isoformat() writes it and fromisoformat() reads it.
naive datetime
A datetime with no time zone: its tzinfo is None, so it is a clock reading that could belong to any place. When Python must convert one, it assumes the computer's local time.
UTC offset
How far a clock is ahead of or behind UTC, such as +05:30 for India Standard Time. A zone with daylight saving time has two offsets a year: Europe/London is +00:00 in winter and +01:00 in summer.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.