API documentation

Global Functions

timezonefinder.timezone_at(*, lng: float, lat: float) str | None[source]

Look up the timezone for a geographic coordinate using the global singleton.

Parameters:
  • lng – Longitude of the point in degrees (-180.0 to 180.0)

  • lat – Latitude of the point in degrees (-90.0 to 90.0)

Returns:

The timezone name of a matching polygon, or None if no match found

Thread Safety:

This function is thread-safe for concurrent calls. The underlying global TimezoneFinder instance uses a thread-safe singleton pattern. However, for performance-critical parallel workloads, create separate TimezoneFinder instances per thread to avoid singleton overhead.

Example:
>>> timezone_at(lng=13.4, lat=52.5)
'Europe/Berlin'
timezonefinder.timezone_ids_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray[source]

Look up many coordinates at once using the global singleton, answering with ids.

Equivalent to TimezoneFinder.timezone_ids_at(), which documents the arguments, the on_invalid policies and every error raised.

Returns:

one int16 timezone id per input coordinate, or NO_ZONE_ID (-1) where the scalar lookup would answer None

Example:
>>> ids = timezone_ids_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
>>> ids.dtype
dtype('int16')
timezonefinder.timezone_names_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None][source]

Look up many coordinates at once using the global singleton, answering with names.

Equivalent to TimezoneFinder.timezone_names_at(). Prefer timezone_ids_at() whenever the names are not the end product.

Returns:

one timezone name per input coordinate, or None where no zone covers the point or the coordinate was skipped

Example:
>>> timezone_names_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
['Europe/Berlin', 'Europe/Paris']
timezonefinder.timezone_at_land(*, lng: float, lat: float) str | None[source]

Look up the land timezone for a geographic coordinate using the global singleton.

Returns None for ocean coordinates (which have fixed-offset timezones like Etc/GMT±XX).

Parameters:
  • lng – Longitude of the point in degrees (-180.0 to 180.0)

  • lat – Latitude of the point in degrees (-90.0 to 90.0)

Returns:

The timezone name for land locations, or None for ocean areas

Thread Safety:

This function is thread-safe for concurrent calls. The underlying global TimezoneFinder instance uses a thread-safe singleton pattern. However, for performance-critical parallel workloads, create separate TimezoneFinder instances per thread to avoid singleton overhead.

timezonefinder.timezone_ids_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray[source]

Look up many coordinates at once using the global singleton, answering with land ids.

Equivalent to TimezoneFinder.timezone_ids_at_land(), which documents the arguments, the on_invalid policies and every error raised.

Returns:

one int16 timezone id per input coordinate, or NO_ZONE_ID (-1) where timezone_at_land() would answer None

Example:
>>> ids = timezone_ids_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
>>> ids[1]  # mid-Atlantic: an ocean zone, so no land answer
np.int16(-1)
timezonefinder.timezone_names_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None][source]

Look up many coordinates at once using the global singleton, answering with land names.

Equivalent to TimezoneFinder.timezone_names_at_land(). Prefer timezone_ids_at_land() whenever the names are not the end product.

Returns:

one timezone name per input coordinate, or None where an ocean zone matched, no zone covers the point, or the coordinate was skipped

Example:
>>> timezone_names_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
['Europe/Berlin', None]
timezonefinder.unique_timezone_at(*, lng: float, lat: float) str | None[source]

Get the timezone for a coordinate if the shortcut zone is unambiguous.

Returns None if the H3 shortcut cell contains multiple timezones or no zones.

Parameters:
  • lng – Longitude of the point in degrees (-180.0 to 180.0)

  • lat – Latitude of the point in degrees (-90.0 to 90.0)

Returns:

The timezone name if the shortcut contains exactly one zone, None otherwise

Thread Safety:

This function is thread-safe for concurrent calls. The underlying global TimezoneFinder instance uses a thread-safe singleton pattern. However, for performance-critical parallel workloads, create separate TimezoneFinder instances per thread to avoid singleton overhead.

Note:

This is faster than timezone_at() but may return None even for valid coordinates if the H3 cell spans multiple timezones.

timezonefinder.certain_timezone_at(*, lng: float, lat: float) str | None[source]

Get the timezone for a coordinate with certainty (tests all polygons).

This function checks if a point is contained in ANY timezone polygon. It is slower than timezone_at() but useful when you have custom timezone data with areas of no coverage.

Parameters:
  • lng – Longitude of the point in degrees (-180.0 to 180.0)

  • lat – Latitude of the point in degrees (-90.0 to 90.0)

Returns:

The timezone name if definitely matched, None if not in any polygon

Thread Safety:

This function is thread-safe for concurrent calls. The underlying global TimezoneFinder instance uses a thread-safe singleton pattern. However, for performance-critical parallel workloads, create separate TimezoneFinder instances per thread to avoid singleton overhead.

Note:

