Benchmarking PyGeoHash: Where It Lands Against the Field
A while back the Apache Superset project swapped out python-geohash for PyGeoHash. The reason was painfully familiar to anyone who has shipped a library with a C extension: python-geohash ships a C++ extension that refuses to build in the slim Docker images a lot of teams run, and the build just falls over when there is no compiler in the container. PyGeoHash ships pre-built wheels, so it just installs. The Superset maintainer made the switch and noted, more or less in passing, that the new library was slower but worth it for the portability.
That offhand comment stuck with me. Slower than what, and by how much? Nobody had put numbers on it. So I did.
A quick word on pytest-benchmark
If you have never used it, pytest-benchmark is a pytest plugin that turns timing code into something that looks like a normal test. You hand it a callable, it runs that callable many times, and it reports the statistics back to you. A benchmark looks like this:
def test_encode_benchmark(benchmark):
result = benchmark(lambda: pgh.encode(42.6, -5.6))
assert len(result) > 0
The benchmark fixture does the heavy lifting. It warms up, figures out how many rounds it needs to get a stable measurement, runs them, and then prints a table with the min, mean, ops per second, and the spread. Because it is just a fixture, your benchmarks live next to your unit tests and run with the same pytest command. You enable them with --benchmark-enable so they do not slow down a normal test run.
The part I lean on most is grouping. If you tag a set of benchmarks with the same group, pytest-benchmark prints them in one table and computes how many times slower each row is than the fastest one. That is exactly what you want when you are comparing libraries head to head.
The two benchmarks
PyGeoHash has carried a small benchmark suite for a while. It measures the core operations against themselves, which is useful for catching regressions release to release. On my M-series Mac it looks like this:
| Operation | Mean | Throughput |
|---|---|---|
| encode | 422 ns | 2.4M ops/sec |
| decode | 1,383 ns | 0.7M ops/sec |
| approximate distance | 464 ns | 2.2M ops/sec |
| haversine distance | 3,013 ns | 0.3M ops/sec |
Those are fine numbers in isolation, but they do not answer the Superset question. So I added a second benchmark that pits PyGeoHash against six other geohash libraries on the three operations that actually matter to most users: encode, decode, and bounding box lookup. Every library in the comparison computes the standard geohash, so it is an apples to apples race. I left out geohash-hilbert, which uses a different curve, and mzgeohash, which does not take a precision argument, because neither would have been a fair fight.
Here is where everyone lands on encode, sorted fastest to slowest:
| Library | Implementation | Mean (ns) |
|---|---|---|
| geohashr | Rust | 116 |
| pygeohash-fast | Rust | 154 |
| python-geohash | C++ | 203 |
| pygeohash | C extension | 415 |
| libgeohash | pure Python | 3,978 |
| geohash-tools | pure Python | 5,585 |
| geolib | pure Python | 12,283 |
Decode and bounding box tell the same story. The Rust libraries lead, the C++ python-geohash follows, PyGeoHash sits a step behind them, and then there is a cliff. The pure Python implementations are an order of magnitude or two further down. Decoding with geolib, for instance, takes about 63 microseconds against PyGeoHash’s 1.6.
So is it slow?
It depends entirely on who you stand it next to. PyGeoHash is slower than the libraries that drop down into Rust or C++ for the hot path. The Superset maintainer was right. PyGeoHash is also roughly ten to forty times faster than every pure Python geohash library I could find. That is the part the offhand comment left out.
I am comfortable with that spot. PyGeoHash trades the last bit of raw speed for wheels that install everywhere without a toolchain, which is the whole reason Superset reached for it in the first place. If you are encoding a few million points in a tight loop and you control your build environment, reach for one of the Rust libraries. If you want something that just works in any container, on any platform, from Python 3.8 onward, the gap between PyGeoHash and the native-code libraries is a couple hundred nanoseconds per call. For almost every workload, that is noise.
The benchmark lives in the repo now, behind an optional benchmark extra so it does not drag a C++ or Rust toolchain into CI. If you want to run it yourself:
uv run pytest tests/test_benchmark_comparison.py --benchmark-enable \
--benchmark-group-by=group --benchmark-sort=mean
The pull request that added it is here. If I missed a library worth including, let me know and I will add it.
Stay in the loop
Get notified when I publish new posts. No spam, unsubscribe anytime.