geopy: GeocoderInsufficientPrivileges, GeocoderRateLimited and ConfigurationError
What each geopy exception means, why the Nominatim geocoder refuses some user agents, why a geocoded column fills with None, and settings for RateLimiter that keep a script within the limits. Checked with geopy 2.5.0.
From HTTP status to exception
geopy turns every answer other than 200 into an exception. All of them derive from GeocoderServiceError:
| HTTP status | Exception | Usually |
|---|---|---|
| 400, 412, 413, 414 | GeocoderQueryError | A wrong parameter, or a query that is too long. |
| 401, 407 | GeocoderAuthenticationFailure | A missing or wrong API key. |
| 402 | GeocoderQuotaExceeded | The paid allowance is used up. |
| 403 | GeocoderInsufficientPrivileges | Blocked by the public Nominatim, or a key not allowed for this request. |
| 408, 503, 504 | GeocoderTimedOut | The service is overloaded or did not answer in time. |
| 429 | GeocoderRateLimited | Too many requests; a subclass of GeocoderQuotaExceeded. |
| 502 | GeocoderServiceError | The service failed. |
| no answer in time | GeocoderTimedOut | The default timeout is 1 second; give timeout=10 to the geocoder. |
The message of a status error reads like Non-successful status code 403.
ConfigurationError: “Using Nominatim with default or sample user_agent”
For the public server nominatim.openstreetmap.org, geopy refuses the default user agent (geopy/2.5.0) and the samples from its documentation (my-application, my_app/1, my_user_agent/1.0, specify_your_app_name_here). It raises ConfigurationError before sending anything, because such requests break the usage policy and end in 403 or 429. Name your own app:
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="shop-locator/1.2 (admin@example.com)", timeout=10)
print(geolocator.geocode("Bundesplatz 3, Bern"))
The check applies only to the public server. With another domain, geopy sends what you set.
GeocoderInsufficientPrivileges: Non-successful status code 403
With the public Nominatim this means the server blocked the client: no proper user agent, more than one request a second, the same queries again and again, or autocomplete. The details are in Nominatim: “Access blocked”, 403 and 429. Other services answer 403 for a key that is not allowed from this place, such as a Google key restricted to websites used from a server.
A script that works on your computer and gets 403 on a cloud host (Streamlit Cloud, Colab, a shared CI runner) may share its IP address with other clients that were blocked.
GeocoderRateLimited: 429
The service wants fewer requests. Its Retry-After header, when sent, is in e.retry_after. Wait at least that long, then go on more slowly.
RateLimiter, and why every row is None
RateLimiter spaces the calls. Its defaults (geopy 2.5.0) are min_delay_seconds=0.0, max_retries=2, error_wait_seconds=5.0, swallow_exceptions=True and return_value_on_exception=None:
- With the default delay of 0 it does not slow anything down. Set
min_delay_seconds=1for the public Nominatim. - Every
GeocoderServiceErroris retried, so a 403 block is sent again twice, 5 seconds apart. - After the last try it returns
Noneand writes a warning to the log. A blocked run therefore fills the whole column withNone. Setswallow_exceptions=Falsewhile testing, so that the first error stops the run.
import pandas as pd
from geopy.extra.rate_limiter import RateLimiter
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1, swallow_exceptions=False)
df = pd.DataFrame({"address": ["Bundesplatz 3, Bern", "Rue du Rhône 42, Genève"]})
unique = df["address"].drop_duplicates() # each address once
found = {a: geocode(a) for a in unique}
df["lat"] = df["address"].map(lambda a: found[a].latitude if found[a] else None)
df["lon"] = df["address"].map(lambda a: found[a].longitude if found[a] else None)
At one request a second, 10,000 addresses take almost three hours, and the usage policy discourages bulk geocoding of larger amounts on the public server.
None without an error
geocode() returns None when the service found nothing. Services on OpenStreetMap data know only the house numbers mapped there; try the street and town alone, check for a stray country or postcode, and leave out apartment numbers and c/o lines.
Use another server
The Nominatim and Photon geocoders of geopy take a domain. With overpass.ch the key goes into it, and the answers keep their format:
from geopy.geocoders import Nominatim, Photon
KEY = "YOUR_KEY"
nominatim = Nominatim(domain=f"api.overpass.ch/v1/{KEY}/nominatim", user_agent="shop-locator/1.2", timeout=10)
photon = Photon(domain=f"api.overpass.ch/v1/{KEY}/geocode", user_agent="shop-locator/1.2", timeout=10)
print(nominatim.geocode("Bundesplatz 3, Bern", addressdetails=True).raw["address"])
print(photon.reverse("47.3769, 8.5417"))
Errors from overpass.ch become the same exceptions: 401 GeocoderAuthenticationFailure, 403 GeocoderInsufficientPrivileges, 429 GeocoderRateLimited. The plans allow far more than one request a second; see pricing.
Geocoding from overpass.ch
The Nominatim and Photon APIs on OpenStreetMap data, tested with geopy every night. 100'000 requests a month are free, and you may store the results.