For the standard global dataset, this is equivalent to timezone_at() since all earth locations are covered by polygons (including ocean zones). This is primarily useful with custom timezone data.

timezonefinder.get_geometry(tz_name: str | None = '', tz_id: int | None = 0, use_id: bool = False, coords_as_pairs: bool = False) list[list[list[tuple[float, float]] | list[list[float]]]][source]

Retrieves the geometry of a timezone polygon. Uses the global TimezoneFinder instance.

Note: This function is not thread-safe. For multi-threaded environments, create separate TimezoneFinder instances.

Parameters:
  • tz_name – one of the names in timezone_names.txt or self.timezone_names

  • tz_id – the id of the timezone (=index in self.timezone_names)

  • use_id – if True uses tz_id instead of tz_name

  • coords_as_pairs – determines the structure of the polygon representation

Returns:

a data structure representing the multipolygon of this timezone output format: [ [polygon1, hole1, hole2...], [polygon2, ...], ...] and each polygon and hole is itself formatted like: ([longitudes], [latitudes]) or [(lng1,lat1), (lng2,lat2),...] if coords_as_pairs=True.

Timezone conversion helpers

The three steps most callers take after the lookup, as global functions and as methods on both finder classes. They resolve the returned IANA name through the standard library’s zoneinfo, which applies the inverted Etc/GMT±X sign convention correctly - deriving an offset by reading the name instead produces the wrong sign without failing, and the packaged data returns an Etc/GMT zone for every coordinate at sea.

Note

Windows ships no system timezone database, so pip install tzdata is required there before any of these resolve a name; without it they raise zoneinfo.ZoneInfoNotFoundError. This package returns IANA names and does not carry the database itself.

timezonefinder.zoneinfo_at(*, lng: float, lat: float) ZoneInfo | None[source]

Look up the timezone for a coordinate as a zoneinfo.ZoneInfo, using the global singleton.

Equivalent to TimezoneFinder.zoneinfo_at(), which documents the arguments and every error raised - including the tzdata a Windows machine needs installed before any IANA name resolves.

Returns:

the zone covering the point, or None where timezone_at() answers None

Example:
>>> zoneinfo_at(lng=13.358, lat=52.5061)
zoneinfo.ZoneInfo(key='Europe/Berlin')
timezonefinder.utc_offset_at(*, lng: float, lat: float, when: datetime | None = None) timedelta | None[source]

Get the UTC offset in force at a coordinate, using the global singleton.

Equivalent to TimezoneFinder.utc_offset_at(), which documents the arguments, how a naive and an aware when differ, and every error raised - including the tzdata a Windows machine needs installed.

Returns:

the offset as a timedelta, or None where timezone_at() answers None

Example:
>>> from datetime import datetime
>>> utc_offset_at(lng=13.358, lat=52.5061, when=datetime(2026, 1, 1))
datetime.timedelta(seconds=3600)
timezonefinder.localize(dt: datetime, *, lng: float, lat: float) datetime | None[source]

Attach the timezone covering a coordinate to a naive datetime, using the global singleton.

Equivalent to TimezoneFinder.localize(), which documents the arguments and every error raised - including the tzdata a Windows machine needs installed.

Returns:

the same wall-clock time made aware, or None where timezone_at() answers None

