timezonefinder: offline timezone lookup that refuses to simplify borders
Offline timezone lookup for WGS84 coordinates, with no polygon simplification - so the answer stays correct at timezone borders.
At a glance
- What is it?
- timezonefinder maps WGS84 coordinates to IANA timezones without calling a network service, and its whole design rests on one decision: the boundary polygons are never simplified, so a coordinate near a timezone border gets the answer the source dataset gives rather than an approximation of it. A grid shortcut index keeps that affordable, and three interchangeable backends accelerate the point-in-polygon test.
- Who is it for?
- Adopt timezonefinder if you need timezone lookup for coordinates in an environment where you cannot make a network call, or if your coordinates cluster near timezone borders and an approximate answer is not good enough. Do not adopt it if you want a small memory footprint and can live with fewer timezone names, because the full dataset with over 440 timezone names is what makes it heavy.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The core claim, and what it means in practice
timezonefinder offers offline timezone lookup for WGS84 coordinates, with no polygon simplification, so the answer stays correct at timezone borders.
That single sentence is the project. Almost every coordinate to timezone lookup draws its polygons from a source dataset and then simplifies them, usually because the full geometry is large and slow to test. Simplification means rounding vertices, which means the boundary moves slightly inward or outward, which means a point close to a real border gets the wrong answer. The readme names the trade-off as central: the boundary polygons are never simplified, so border accuracy is limited only by the source dataset.
So the accuracy guarantee is inherited from upstream data rather than created by the library, and that distinction matters. The library is not claiming to know where a border is; it is claiming not to lose that knowledge on the way in.
The consequence for users is that answers can change without the code changing, because the data can change. The readme is explicit about this: a dataset update can change the answer for a coordinate, so you should pin it when a result has to stay reproducible across deployments. And the pin is done on a separate distribution, with a version in the form pip install timezonefinder "timezonefinder-data==<version>".
The data ships separately for a good reason, and the readme spells it out: the timezone boundary data is installed automatically as the separate timezonefinder-data distribution, so that a new dataset ships without a new timezonefinder release. That decouples data updates from code updates, which means a security fix to the library and a border correction to the dataset can move independently.
The package aims at maximum accuracy around timezone borders while offering fast lookup performance and compatibility with many Python runtime environments, and it combines preprocessed polygon data, grid-based spatial shortcuts, and optional acceleration via Numba or a clang-backed point-in-polygon routine.
It is MIT licensed, the version in the pyproject is 9.1.0 with recent releases 9.0.0 from 2026-09-11 and 8.3.0 from 2026-08-19, and the last push was on 2026-10-20.
How a lookup actually works
The how it works section is short and precise, and it is the part to read if you want to know whether the accuracy claim is real.
A lookup scales the coordinates to 32-bit integers, multiplying by ten to the power of seven, which gives steps of roughly 1.1 centimetres. It then finds the point's cell in an aligned grid tree, with 0.75-degree cells refining to 0.1875 degrees where several zones meet, and it does that with one integer division per axis. Finally it reads a precomputed shortcut for that cell.
The key optimisation is the next sentence: most cells are covered by a single timezone, so the answer is returned immediately with no geometry touched at all.
That is what makes the unsimplified approach affordable. The full resolution polygons are expensive, but you only reach them for cells that straddle a border, and the grid tells you which those are in advance. The shortcut index is what makes carrying full-resolution geometry affordable, as the readme puts it.
Only an ambiguous cell falls through to the polygons it lists, and there the process is two-stage. Bounding-box rejection comes first, which is a cheap test that discards polygons that cannot possibly contain the point. Then a ray-casting point-in-polygon test runs, and the readme notes the order of holes before the outer ring, since holes are smaller and can reject the polygon outright.
The last two paragraphs of that section describe behaviour that is easy to get wrong and that the library handles explicitly.
Since the dataset includes ocean zones, every coordinate on earth matches some timezone, so you use timezone_at_land when you need to tell land from sea. That is a deliberate design choice with a consequence: there is no such thing as an out of range result, and a query for the middle of the Pacific returns an ocean zone rather than an error.
The dataset also contains genuinely overlapping zones, so a coordinate can match more than one, and the two methods differ here. timezone_at picks the smaller one, and timezones_at returns all of them. So the single-value API is a convenience with a documented tie-break rule rather than a claim that only one zone can apply.
Three backends for the same answer
The performance section states the headline figure and then is careful about how far it can be trusted.
Hundreds of thousands of lookups per second on a single core for uniformly random query points. The exact figure depends on the acceleration backend, the machine and the dataset version, so the benchmark reports carry it, each stating the configuration it was measured in, rather than the readme page, which would go stale.
That is a good instinct, because a benchmark number without its configuration is nearly useless. The readme also points at a benchmark trend chart that is appended to on every push.
The point-in-polygon routine has three interchangeable backends, selected at import time in a specific order. A clang-compiled C extension, built automatically when a compiler is available. Numba JIT compilation, used only where the C extension is unavailable and the optional numba dependency is installed. And pure Python.
The guarantee that matters is this: all three return identical answers, so a missing compiler costs speed, never results. That is the right property for an acceleration layer. A fast path that occasionally disagrees with the slow path turns a build environment difference into a data correctness bug, and the readme is claiming that cannot happen here.
The setup is also careful about the detail that the extension wins wherever it loaded, and that where it is not the kernel, the numba extra also costs a process more resident memory than the packaged data. So there is a memory trade-off in choosing Numba over pure Python, and the acceleration path comparison measures all three against each other and is regenerated with every report.
The setup.py shows how the C extension is built, and it is more careful than most. It uses cffi_modules with an ffibuilder, and it defines a custom build_ext subclass named fallible_build_ext that catches a build failure and prints that it will proceed with the non-native implementation rather than aborting the install. In other words, a missing compiler degrades the build rather than breaking it, which is what makes the three-backend design work as a single install story.
The same file also handles the ABI3 wheel naming, with a comment explaining that no free-threaded build may claim py_limited_api, because setuptools raises on that whenever Py_GIL_DISABLED is set, and the stricter of the two gates wins.
Using it, and the batch API
Installation is a single pip command:
pip install timezonefinderAnd the basic use is a global function:
# use the global function for convenience:
from timezonefinder import timezone_at
tz = timezone_at(lng=13.358, lat=52.5061) # 'Europe/Berlin'For repeated work the readme recommends reusing one instance, and it shows why with a context manager:
# For improved performance and control, reuse one instance for the whole job.
# The context manager releases its coordinate data even if the job raises:
from timezonefinder import TimezoneFinder
with TimezoneFinder(in_memory=True) as tf:
tz = tf.timezone_at(lng=13.358, lat=52.5061) # 'Europe/Berlin'The in_memory option is the relevant one for a batch job, because it keeps the coordinate data in memory rather than reading it from disk per lookup. And the context manager is what makes that safe, since it releases the data even if the job raises.
For many coordinates at once there are two batch methods, and the difference between them is about who does the mapping:
# them itself, names for one that does not:
lngs = [13.358, 2.3522]
lats = [52.5061, 48.8566]
zone_ids = tf.timezone_ids_at(lngs=lngs, lats=lats)
names = tf.timezone_names_at(lngs=lngs, lats=lats)
# ['Europe/Berlin', 'Europe/Paris']```
So timezone_ids_at gives numeric identifiers for a caller that maps them itself, and timezone_names_at gives the IANA names directly. The comment in the example is explicit that the arrays are one per axis rather than paired, so the longitude list and the latitude list are parallel arrays of equal length.
The examples directory backs this up with scripts for aware_datetime, batch_processing, get_geometry, get_offset, introduction, parallel_multiprocessing, parallel_threading and resource_management. The two parallel examples and the resource management one together tell you what the library expects you to care about: how to fan out across processes or threads, and how to release the data when you are done.
Memory, dataset size and the project around it
The library uses the full original timezone dataset with all over 440 timezone names, which the readme says provides full localization capabilities and historical timezone accuracy. For applications that prefer a smaller memory footprint, there is a reduced timezones-now dataset available through the update_data.sh script.
So the size of the install is the main trade-off you are making, and it is a real one. The full dataset means historical timezones are present, so a coordinate in a region that has changed its zone since the data was compiled still resolves to the right historical name, which matters if you are converting old timestamps. The reduced dataset trades that away for footprint.
The top level of the repository shows how seriously the data pipeline is taken. There are three files at the root, DATA_SOURCE, DATA_VERSION and DATA_BUILD_RUN, which together describe where the data comes from, which version it is and how it was built. There is a packages/ directory and a prototypes/ directory, and a scripts/ directory.
The Makefile is the place to look for the workflow, and its target comments are unusually explicit. The bootstrap target populates the packaged boundary data from the published timezonefinder-data wheel, is idempotent, and re-fetches when the checkout moves to a commit declaring a different data version. The check-data target fails with the bootstrap hint if that data is missing or stale. That pairing is a good pattern, because it means a developer who has moved to a commit with a new data version is told what to do rather than being left with subtly wrong results.
The test targets are also split by cost: test runs the unit tests excluding integration and slow tests, testint runs the integration tests only, and testall runs everything including the slow ones. The benchmark targets are separate again, with benchmarks running the full pytest-benchmark suite and benchmarks-ci described as the exact core-subset measurement the benchmark CI workflow records.
The build tooling is uv, with a uv.lock file, a pyproject.toml, a tox.ini and a pre-commit configuration, and the badge row in the readme shows the linter is ruff. The repository also carries a .cursor directory, a .claude directory and an AGENTS.md and CLAUDE.md, which is the current convention for shipping repository guidance to coding assistants.
The Python support matrix in the pyproject is worth reading for the reasoning rather than the numbers. The classifiers list 3.12, 3.13 and 3.14, and a comment explains that the floor follows the official NumPy support and deprecation schedule, NEP 29, and that 3.11 reached its drop date on 2026-04-24, which is the next version to drop. There is a free-threaded classifier at 3 - Stable, with a comment explaining that it is not rated 4 - Resilient because cleanup on a shared instance must wait for every thread using it, and that one thread-safety limit is documented.
Editorial conclusion
Adopt timezonefinder if you need timezone lookup for coordinates in an environment where you cannot make a network call, or if your coordinates cluster near timezone borders and an approximate answer is not good enough. Do not adopt it if you want a small memory footprint and can live with fewer timezone names, because the full dataset with over 440 timezone names is what makes it heavy. Before you commit, pin the timezonefinder-data version, because a dataset update can change the answer for a given coordinate and that is the one thing that will silently break a reproducible deployment.
Frequently asked questions
What does timezonefinder do?
It provides offline timezone lookup for WGS84 coordinates, mapping a latitude and longitude to an IANA timezone name without contacting a network service. It is a Python package, MIT licensed, published on PyPI and conda-forge, and it uses the full original timezone dataset with over 440 timezone names for full localization and historical accuracy.
Why does timezonefinder not simplify its timezone polygons?
Because simplification moves the border slightly, which would give the wrong answer for coordinates close to a real border. The readme states that the boundary polygons are never simplified, so border accuracy is limited only by the source dataset, and that a grid shortcut index is what makes carrying full-resolution geometry affordable.
How does timezonefinder handle a coordinate that matches several timezones?
The dataset contains genuinely overlapping zones, so a coordinate can match more than one. The timezone_at method picks the smaller one, and the timezones_at method returns all of them. The single-value method is therefore a convenience with a documented tie-break rule.
What are the acceleration backends in timezonefinder?
Three, selected at import time in order: a clang-compiled C extension built automatically when a compiler is available, Numba JIT compilation used only where the extension is unavailable and the numba extra is installed, and pure Python. All three return identical answers, so a missing compiler costs speed and never results.
How do I keep timezonefinder results reproducible across deployments?
Pin the separate timezonefinder-data distribution, for example with pip install timezonefinder "timezonefinder-data==<version>". The boundary data ships independently of the library so a new dataset does not require a new release, but the readme warns that a dataset update can change the answer for a coordinate, so pinning is what keeps a result stable.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jannikmi-timezonefinder)Was this useful?