Time Zones (tz_localize & tz_convert)
Understand naive vs time-zone-aware datetimes, attach a zone with tz_localize, change it with tz_convert, and handle IST, UTC and UAE match times correctly.
IPL 2020 was played in the UAE. A match that started at 19:30 IST on Indian TV started at 18:00 local time in Dubai. Servers and APIs usually store times in UTC. If you mix these without labelling them, every "time of day" analysis is off by hours. pandas solves this with time-zone-aware datetimes and two methods from Munna's notebook 09: tz_localize() (attach a zone) and tz_convert() (change the zone).
Definition
A naive datetime has no time-zone information —
2020-10-10 19:30could be in any country. A time-zone-aware datetime carries a zone (2020-10-10 19:30:00+05:30) and therefore marks one exact instant worldwide.tz_localize(tz)declares which zone naive times are in (the clock values stay the same), andtz_convert(tz)translates aware times into another zone (the instant stays the same, the clock values change).
- No time-zone information — the default for anything you parse from plain text.
- Exact instant — 19:30 in India and 14:00 in UTC on the same date are the same moment.
- Declares vs translates — localize = "these clock times were Indian times"; convert = "what did the clock show in Dubai at that moment?"
text
naive 19:30 ──tz_localize("Asia/Kolkata")──► 19:30+05:30 ──tz_convert("Asia/Dubai")──► 18:00+04:00
(label it, clock unchanged) (same instant, new clock)Syntax
python
pd.date_range(..., tz="Asia/Kolkata") # create aware dates directly
pd.Timestamp("2020-10-10 19:30", tz="Asia/Kolkata")
index.tz_localize(tz) / index.tz_convert(tz) # DatetimeIndex
s.dt.tz_localize(tz) / s.dt.tz_convert(tz) # datetime Series (column)
pd.to_datetime(values, utc=True) # parse straight to UTC
s.dt.tz_localize(None) # remove the zone (back to naive)| Parameter | Meaning | Default |
|---|---|---|
tz | zone name ("Asia/Kolkata", "Asia/Dubai", "UTC", "Europe/London") or a fixed offset string ("+05:30") | required |
ambiguous (localize) | what to do with repeated clock times when clocks go back (DST) | "raise" |
nonexistent (localize) | what to do with skipped clock times when clocks go forward (DST) | "raise" |
utc (to_datetime) | return UTC-aware values | False |
Note: India has no daylight saving, so IST is always UTC +05:30. Zones like
"Europe/London"change between +00:00 and +01:00 during the year — named zones handle this automatically.
Example — creating aware dates (notebook 09)
python
print(pd.date_range(start="2024-01-01", periods=3, freq="D", tz="Asia/Kolkata"))
print(pd.date_range(start="2024-01-01", periods=3, freq="h", tz="UTC"))text
DatetimeIndex(['2024-01-01 00:00:00+05:30', '2024-01-02 00:00:00+05:30',
'2024-01-03 00:00:00+05:30'],
dtype='datetime64[us, Asia/Kolkata]', freq='D')
DatetimeIndex(['2024-01-01 00:00:00+00:00', '2024-01-01 01:00:00+00:00',
'2024-01-01 02:00:00+00:00'],
dtype='datetime64[us, UTC]', freq='h')How it works: the tz= argument makes the dates aware from the start. The dtype now includes the zone, e.g. datetime64[us, Asia/Kolkata], and each value prints its offset (+05:30).
Example — tz_localize: attach a zone
python
dates = pd.date_range("2024-01-01", periods=3)
print(dates)
print(dates.tz_localize("Asia/Kolkata"))text
DatetimeIndex(['2024-01-01', '2024-01-02', '2024-01-03'], dtype='datetime64[us]', freq='D')
DatetimeIndex(['2024-01-01 00:00:00+05:30', '2024-01-02 00:00:00+05:30',
'2024-01-03 00:00:00+05:30'],
dtype='datetime64[us, Asia/Kolkata]', freq=None)The clock values (00:00) do not change — we only declared that they are Indian times.
Example — tz_convert: translate to another zone
python
utc_dates = pd.date_range("2024-01-01", periods=3, tz="UTC")
print(utc_dates.tz_convert("Asia/Kolkata"))text
DatetimeIndex(['2024-01-01 05:30:00+05:30', '2024-01-02 05:30:00+05:30',
'2024-01-03 05:30:00+05:30'],
dtype='datetime64[us, Asia/Kolkata]', freq=None)How it works: midnight UTC is 05:30 in India. The instant is the same; only the displayed clock changes. This is Munna's notebook example.
Example — IPL 2020 in the UAE (on a column)
python
ipl2020 = pd.DataFrame({
"match": ["MI vs CSK", "DC vs KXIP", "KKR vs KXIP"],
"start_ist": pd.to_datetime(["2020-09-19 19:30", "2020-09-20 19:30", "2020-10-10 15:30"]),
})
ipl2020["start_ist"] = ipl2020["start_ist"].dt.tz_localize("Asia/Kolkata")
ipl2020["start_dubai"] = ipl2020["start_ist"].dt.tz_convert("Asia/Dubai")
ipl2020["start_utc"] = ipl2020["start_ist"].dt.tz_convert("UTC")
print(ipl2020[["match", "start_dubai", "start_utc"]])
print(ipl2020.dtypes)text
match start_dubai start_utc
0 MI vs CSK 2020-09-19 18:00:00+04:00 2020-09-19 14:00:00+00:00
1 DC vs KXIP 2020-09-20 18:00:00+04:00 2020-09-20 14:00:00+00:00
2 KKR vs KXIP 2020-10-10 14:00:00+04:00 2020-10-10 10:00:00+00:00
match str
start_ist datetime64[us, Asia/Kolkata]
start_dubai datetime64[us, Asia/Dubai]
start_utc datetime64[us, UTC]
dtype: objectHow it works: for a column use .dt.tz_localize / .dt.tz_convert. Dubai is UTC+04:00, so 19:30 IST = 18:00 Gulf time = 14:00 UTC — three views of one instant.
Example — same instant, equal values
python
a = pd.Timestamp("2020-09-19 19:30", tz="Asia/Kolkata")
b = pd.Timestamp("2020-09-19 18:00", tz="Asia/Dubai")
print(a == b)
print(a - b)text
True
0 days 00:00:00Aware Timestamps compare by instant, so these are equal and their difference is zero.
Example — common errors
python
pd.date_range("2024-01-01", periods=2).tz_convert("UTC")text
TypeError: Cannot convert tz-naive timestamps, use tz_localize to localizepython
pd.date_range("2024-01-01", periods=2, tz="UTC").tz_localize("Asia/Kolkata")text
TypeError: Already tz-aware, use tz_convert to convert.python
pd.Timestamp("2024-03-22", tz="UTC") - pd.Timestamp("2024-03-21")text
TypeError: Cannot subtract tz-naive and tz-aware datetime-like objects.| Error | Fix |
|---|---|
| Cannot convert tz-naive timestamps, use tz_localize | naive data → tz_localize first |
| Already tz-aware, use tz_convert | aware data → tz_convert (or tz_localize(None) to strip) |
| Cannot subtract tz-naive and tz-aware | make both naive or both aware |
Comparison — tz_localize vs tz_convert
tz_localize(tz) | tz_convert(tz) | |
|---|---|---|
| Works on | naive datetimes | aware datetimes |
| Clock values | unchanged | changed |
| Instant in time | becomes defined | unchanged |
| Question it answers | "which zone were these times recorded in?" | "what time was it over there?" |
Best practice
Store and compute in UTC, convert to a local zone only for display: pd.to_datetime(raw, utc=True) when parsing, .dt.tz_convert("Asia/Kolkata") when showing results to Indian users.
Note: India (
+05:30) and the UAE (+04:00) never use daylight saving, so a fixed offset gives the same result as the named zone for them. For countries that do change their clocks (UK, USA, Europe) always use the named zone, such as"Europe/London", so pandas applies summer time correctly.
Interview tip
Q: What is the difference between tz_localize and tz_convert?
A: tz_localize attaches a time zone to naive timestamps without changing the clock time; tz_convert changes an already-aware timestamp to another zone, keeping the same instant but changing the clock time.
Key points to remember
- A naive datetime has no zone; an aware datetime carries one and marks an exact instant.
- tz_localize(tz) attaches a zone to naive times — clock values stay the same.
- tz_convert(tz) changes aware times to another zone — same instant, new clock values.
- For DataFrame columns use s.dt.tz_localize / s.dt.tz_convert; for a DatetimeIndex call them directly.
- date_range(..., tz='UTC') and Timestamp(..., tz=...) create aware values directly.
- Mixing naive and aware datetimes raises TypeError — make both the same kind.
- IST is always UTC+05:30 (no daylight saving); store in UTC, convert only for display.
Try it yourself
Localize, then convert (IST, UAE, UTC)
UTC dates shown in India
Named time zones (Asia/Kolkata, Asia/Dubai, Europe/London)
Practice: fix the naive/aware mix-up
Revision Flashcards
Tap a card to flip it and see the answer.