Example:
>>> from datetime import datetime
>>> localize(datetime(2026, 1, 1, 12), lng=13.358, lat=52.5061)
datetime.datetime(2026, 1, 1, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin'))

TimezoneFinderL

class timezonefinder.TimezoneFinderL(bin_file_location: str | Path | None = None)[source]

Bases: AbstractTimezoneFinder

A lightweight version of TimezoneFinder for quick timezone suggestions.

Instead of using timezone polygon data like TimezoneFinder, this class only uses a precomputed ‘shortcut’ to suggest a probable result: the most common zone in a rectangle of a half degree of latitude and one degree of longitude.

Thread Safety:

Each thread that performs timezone lookups must create its own independent TimezoneFinderL instance. Do not share a single instance across threads.

timezone_at(*, lng: float, lat: float) str | None[source]

instantly returns the name of the most common zone within the corresponding shortcut

Note: ‘most common’ in this context means that the boundary polygons with the most coordinates in sum

occurring in the corresponding shortcut belong to this zone.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the timezone name of the most common zone or None if there are no timezone polygons in this shortcut

data_location: Path
shortcuts: ShortcutIndex

which timezones can possibly cover a point. This class asks it what a cell resolves to and never how that is stored - see timezonefinder/shortcut_index.py.

zone_names: ZoneNames

the dataset’s names, and every way a zone id becomes one. This class produces ids and asks it to name them - see timezonefinder/zone_names.py.

zone_ids: ndarray
holes_dir
boundaries_dir
boundaries
holes
__init__(bin_file_location: str | Path | None = None)

Initialize the AbstractTimezoneFinder.

Loads the zone names, the per-polygon zone ids and the shortcut index, all of which are always held in memory. Selecting how the polygon coordinate data is accessed belongs to the subclass that loads it: TimezoneFinder takes in_memory for that, and this class has nothing to apply it to.

Parameters:

bin_file_location – Path to the directory containing binary timezone data. If None, uses the bundled package data directory.

Raises:
  • FileNotFoundError – If timezone data files cannot be found at the specified location

  • ValueError – If timezone data files are corrupted or in an invalid format

cleanup() None

Clean up resources. Override in subclasses as needed.

property data_version: str

The timezone-boundary-builder release this finder answers from.

Reads the stamp scripts/file_converter.py wrote into the data directory at build time (data_version.txt), so an installed timezonefinder can state which dataset it is answering from without reverse-engineering it from the package version - which also changes for unrelated code fixes and, under the automated data-update pipeline, changes together with the data in a way callers cannot distinguish.

For the packaged data this is the release update_data.sh downloaded. A data directory compiled from your own GeoJSON reads "unknown" unless scripts/file_converter.py --data-version named the release it came from, since nothing about the input states it.

Raises:

FileNotFoundError – if the data directory carries no stamp, which a directory compiled before this file existed does not.

localize(dt: datetime, *, lng: float, lat: float) datetime | None

Attach the timezone covering a point to a naive datetime.

The datetime is read as local wall-clock time there: the instant it denotes is decided by the zone, which is what makes this different from dt.astimezone(...) on an already-aware value.

Parameters:
  • dt – a naive datetime, i.e. local time at the point

  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the same wall-clock time made aware, or None where timezone_at() answers None

Raises:
  • ValueError – if the coordinates are out of bounds, or if dt already carries a timezone - converting one is dt.astimezone()’s job, and silently re-labelling it would move the instant it denotes

  • zoneinfo.ZoneInfoNotFoundError – on a platform without a timezone database - see zoneinfo_at(), which names the Windows case

Example:
>>> tf = TimezoneFinder()
>>> tf.localize(datetime(2026, 1, 1, 12), lng=13.358, lat=52.5061)
datetime.datetime(2026, 1, 1, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin'))
property nr_of_zones: int

Get the number of timezones.

Return type:

int

timezone_at_land(*, lng: float, lat: float) str | None

computes in which land timezone a point is included in

Especially for large polygons it is expensive to check if a point is really included. To speed things up there are “shortcuts” being used (stored in a binary file), which have been precomputed and store which timezone polygons have to be checked.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the timezone name of a matching polygon or None when an ocean timezone (“Etc/GMT+-XX”) has been matched.

timezone_ids_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray

Look up many coordinates at once, answering with timezone ids.

The batch counterpart of timezone_at(), and the primary one: a caller doing millions of lookups should not pay for millions of string lookups it maps straight back to something else. timezone_names_at() is the convenience on top.

What a batch amortises is the per-call overhead, not the geometry. Validation, the integer scaling and the shortcut lookup run once over the whole batch as numpy operations; ambiguous points still fall through to the point-in-polygon loop one at a time, and h3’s cell lookup has no vectorised form, so it stays a loop too. Expect the win to be largest on points whose cell a single zone covers.

Parameters:
  • lngs – longitudes in degrees, as any 1-D array-like. A C-contiguous float64 numpy array is used without copying.

  • lats – latitudes in degrees, the same length as lngs.

  • on_invalid – what to do with a coordinate outside the valid range (which includes NaN and infinity). "raise" (the default) matches the scalar methods; "skip" answers those points with NO_ZONE_ID and the rest normally.

Returns:

one int16 per input coordinate - a timezone id, or NO_ZONE_ID (-1) where the scalar method would answer None: no zone covers the point, or it was skipped.

Raises:
  • TypeError – if either axis holds values that are not numbers.

  • ValueError – if the two axes differ in length, either is not one-dimensional, on_invalid is not a known policy, or - under on_invalid="raise" - a coordinate is out of range.

Note

coordinates are passed one axis per argument on purpose. A single (N, 2) array would have to be read positionally, and a swapped pair is still a valid coordinate for most of the populated world - so the mistake would return a real but wrong timezone instead of raising.

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
>>> [tf.zone_name_from_id(int(i)) for i in ids]
['Europe/Berlin', 'Europe/Paris']
timezone_ids_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray

Look up many coordinates at once, answering with land timezone ids.

The batch counterpart of timezone_at_land(), and - as with timezone_ids_at(), whose arguments and errors this shares - the primary one, with timezone_names_at_land() the convenience on top.

The ocean check costs nothing per point here. Ocean-ness is a fixed property of a zone id for a given dataset, so the whole answer array is masked in one indexing operation rather than testing each answer’s name - which makes this cheaper per point than calling timezone_at_land() in a loop, not merely equal to it.

Parameters:
  • lngs – longitudes in degrees, as any 1-D array-like.

  • lats – latitudes in degrees, the same length as lngs.

  • on_invalid – what to do with a coordinate outside the valid range - see timezone_ids_at(), which documents the policies.

Returns:

one int16 per input coordinate - a land timezone id, or NO_ZONE_ID (-1) where timezone_at_land() would answer None: an ocean zone matched, no zone covers the point, or it was skipped. The three are deliberately one sentinel, exactly as in timezone_ids_at().

Raises:
  • TypeError – if either axis holds values that are not numbers.

  • ValueError – if the two axes differ in length, either is not one-dimensional, on_invalid is not a known policy, or - under on_invalid="raise" - a coordinate is out of range.

Note

coordinates are passed one axis per argument, for the reason timezone_ids_at() gives: a single (N, 2) array would be read positionally, and a swapped pair is still a valid coordinate for most of the populated world - so the mistake would return a real but wrong answer instead of raising. Such an array is rejected as not one-dimensional.

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
>>> ids[1] == NO_ZONE_ID  # mid-Atlantic: an ocean zone, so no land answer
True
property timezone_names: list[str]

All timezone names of the loaded dataset, in zone id order.

A read-only view onto zone_names, which owns the list and everything that turns an id back into one.

Return type:

list[str]

timezone_names_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None]

Look up many coordinates at once, answering with timezone names.

The convenience on top of timezone_ids_at(), which documents the arguments, the on_invalid policies and every error raised. Each answer is what timezone_at() would return for that point, None included.

Prefer the id form whenever the names are not the end product: this method adds one list index and one Python object per coordinate, which is most of what a batch lookup was meant to avoid.

Returns:

one timezone name per input coordinate, or None where no zone covers the point or the coordinate was skipped.

Example:
>>> tf = TimezoneFinder()
>>> tf.timezone_names_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
['Europe/Berlin', 'Europe/Paris']
timezone_names_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None]

