Guide

Overpass API in Python

Three ways to query Overpass from Python, from plain requests to the overpy and overpass packages, with the server URL, retries for 429 and 504, GeoJSON and pandas. Every example here was run against overpass.ch.

Overpass is one HTTP endpoint. POST your query as the form field data and read the JSON:
import requests

r = requests.post("https://api.overpass.ch/v1/YOUR_API_KEY/interpreter",
                  data={"data": '[out:json];node["amenity"="drinking_water"](46.00,7.73,46.03,7.77);out;'},
                  timeout=120)
r.raise_for_status()
print(len(r.json()["elements"]))

1. requests, with retries

For scripts and services, requests on its own is enough. Add a retry for the answers that mean “try again later”: 429 (rate limit), 503 and 504 (server busy or timeout). Overpass also reports some errors inside a successful answer, in remark:

import os
import time
import requests

URL = f"https://api.overpass.ch/v1/{os.environ['OVERPASS_CH_KEY']}/interpreter"

def overpass(query, tries=5):
    for attempt in range(tries):
        r = requests.post(URL, data={"data": query}, timeout=200)
        if r.status_code in (429, 503, 504) and attempt < tries - 1:
            time.sleep(float(r.headers.get("Retry-After", 10 * 2 ** attempt)))
            continue
        r.raise_for_status()
        data = r.json()
        if "runtime error" in data.get("remark", ""):
            raise RuntimeError(data["remark"])   # e.g. "Query timed out"
        return data

data = overpass("""
[out:json][timeout:60];
area["ISO3166-2"="CH-VS"]->.valais;
nwr["tourism"="alpine_hut"](area.valais);
out center;
""")
for el in data["elements"]:
    print(el["tags"].get("name"), el.get("lat") or el["center"]["lat"])
  • One query at a time. Parallel requests are the most common cause of 429 on public servers. On overpass.ch, your plan sets how many queries may run at once.
  • Retry-After says how many seconds to wait. Public servers don't always send it, hence the doubling fallback.
  • More on each error: Overpass errors explained.

2. overpy

overpy turns the answer into Python objects with nodes, ways and relations, and raises an exception for each kind of error. Pass the full interpreter URL:

import overpy

api = overpy.Overpass(url="https://api.overpass.ch/v1/YOUR_API_KEY/interpreter")
result = api.query("""
[out:json][timeout:25];
way["highway"="path"](46.015,7.74,46.02,7.75);
(._;>;);
out body;
""")
for way in result.ways:
    nodes = way.get_nodes(resolve_missing=False)
    print(way.id, way.tags.get("name"), len(nodes), "nodes")

Errors arrive as exceptions such as overpy.exception.OverpassBadRequest (a syntax error) and OverpassTooManyRequests. Tested with overpy 0.7.

3. overpass (GeoJSON)

The overpass package wraps your filter in a complete query and can return GeoJSON directly, which is handy for maps:

import overpass

api = overpass.API(endpoint="https://api.overpass.ch/v1/YOUR_API_KEY/interpreter", timeout=60)
geojson = api.get('nwr["amenity"="drinking_water"](46.00,7.73,46.03,7.77)',
                  verbosity="center", responseformat="geojson")
print(len(geojson["features"]))

Version 0.8 warns that API and the responseformat and verbosity arguments will be replaced in 1.0. The calls above work in 0.8.2.

Into pandas

For tables, ask Overpass for CSV and read it directly. The columns are the tags you list, plus ::id, ::lat and ::lon for the ID and position of the element:

import io
import pandas as pd

r = requests.post(URL, data={"data": """
[out:csv(::id, ::lat, ::lon, name, ele; true; ",")][timeout:25];
node["natural"="peak"]["name"](45.90,7.60,46.10,7.95);
out;
"""}, timeout=120)
r.raise_for_status()
peaks = pd.read_csv(io.BytesIO(r.content))   # bytes: pandas reads them as UTF-8

The columns are named @id, @lat, @lon, name and ele. Passing the bytes works with any server. Overpass sends CSV as UTF-8 without saying so in the content type, and requests then decodes r.text as Latin-1, which garbles names like Dent d'Hérens. On overpass.ch r.text works too, because we add charset=utf-8.

From JSON, pd.json_normalize(data["elements"]) gives one column per tag (tags.name, tags.ele, …).

Heights in the same request

On overpass.ch, add elevation=1 to the URL and every node, centre and geometry point of a JSON answer gets an elevation in metres. Each one uses a lookup from your elevation allowance:

r = requests.post(URL, params={"elevation": 1}, data={"data": query}, timeout=200)

See the elevation examples for peaks, huts, lifts and route profiles.

OSMnx and OSMPythonTools

  • OSMnx has its own settings for the server; see the OSMnx guide.
  • OSMPythonTools reads the status page of the server before every query and expects the plain-text format of overpass-api.de. overpass.ch answers in that format, so you only need to set the endpoint (with a trailing slash):
    from OSMPythonTools.overpass import Overpass
    
    op = Overpass(endpoint="https://api.overpass.ch/v1/YOUR_API_KEY/")
    peaks = op.query('node["natural"="peak"](45.90,7.60,46.10,7.95); out;', timeout=25)
    On servers whose status page it cannot read, it stops with “could not fetch or interpret status of the endpoint”.