How to Find the Time Zone of a Coordinate in Python
On this page
Problem statement
Timestamps from sensors, GPS units and APIs usually come in UTC, and turning them into local time needs the time zone at each location: not a fixed offset, but an IANA zone name such as America/Denver that knows its daylight saving rules and history. Longitude divided by 15 gives a rough offset and is wrong for a large part of the world, because zone boundaries follow countries, not meridians. The timezonefinder package looks the zone up offline from the coordinates, and Python's zoneinfo does the rest.
This page finds the IANA time zone of every large airport in OurAirports with timezonefinder, compares the real UTC offsets with what longitude alone would suggest, converts UTC times to local time across daylight saving changes, and handles points at sea, where there is no country to inherit a zone from.
Quick answer
Look up the zone name offline, then let zoneinfo convert:
from timezonefinder import TimezoneFinder
from zoneinfo import ZoneInfo
tf = TimezoneFinder()
for code in ("JFK", "DEN", "LHR", "DEL", "KTM", "URC"):
a = AP.loc[code]
tz = tf.timezone_at(lng=a.longitude_deg, lat=a.latitude_deg)
local = pd.Timestamp("2024-07-01 12:00", tz="UTC").tz_convert(ZoneInfo(tz))
print(f"{code}: {tz:<20} 12:00 UTC on 1 July 2024 = {local:%H:%M} local (UTC{local:%z})")
JFK: America/New_York 12:00 UTC on 1 July 2024 = 08:00 local (UTC-0400)
DEN: America/Denver 12:00 UTC on 1 July 2024 = 06:00 local (UTC-0600)
LHR: Europe/London 12:00 UTC on 1 July 2024 = 13:00 local (UTC+0100)
DEL: Asia/Kolkata 12:00 UTC on 1 July 2024 = 17:30 local (UTC+0530)
KTM: Asia/Kathmandu 12:00 UTC on 1 July 2024 = 17:45 local (UTC+0545)
URC: Asia/Urumqi 12:00 UTC on 1 July 2024 = 18:00 local (UTC+0600)
Noon UTC on 1 July 2024 is 08:00 in New York, which is on daylight saving time, and 13:00 in London. Delhi is 5 hours 30 minutes ahead of UTC and Kathmandu 5 hours 45 minutes. Ürümqi's airport resolves to Asia/Urumqi, UTC+6, the zone the IANA database keeps for the region, although official Chinese time there is Beijing time, UTC+8: the database records what the region's clocks commonly show, and the two disagree in Xinjiang.
Every airport at once, offsets against longitude, daylight saving
1. Every airport at once
timezone_at is fast enough to call row by row for thousands of points:
AP["tz"] = [tf.timezone_at(lng=x, lat=y) for x, y in zip(AP.longitude_deg, AP.latitude_deg)]
print(f"airports: {len(AP):,}; distinct zones: {AP.tz.nunique()}; none found: {AP.tz.isna().sum()}")
print("most common:", ", ".join(f"{k} ({v})" for k, v in AP.tz.value_counts().head(5).items()))
airports: 1,172; distinct zones: 304; none found: 0
most common: Asia/Shanghai (67), Asia/Kolkata (43), America/New_York (39), Asia/Tokyo (29), Europe/Rome (25)
Every airport gets a zone, and the 1,172 airports fall in 304 distinct IANA zones. The most common zone is Asia/Shanghai, with 67 airports.
2. Real offsets against longitude
A zone's offset is set by law. Compare each airport's standard-time offset in January with the offset its longitude implies, longitude / 15 hours:
jan = pd.Timestamp("2024-01-15 12:00", tz="UTC")
AP["offset_h"] = [jan.tz_convert(ZoneInfo(z)).utcoffset().total_seconds() / 3600 for z in AP.tz]
AP["solar_h"] = AP.longitude_deg / 15
AP["diff_h"] = AP.offset_h - AP.solar_h
print(f"median |offset - longitude/15|: {AP.diff_h.abs().median():.2f} h; airports more than 1 h off: {(AP.diff_h.abs() > 1).mean():.0%}")
print(AP.reindex(AP.diff_h.abs().nlargest(5).index)[["municipality", "tz", "offset_h", "solar_h"]].round(2).to_string())
median |offset - longitude/15|: 0.35 h; airports more than 1 h off: 12%
municipality tz offset_h solar_h
iata_code
TBU Nuku'alofa Pacific/Tongatapu 13.0 -11.68
VAV Vava'u Island Pacific/Tongatapu 13.0 -11.60
CXI Kiritimati Pacific/Kiritimati 14.0 -10.49
APW Apia Pacific/Apia 13.0 -11.47
WLS Wallis Island Pacific/Wallis 12.0 -11.75
The median airport's legal offset is 0.35 hours from the solar offset its longitude implies, and 12% are more than an hour away. The largest differences, around 24 hours, are in Tonga, Samoa, Kiribati and Wallis: they lie just east of the 180° meridian, at about 175°W, but keep their clocks on the Asian side of the date line, at UTC+12 to UTC+14.
3. Daylight saving
The same zone has different offsets in winter and summer, and some zones have none. Count airports whose offset changes between January and July:
jul = pd.Timestamp("2024-07-15 12:00", tz="UTC")
AP["offset_jul"] = [jul.tz_convert(ZoneInfo(z)).utcoffset().total_seconds() / 3600 for z in AP.tz]
dst = AP.offset_jul != AP.offset_h
print(f"airports observing daylight saving in 2024: {dst.sum()} ({dst.mean():.0%})")
print("by continent:", AP[dst].continent.value_counts().to_dict())
print(f"half-hour or quarter-hour offsets in January: {(AP.offset_h % 1 != 0).sum()}: {sorted(AP[AP.offset_h % 1 != 0].tz.unique())[:8]}")
airports observing daylight saving in 2024: 417 (36%)
by continent: {'EU': 235, 'NA': 119, 'AS': 23, 'AF': 18, 'OC': 12, 'SA': 10}
half-hour or quarter-hour offsets in January: 75: ['America/St_Johns', 'Asia/Colombo', 'Asia/Kabul', 'Asia/Kathmandu', 'Asia/Kolkata', 'Asia/Tehran', 'Asia/Yangon', 'Australia/Adelaide']
417 airports, 36%, change their clocks between January and July 2024, most of them in Europe and North America; most of Asia, Africa and South America does not. 75 airports use offsets that are not whole hours, from St John's in Newfoundland to Kolkata, Kathmandu, Tehran and Adelaide.
Points at sea
The data used on this page
Coordinates of the large airports with an IATA code in OurAirports, a public-domain database. timezonefinder ships its own copy of the timezone-boundary-builder polygons, built from OpenStreetMap; zoneinfo uses the IANA time zone database.
from pathlib import Path
import numpy as np
import pandas as pd
import requests
import timezonefinder
Path("data").mkdir(exist_ok=True)
p = Path("data/airports.csv")
if not p.exists():
r = requests.get("https://davidmegginson.github.io/ourairports-data/airports.csv", timeout=300)
r.raise_for_status()
p.write_bytes(r.content)
AP = pd.read_csv(p, keep_default_na=False, na_values=[""])
AP = AP[(AP.type == "large_airport") & AP.iata_code.notna()].set_index("iata_code")
print(f"{len(AP):,} large airports; timezonefinder {timezonefinder.__version__}")
1,172 large airports; timezonefinder 9.0.0
The data has 1,172 large airports; the lookups use timezonefinder 9.0.0.
Example 1: points at sea
Over the oceans there are no national zones; the boundary data assigns nautical zones such as Etc/GMT+5, whose sign is inverted by POSIX convention:
for lat, lon, where in ((40.0, -50.0, "mid-Atlantic"), (0.0, -160.0, "central Pacific"), (-50.0, 100.0, "Southern Ocean")):
tz = tf.timezone_at(lng=lon, lat=lat)
off = jan.tz_convert(ZoneInfo(tz)).utcoffset().total_seconds() / 3600
print(f"{where:>15} ({lat}, {lon}): {tz}, UTC{off:+.0f}")
mid-Atlantic (40.0, -50.0): Etc/GMT+3, UTC-3
central Pacific (0.0, -160.0): Etc/GMT+11, UTC-11
Southern Ocean (-50.0, 100.0): Etc/GMT-7, UTC+7
At sea the zones are nautical ones 15° wide: the mid-Atlantic point at 50°W is Etc/GMT+3, three hours behind UTC despite the plus sign, and the Southern Ocean point at 100°E is Etc/GMT-7, seven hours ahead.
What an IANA time zone is
A time zone in the IANA database is a region whose clocks have agreed since 1970, with its full history of offsets and daylight saving rules. timezonefinder answers which region contains a point by testing polygons built from OpenStreetMap boundaries, and zoneinfo applies the rules for a given instant. Storing the zone name rather than an offset matters because offsets change: twice a year for daylight saving in many places, and whenever a government changes its rules.
Longitude is a poor guide. Solar time moves an hour every 15°, but countries choose their zones for unity and trade: all of China uses Beijing time across more than 60° of longitude, much of western Europe keeps Central European Time although it lies in the longitudes of Greenwich, and India, Nepal, Iran and parts of Australia use half-hour or quarter-hour offsets. Near borders, a coordinate's zone also depends on the precision of the boundary data; for points within a few hundred metres of a border, check the result.
Etc/GMT signs and ambiguous local times
- Etc/GMT signs:
Etc/GMT+5means UTC−5; the POSIX convention inverts the sign. - Boundary data versions: zones are created and changed; update
timezonefinderwith the IANA database. - Ambiguous local times: during the autumn change an hour repeats; convert from UTC to local, not the other way, whenever possible.
- Historical dates:
zoneinfoapplies historical rules automatically when you convert a past instant.
Internal links
- Fixing timezone errors in spatial time columns — the timestamp side
- How to find the country of a coordinate in Python — the same idea for countries
- Fixing USGS earthquake times that show up as 1970 — UTC timestamps in practice
FAQ
How do I get the time zone from latitude and longitude in Python?
Use TimezoneFinder().timezone_at(lng=lon, lat=lat), which returns an IANA zone name such as Europe/London, then convert times with zoneinfo.ZoneInfo.
Can I compute the time zone from the longitude?
Only roughly: longitude / 15 gives solar time, but legal time zones follow borders; at most airports the two differ, and by several hours in some places.
Does timezonefinder need the internet?
No, it ships its boundary data and works offline.
What does Etc/GMT+5 mean?
A nautical zone five hours behind UTC; the sign is inverted by POSIX convention.