Look up many coordinates at once, answering with land timezone names.

The convenience on top of timezone_ids_at_land(), which documents the arguments and every error raised. Each answer is what timezone_at_land() would return for that point, None included.

Prefer the id form whenever the names are not the end product, for the reason timezone_names_at() gives.

Returns:

one timezone name per input coordinate, or None where an ocean zone matched, no zone covers the point, or the coordinate was skipped.

Example:
>>> tf = TimezoneFinder()
>>> tf.timezone_names_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
['Europe/Berlin', None]
unique_timezone_at(*, lng: float, lat: float) str | None

returns the name of a unique zone within the corresponding shortcut

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the timezone name of the unique zone or None if there are no or multiple zones in this shortcut

static using_clang_pip() bool
Returns:

True if the compiled C implementation of the point in polygon algorithm is being used

static using_numba() bool

Check if Numba is being used.

Return type:

bool

Returns:

True if Numba is being used to JIT compile helper functions

utc_offset_at(*, lng: float, lat: float, when: datetime | None = None) timedelta | None

The UTC offset in force at a point, at a given moment.

The offset is a property of a zone and a date, since it changes with daylight saving time - so it is read off an aware datetime rather than off the zone.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

  • when – the moment to read the offset at, defaulting to now. A naive datetime is read as local wall-clock time in the zone found; an aware one is read as the instant it denotes.

Returns:

the offset as a timedelta, or None where timezone_at() answers None

Raises:
  • ValueError – if the coordinates are out of bounds

  • zoneinfo.ZoneInfoNotFoundError – on a platform without a timezone database - see zoneinfo_at(), which names the Windows case

Example:
>>> tf = TimezoneFinder()
>>> tf.utc_offset_at(lng=13.358, lat=52.5061, when=datetime(2026, 1, 1))
datetime.timedelta(seconds=3600)
zone_id_of(boundary_id: int | integer) int

Get the timezone ID for a specific boundary polygon.

Parameters:

boundary_id – The numeric identifier of the boundary polygon

Returns:

The timezone ID (index into timezone_names)

Raises:

ValueError – If boundary_id does not select exactly one zone id - negative, out of range, not usable as an index, or selecting several. The underlying IndexError and TypeError are both re-raised as ValueError, so that is the only type callers have to handle.

zone_ids_of(boundary_ids: ndarray) ndarray

Get the zone IDs of multiple boundary polygons.

Parameters:

boundary_ids – An array of boundary polygon IDs.

Returns:

array of corresponding timezone IDs.

Raises:

ValueError – If any id is negative. Out-of-range ids raise IndexError from NumPy, as they do for any array indexing.

zone_name_from_boundary_id(boundary_id: int | integer) str

Get the zone name from a boundary polygon ID.

Parameters:

boundary_id – The ID of the boundary polygon.

Returns:

The name of the zone.

Raises:

ValueError – If boundary_id is negative or does not select exactly one zone id, as for zone_id_of().

zone_name_from_id(zone_id: int) str

Get the timezone name corresponding to a zone ID.

Parameters:

zone_id – The numeric ID of the timezone (0-based index)

Returns:

The IANA timezone name (e.g., ‘Europe/Berlin’)

