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.

From a coordinate and a UTC timestamp to local time. coordinate to local timelat, lonJFK 40.64N 73.78Wtimezonefinderoffline polygonsIANA nameAmerica/New_Yorkzoneinforules for thatinstantlocal time12:00 UTC = 08:00EDTStore the zone name, not the offset spatialworkflow.io
Coordinate to local time.

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.

World map of large airports coloured by how far their January clock time is ahead of or behind solar time from longitude: most are ahead, in red, especially in western Europe, Spain, western China and Argentina, with few behind. World map of large airports coloured by how far their January clock time is ahead of or behind solar time from longitude: most are ahead, in red, especially in western Europe, Spain, western China and Argentina, with few behind.
Clock time against solar time.

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.

Scatter of each airport's legal January UTC offset against its longitude with the line longitude over 15: offsets form steps around the line, red half- and quarter-hour zones in South Asia, Iran and Australia, and a cluster at plus 13 hours at 175 degrees west for Tonga. Scatter of each airport's legal January UTC offset against its longitude with the line longitude over 15: offsets form steps around the line, red half- and quarter-hour zones in South Asia, Iran and Australia, and a cluster at plus 13 hours at 175 degrees west for Tonga.
Legal offsets against longitude.

What an IANA time zone is

Large airports observing daylight saving time in 2024 by continent. airports changing clocks in 2024, by continentEurope235North America119Asia23Africa18Oceania12South America10050100150200250417 of 1,172 large airports (36%); 75 use half- or quarter-hour offsets spatialworkflow.io
Daylight saving by continent.

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

Checks when assigning time zones to coordinates. before converting to local timezone name, not offsetoffsets change with DSTnot longitude / 1512% of airports over 1 h offEtc/GMT signs invertedEtc/GMT+3 = UTC-3convert from UTClocal times can repeatcheck near bordersboundary precision304 IANA zones among 1,172 airports spatialworkflow.io
Before converting to local time.
  • Etc/GMT signs: Etc/GMT+5 means UTC−5; the POSIX convention inverts the sign.
  • Boundary data versions: zones are created and changed; update timezonefinder with 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: zoneinfo applies historical rules automatically when you convert a past instant.

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.