Raises:
  • ValueError – If zone_id is negative or out of range for the loaded dataset. The underlying IndexError is re-raised as ValueError.

  • TypeError – If zone_id is not an integer.

Example:
>>> tf = TimezoneFinder()
>>> tf.zone_name_from_id(0)
'Africa/Abidjan'
zone_names_from_ids(zone_ids: ArrayLike) list[str | None]

Convert many zone ids to timezone names in one call.

The batch counterpart of zone_name_from_id(), and what timezone_ids_at() is meant to be paired with: keep the ids while they are being joined, filtered or grouped, and name them once at the end. Above a threshold the conversion is a numpy gather rather than a Python loop, which is several times faster per id on a large batch.

Parameters:

zone_ids – the ids to name, as any 1-D array-like of integers.

Returns:

one name per id, with None wherever the id is NO_ZONE_ID (-1) - so an answer from timezone_ids_at() round-trips to exactly what timezone_names_at() would have returned.

Raises:
  • TypeError – if zone_ids does not hold integers.

  • ValueError – if zone_ids is not one-dimensional, or holds an id that is neither a valid zone id nor the sentinel. A negative other than -1 is rejected rather than counted from the end of the dataset, as for zone_name_from_id().

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
>>> tf.zone_names_from_ids(ids)
['Europe/Berlin', 'Europe/Paris']
zoneinfo_at(*, lng: float, lat: float) ZoneInfo | None

The timezone covering a point, as a zoneinfo.ZoneInfo.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the zone timezone_at() names, or None where that answers None

Raises:
  • ValueError – if the coordinates are out of bounds

  • zoneinfo.ZoneInfoNotFoundError – if the platform has no timezone database holding that name. Windows ships none, so pip install tzdata is required there - this package returns IANA names and does not carry the database itself.

Example:
>>> tf = TimezoneFinder()
>>> tf.zoneinfo_at(lng=13.358, lat=52.5061)
zoneinfo.ZoneInfo(key='Europe/Berlin')

TimezoneFinder

class timezonefinder.TimezoneFinder(bin_file_location: str | Path | None = None, in_memory: bool = False)[source]

Bases: AbstractTimezoneFinder

Class for quickly finding the timezone of a point on earth offline.

Because of indexing (“shortcuts”), not all timezone polygons have to be tested during a query.

Opens the required timezone polygon data in binary files to enable fast access. For a detailed documentation of data management please refer to the code documentation of file_converter.py

Thread Safety:

Each thread that performs timezone lookups must create its own independent TimezoneFinder instance. Do not share a single instance across threads, as this can lead to race conditions and incorrect results. Example:

import threading from timezonefinder import TimezoneFinder

def lookup_in_thread(lng, lat):

# Each thread creates its own instance tf = TimezoneFinder(in_memory=True) return tf.timezone_at(lng=lng, lat=lat)

Parameters:
param bin_file_location:

path to the binary data files to use, None if native package data should be used

param in_memory:

Whether to completely read and keep the coordinate data in memory as numpy arrays.

__init__(bin_file_location: str | Path | None = None, in_memory: bool = False)[source]

Initialize the AbstractTimezoneFinder.

Loads the zone names, the per-polygon zone ids and the shortcut index, all of which are always held in memory. Selecting how the polygon coordinate data is accessed belongs to the subclass that loads it: TimezoneFinder takes in_memory for that, and this class has nothing to apply it to.

Parameters:

bin_file_location – Path to the directory containing binary timezone data. If None, uses the bundled package data directory.

Raises:
  • FileNotFoundError – If timezone data files cannot be found at the specified location

  • ValueError – If timezone data files are corrupted or in an invalid format

holes_dir
boundaries_dir
boundaries
holes
hole_registry
property nr_of_polygons: int
property nr_of_holes: int
coords_of(boundary_id: int | integer = 0) ndarray[source]

Get the coordinates of a boundary polygon from the FlatBuffers collection.

Parameters:

boundary_id – The index of the polygon.

Returns:

Array of coordinates.

get_polygon(boundary_id: int | integer, coords_as_pairs: bool = False) list[list[tuple[float, float]] | list[list[float]]][source]

Get the polygon coordinates of a given boundary polygon including its holes.

Parameters:
  • boundary_id – ID of the boundary polygon

  • coords_as_pairs – If True, returns coordinates as pairs (lng, lat). If False, returns coordinates as separate lists of longitudes and latitudes.

Returns:

List of polygon coordinates

get_geometry(tz_name: str | None = '', tz_id: int | None = 0, use_id: bool = False, coords_as_pairs: bool = False) list[list[list[tuple[float, float]] | list[list[float]]]][source]

retrieves the geometry of a timezone: multiple boundary polygons with holes

Parameters:
  • tz_name – one of the names in timezone_names.txt or self.timezone_names

  • tz_id – the id of the timezone (=index in self.timezone_names)

  • use_id – if True uses tz_id instead of tz_name

  • coords_as_pairs – determines the structure of the polygon representation

Returns:

a data structure representing the multipolygon of this timezone output format: [ [polygon1, hole1, hole2...], [polygon2, ...], ...] and each polygon and hole is itself formatted like: ([longitudes], [latitudes]) or [(lng1,lat1), (lng2,lat2),...] if coords_as_pairs=True.

inside_of_polygon(boundary_id: int | integer, x: int, y: int) bool[source]

Check if a point is inside a boundary polygon.

Parameters:
  • boundary_id – boundary polygon ID

  • x – X-coordinate of the point

  • y – Y-coordinate of the point

Returns:

True if the point lies inside the boundary polygon, False if outside or in a hole.

timezone_at(*, lng: float, lat: float) str | None[source]

Find the timezone for a given point using hybrid shortcuts, considering both land and ocean timezones.

Uses precomputed hybrid shortcuts to reduce the number of polygons checked. Returns the timezone name of the matched polygon, which may be an ocean timezone (“Etc/GMT+-XX”) if applicable.

Since ocean timezones span the whole globe, some timezone will always be matched! None can only be returned when using custom timezone data without such ocean timezones.

Note

for speed the last remaining zone is returned without a point in polygon test: once no other zone can be matched, its polygons cannot change the outcome. With the packaged data this is always correct, since the ocean zones cover the globe and every point therefore lies within one of the candidate polygons. With custom data that leaves areas uncovered it is not: a point inside none of the candidates is still attributed to that last zone. Use certain_timezone_at() there, which tests every candidate.

Parameters:
  • lng – longitude of the point in degrees (-180.0 to 180.0)

  • lat – latitude of the point in degrees (90.0 to -90.0)

Returns:

the timezone name of the matched polygon, or None if no match is found.

cleanup() None

Clean up resources. Override in subclasses as needed.

data_location: Path
property data_version: str

The timezone-boundary-builder release this finder answers from.

Reads the stamp scripts/file_converter.py wrote into the data directory at build time (data_version.txt), so an installed timezonefinder can state which dataset it is answering from without reverse-engineering it from the package version - which also changes for unrelated code fixes and, under the automated data-update pipeline, changes together with the data in a way callers cannot distinguish.

For the packaged data this is the release update_data.sh downloaded. A data directory compiled from your own GeoJSON reads "unknown" unless scripts/file_converter.py --data-version named the release it came from, since nothing about the input states it.

Raises:

FileNotFoundError – if the data directory carries no stamp, which a directory compiled before this file existed does not.

localize(dt: datetime, *, lng: float, lat: float) datetime | None

Attach the timezone covering a point to a naive datetime.

The datetime is read as local wall-clock time there: the instant it denotes is decided by the zone, which is what makes this different from dt.astimezone(...) on an already-aware value.

Parameters:
  • dt – a naive datetime, i.e. local time at the point

  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the same wall-clock time made aware, or None where timezone_at() answers None

Raises:
  • ValueError – if the coordinates are out of bounds, or if dt already carries a timezone - converting one is dt.astimezone()’s job, and silently re-labelling it would move the instant it denotes

  • zoneinfo.ZoneInfoNotFoundError – on a platform without a timezone database - see zoneinfo_at(), which names the Windows case

Example:
>>> tf = TimezoneFinder()
>>> tf.localize(datetime(2026, 1, 1, 12), lng=13.358, lat=52.5061)
datetime.datetime(2026, 1, 1, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin'))
property nr_of_zones: int

Get the number of timezones.

Return type:

int

shortcuts: ShortcutIndex

which timezones can possibly cover a point. This class asks it what a cell resolves to and never how that is stored - see timezonefinder/shortcut_index.py.

timezone_at_land(*, lng: float, lat: float) str | None

computes in which land timezone a point is included in

Especially for large polygons it is expensive to check if a point is really included. To speed things up there are “shortcuts” being used (stored in a binary file), which have been precomputed and store which timezone polygons have to be checked.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the timezone name of a matching polygon or None when an ocean timezone (“Etc/GMT+-XX”) has been matched.

timezone_ids_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray

Look up many coordinates at once, answering with timezone ids.

The batch counterpart of timezone_at(), and the primary one: a caller doing millions of lookups should not pay for millions of string lookups it maps straight back to something else. timezone_names_at() is the convenience on top.

What a batch amortises is the per-call overhead, not the geometry. Validation, the integer scaling and the shortcut lookup run once over the whole batch as numpy operations; ambiguous points still fall through to the point-in-polygon loop one at a time, and h3’s cell lookup has no vectorised form, so it stays a loop too. Expect the win to be largest on points whose cell a single zone covers.

Parameters:
  • lngs – longitudes in degrees, as any 1-D array-like. A C-contiguous float64 numpy array is used without copying.

  • lats – latitudes in degrees, the same length as lngs.

  • on_invalid – what to do with a coordinate outside the valid range (which includes NaN and infinity). "raise" (the default) matches the scalar methods; "skip" answers those points with NO_ZONE_ID and the rest normally.

Returns:

one int16 per input coordinate - a timezone id, or NO_ZONE_ID (-1) where the scalar method would answer None: no zone covers the point, or it was skipped.

Raises:
  • TypeError – if either axis holds values that are not numbers.

  • ValueError – if the two axes differ in length, either is not one-dimensional, on_invalid is not a known policy, or - under on_invalid="raise" - a coordinate is out of range.

Note

coordinates are passed one axis per argument on purpose. A single (N, 2) array would have to be read positionally, and a swapped pair is still a valid coordinate for most of the populated world - so the mistake would return a real but wrong timezone instead of raising.

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
>>> [tf.zone_name_from_id(int(i)) for i in ids]
['Europe/Berlin', 'Europe/Paris']
timezone_ids_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') ndarray

Look up many coordinates at once, answering with land timezone ids.

The batch counterpart of timezone_at_land(), and - as with timezone_ids_at(), whose arguments and errors this shares - the primary one, with timezone_names_at_land() the convenience on top.

The ocean check costs nothing per point here. Ocean-ness is a fixed property of a zone id for a given dataset, so the whole answer array is masked in one indexing operation rather than testing each answer’s name - which makes this cheaper per point than calling timezone_at_land() in a loop, not merely equal to it.

Parameters:
  • lngs – longitudes in degrees, as any 1-D array-like.

  • lats – latitudes in degrees, the same length as lngs.

  • on_invalid – what to do with a coordinate outside the valid range - see timezone_ids_at(), which documents the policies.

Returns:

one int16 per input coordinate - a land timezone id, or NO_ZONE_ID (-1) where timezone_at_land() would answer None: an ocean zone matched, no zone covers the point, or it was skipped. The three are deliberately one sentinel, exactly as in timezone_ids_at().

Raises:
  • TypeError – if either axis holds values that are not numbers.

  • ValueError – if the two axes differ in length, either is not one-dimensional, on_invalid is not a known policy, or - under on_invalid="raise" - a coordinate is out of range.

Note

coordinates are passed one axis per argument, for the reason timezone_ids_at() gives: a single (N, 2) array would be read positionally, and a swapped pair is still a valid coordinate for most of the populated world - so the mistake would return a real but wrong answer instead of raising. Such an array is rejected as not one-dimensional.

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
>>> ids[1] == NO_ZONE_ID  # mid-Atlantic: an ocean zone, so no land answer
True
property timezone_names: list[str]

All timezone names of the loaded dataset, in zone id order.

A read-only view onto zone_names, which owns the list and everything that turns an id back into one.

Return type:

list[str]

timezone_names_at(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None]

Look up many coordinates at once, answering with timezone names.

The convenience on top of timezone_ids_at(), which documents the arguments, the on_invalid policies and every error raised. Each answer is what timezone_at() would return for that point, None included.

Prefer the id form whenever the names are not the end product: this method adds one list index and one Python object per coordinate, which is most of what a batch lookup was meant to avoid.

Returns:

one timezone name per input coordinate, or None where no zone covers the point or the coordinate was skipped.

Example:
>>> tf = TimezoneFinder()
>>> tf.timezone_names_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
['Europe/Berlin', 'Europe/Paris']
timezone_names_at_land(*, lngs: ArrayLike, lats: ArrayLike, on_invalid: Literal['raise', 'skip'] = 'raise') list[str | None]

Look up many coordinates at once, answering with land timezone names.

The convenience on top of timezone_ids_at_land(), which documents the arguments and every error raised. Each answer is what timezone_at_land() would return for that point, None included.

Prefer the id form whenever the names are not the end product, for the reason timezone_names_at() gives.

Returns:

one timezone name per input coordinate, or None where an ocean zone matched, no zone covers the point, or the coordinate was skipped.

Example:
>>> tf = TimezoneFinder()
>>> tf.timezone_names_at_land(lngs=[13.358, -30.0], lats=[52.5061, 0.0])
['Europe/Berlin', None]
unique_timezone_at(*, lng: float, lat: float) str | None

returns the name of a unique zone within the corresponding shortcut

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the timezone name of the unique zone or None if there are no or multiple zones in this shortcut

static using_clang_pip() bool
Returns:

True if the compiled C implementation of the point in polygon algorithm is being used

static using_numba() bool

Check if Numba is being used.

Return type:

bool

Returns:

True if Numba is being used to JIT compile helper functions

utc_offset_at(*, lng: float, lat: float, when: datetime | None = None) timedelta | None

The UTC offset in force at a point, at a given moment.

The offset is a property of a zone and a date, since it changes with daylight saving time - so it is read off an aware datetime rather than off the zone.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

  • when – the moment to read the offset at, defaulting to now. A naive datetime is read as local wall-clock time in the zone found; an aware one is read as the instant it denotes.

Returns:

the offset as a timedelta, or None where timezone_at() answers None

Raises:
  • ValueError – if the coordinates are out of bounds

  • zoneinfo.ZoneInfoNotFoundError – on a platform without a timezone database - see zoneinfo_at(), which names the Windows case

Example:
>>> tf = TimezoneFinder()
>>> tf.utc_offset_at(lng=13.358, lat=52.5061, when=datetime(2026, 1, 1))
datetime.timedelta(seconds=3600)
zone_id_of(boundary_id: int | integer) int

Get the timezone ID for a specific boundary polygon.

Parameters:

boundary_id – The numeric identifier of the boundary polygon

Returns:

The timezone ID (index into timezone_names)

Raises:

ValueError – If boundary_id does not select exactly one zone id - negative, out of range, not usable as an index, or selecting several. The underlying IndexError and TypeError are both re-raised as ValueError, so that is the only type callers have to handle.

zone_ids: ndarray
zone_ids_of(boundary_ids: ndarray) ndarray

Get the zone IDs of multiple boundary polygons.

Parameters:

boundary_ids – An array of boundary polygon IDs.

Returns:

array of corresponding timezone IDs.

Raises:

ValueError – If any id is negative. Out-of-range ids raise IndexError from NumPy, as they do for any array indexing.

zone_name_from_boundary_id(boundary_id: int | integer) str

Get the zone name from a boundary polygon ID.

Parameters:

boundary_id – The ID of the boundary polygon.

Returns:

The name of the zone.

Raises:

ValueError – If boundary_id is negative or does not select exactly one zone id, as for zone_id_of().

zone_name_from_id(zone_id: int) str

Get the timezone name corresponding to a zone ID.

Parameters:

zone_id – The numeric ID of the timezone (0-based index)

Returns:

The IANA timezone name (e.g., ‘Europe/Berlin’)

Raises:
  • ValueError – If zone_id is negative or out of range for the loaded dataset. The underlying IndexError is re-raised as ValueError.

  • TypeError – If zone_id is not an integer.

Example:
>>> tf = TimezoneFinder()
>>> tf.zone_name_from_id(0)
'Africa/Abidjan'
zone_names: ZoneNames

the dataset’s names, and every way a zone id becomes one. This class produces ids and asks it to name them - see timezonefinder/zone_names.py.

zone_names_from_ids(zone_ids: ArrayLike) list[str | None]

Convert many zone ids to timezone names in one call.

The batch counterpart of zone_name_from_id(), and what timezone_ids_at() is meant to be paired with: keep the ids while they are being joined, filtered or grouped, and name them once at the end. Above a threshold the conversion is a numpy gather rather than a Python loop, which is several times faster per id on a large batch.

Parameters:

zone_ids – the ids to name, as any 1-D array-like of integers.

Returns:

one name per id, with None wherever the id is NO_ZONE_ID (-1) - so an answer from timezone_ids_at() round-trips to exactly what timezone_names_at() would have returned.

Raises:
  • TypeError – if zone_ids does not hold integers.

  • ValueError – if zone_ids is not one-dimensional, or holds an id that is neither a valid zone id nor the sentinel. A negative other than -1 is rejected rather than counted from the end of the dataset, as for zone_name_from_id().

Example:
>>> tf = TimezoneFinder()
>>> ids = tf.timezone_ids_at(lngs=[13.358, 2.3522], lats=[52.5061, 48.8566])
>>> tf.zone_names_from_ids(ids)
['Europe/Berlin', 'Europe/Paris']
zoneinfo_at(*, lng: float, lat: float) ZoneInfo | None

The timezone covering a point, as a zoneinfo.ZoneInfo.

Parameters:
  • lng – longitude of the point in degree (-180.0 to 180.0)

  • lat – latitude in degree (90.0 to -90.0)

Returns:

the zone timezone_at() names, or None where that answers None

Raises:
  • ValueError – if the coordinates are out of bounds

  • zoneinfo.ZoneInfoNotFoundError – if the platform has no timezone database holding that name. Windows ships none, so pip install tzdata is required there - this package returns IANA names and does not carry the database itself.

Example:
>>> tf = TimezoneFinder()
>>> tf.zoneinfo_at(lng=13.358, lat=52.5061)
zoneinfo.ZoneInfo(key='Europe/Berlin')
certain_timezone_at(*, lng: float, lat: float) str | None[source]

checks in which timezone polygon the point is certainly included in using hybrid shortcuts

Note

this is only meaningful when you have compiled your own timezone data where there are areas without timezone polygon coverage. Otherwise, some timezone will always be matched and the functionality is equal to using .timezone_at() -> useless to actually test all polygons.

Note

using this function is less performant than .timezone_at()

Parameters:
  • lng – longitude of the point in degree

  • lat – latitude of the point in degree

Returns:

the timezone name of the polygon the point is included in or None