Skip to main content

openwalkscore

Open, transparent, API-key-free walkability scores (0-100) for any point or area on Earth, computed from OpenStreetMap.

CI PyPI Python License: MIT

openwalkscore measures how many everyday destinations (groceries, cafes, schools, parks, clinics, transit stops and more) you can reach on foot, and how walkable the street network around you is. Every number in the method is published below and lives in an editable profile, so a score can always be reproduced, audited and adapted.

It is written for urban data scientists, real-estate and proptech developers, planners and students who need a walkability indicator they can explain, and that works for any city.

Disclaimer. openwalkscore is an independent open-source project. It is not affiliated with, endorsed by or connected to Walk Score® or Redfin. Walk Score® is a registered trademark of its owner. openwalkscore uses its own method and its own data (OpenStreetMap); its scores are not comparable to Walk Score® values and must not be presented as such.


Contents

Install

pip install openwalkscore
# optional: run shortest paths with networkx instead of the built-in Dijkstra
pip install "openwalkscore[networkx]"

Python 3.9 or newer. The only runtime dependency is certifi (plus tomli on Python < 3.11). No API key, no account, no osmnx/geopandas stack required.

Quick start

from openwalkscore import score_point

r = score_point(48.8530, 2.3499)          # Ile de la Cite, Paris
print(r.score, r.rating)                  # e.g. 100 Excellent
print(r.subscores)                        # {'grocery': 100.0, 'restaurants_cafes': 100.0, ...}
print(r.explain())                        # plain-text, line-by-line justification

for hit in r.nearest_amenities(limit=5):
    print(f"{hit.distance_m:5.0f} m  {hit.category:<18} {hit.name}")

The first call downloads amenities and the walking network around the point from the public Overpass API (this can take up to a minute in a dense city centre) and caches the response on disk; repeated calls are instant.

openwalkscore score --lat 48.8530 --lon 2.3499
openwalkscore explain --lat 48.8530 --lon 2.3499
openwalkscore grid --bbox 2.340,48.850,2.360,48.860 --cell 200 -o paris.geojson --html paris.html

Methodology

For a point p and a profile with categories c (weight W_c):

1. Amenity categories

Each OpenStreetMap feature with a location (node, or way/relation centroid) is assigned to the first category whose tag rules match (shop=* comes after the grocery and healthcare rules, so a supermarket counts as grocery, not shopping). Default profile:

Category Weight Counted (n) Ratio (r) Tags (abridged)
Grocery 3 2 0.4 shop=supermarket/convenience/greengrocer/bakery/butcher/deli, amenity=marketplace
Restaurants & cafes 3 10 0.8 amenity=restaurant/cafe/fast_food/pub/bar/food_court/ice_cream
Healthcare 1 2 0.5 amenity=pharmacy/clinic/doctors/hospital/dentist, shop=chemist, healthcare=*
Shopping 2 5 0.7 shop=* (except vacant, car, car_parts, fuel)
Schools 1 1 - amenity=school/kindergarten/childcare/college/university
Parks & play 1 2 0.5 leisure=park/playground/garden/recreation_ground/nature_reserve (not access=private)
Banks & post 1 1 - amenity=bank/atm/bureau_de_change/post_office
Culture & community 1 2 0.5 amenity=library/theatre/cinema/arts_centre/community_centre, tourism=museum/gallery
Transit stops 2 3 0.5 highway=bus_stop, public_transport=platform, railway=station/halt/tram_stop/subway_entrance, amenity=ferry_terminal

Run openwalkscore explain (no coordinates) to print the complete profile as JSON.

2. Walking distance

  • The walking network is built from OSM ways with a walkable highway=* value (footway, path, pedestrian, steps, living_street, residential, service, tertiary ... primary, track, cycleway, ...). Motorways and trunk roads are excluded unless foot=yes; foot=no, access=private and foot=use_sidepath are respected.
  • Every pair of consecutive way nodes is an undirected edge weighted by its great-circle length.
  • The scored point and every amenity are snapped to the nearest edge (not node), and the perpendicular access legs are added to the route.
  • Shortest paths come from a pure-Python multi-source Dijkstra that starts part-way along the origin edge and stops at the decay cut-off (1.6 km). engine="networkx" gives identical results with networkx.
  • Fallback: amenities more than max_snap_m (250 m) from the network, or every amenity when the point itself has no network nearby, use straight-line distance x detour factor 1.3. The result reports distance_mode (network, straight-line or mixed) per amenity and overall.
  • An amenity that is close but not connected to the point's network (for example across a river with no bridge in the data) gets no credit.

3. Distance decay

Credit for an amenity at walking distance d:

f(d) = 1                                   if d <= 400 m
f(d) = 0.5 * (1 + cos(pi * t))             if 400 m < d < 1600 m,  t = (d - 400) / 1200
f(d) = 0                                   if d >= 1600 m

The cosine taper is smooth at both ends: an amenity 700 m away earns 0.85, 1 km away 0.50, 1.3 km away 0.15. A linear ramp is available (decay.shape = "linear").

4. Diminishing returns

Within a category the amenities are ranked by credit and the best n are kept. The k-th one is weighted by

s_k = r^(k-1) / (1 + r + ... + r^(n-1))

so the shares sum to 1 and each extra cafe adds less than the previous one. The category subscore is S_c = sum_k s_k * f(d_k), between 0 and 1. Example (restaurants & cafes, n = 10, r = 0.8): one cafe next door is worth 22 % of the category, five cafes 75 %, ten cafes 100 %.

5. Access score

A = 100 * sum_c W_c * S_c / sum_c W_c

6. Network penalties

Measured within analysis_radius_m (800 m) of the point:

Penalty Metric No penalty at Full penalty at Max
Intersection density nodes where >= 3 street edges meet, per km² >= 90 /km² <= 30 /km² 10 %
Block length mean length of street segments between intersections / dead ends <= 120 m >= 240 m 10 %
Slope (only with a DEM) length-weighted mean absolute grade of those segments <= 3 % >= 15 % 10 %

Each penalty is linear between its two thresholds. They combine multiplicatively, P = 1 - prod_i (1 - P_i), capped at max_total_penalty (30 %). Penalties are skipped (and the explanation says so) when the point has no network nearby.

7. Final score

score = round(A * (1 - P))        # integer 0..100
Score Rating
90-100 Excellent
70-89 High
50-69 Moderate
25-49 Low
0-24 Very low

Profiles

Built-in profiles: default, family (schools, parks and healthcare weighted up), senior (300 m full credit, 1 km cut-off, healthcare and transit weighted up, stronger slope penalty) and transit (transit stops dominate).

A custom profile overrides a base profile key by key, from a dict or a TOML file:

# nightlife.toml
[profile]
name = "nightlife"
base = "default"

[profile.decay]
full_m = 300
zero_m = 1200
shape = "linear"

[profile.categories.restaurants_cafes]
weight = 6
max_count = 12

[profile.categories.nightclubs]          # a new category
weight = 2
tags = ["amenity=nightclub", "amenity=music_venue"]
max_count = 3
ratio = 0.6

[profile.penalties.slope]
max_penalty = 0.15
from openwalkscore import Profile, score_point

p = Profile.from_toml("nightlife.toml")
p = Profile.from_dict({"categories": {"shopping": {"weight": 0}}})   # switch a category off
score_point(52.52, 13.405, profile=p)
score_point(52.52, 13.405, profile="nightlife.toml")                  # a path works too

Unknown keys raise ValueError, so a typo never silently changes a score. Set replace_categories = true to start from an empty category list.

Python API

Function / class Purpose
score_point(lat, lon, profile="default", *, data=None, dem=None, engine="python", client=None, use_cache=True, name="") Score one point; returns WalkScoreResult.
score_grid(bbox, cell_m=250, profile="default", *, data=None, ...) Score square cells over bbox = (west, south, east, north); returns a GeoJSON FeatureCollection with score, rating, sub_<category> per cell.
compare(points, profile="default", *, data=None, ...) Score (lat, lon), (name, lat, lon) or {"name", "lat", "lon"} items; close points share one download.
WalkScorer(data, profile, dem=None, engine="python") Build the network once, call .score(lat, lon) / .grid(bbox, cell_m) many times.
WalkScoreResult score, rating, access_score, total_penalty, subscores, categories (with counted AmenityHits), penalties, metrics, distance_mode, explain(), nearest_amenities(), to_dict(), to_geojson_feature().
Profile, get_profile, list_profiles Profiles and their configuration.
OverpassClient(endpoints, timeout_s, cache_dir, cache_ttl_s, min_interval_s, retries) Download control.
load_osm(path_or_obj) Read .osm, .geojson, Overpass .json, or a dict.
WalkNetwork, dijkstra The graph and the shortest-path routine, usable on their own.
DEM.from_ascii_grid(path), DEM.from_function(fn) Elevation for the slope penalty.
render_html(obj), write_html(obj, path) Leaflet map of a result, a list of results or a grid.
from openwalkscore import WalkScorer, load_osm

scorer = WalkScorer(load_osm("district.osm"), profile="family")
results = [scorer.score(lat, lon) for lat, lon in listings]       # fast: graph built once

Using a DEM:

score_point(41.03, 28.98, dem="istanbul_wgs84.asc")              # ESRI ASCII grid in degrees

import rasterio                                                 # or any sampler you like
src = rasterio.open("dem_wgs84.tif"); band = src.read(1)
def elevation(lat, lon):
    row, col = src.index(lon, lat)
    return float(band[row, col])
score_point(41.03, 28.98, dem=elevation)

Command line

openwalkscore score   --lat LAT --lon LON [--profile P] [--json] [--html map.html]
openwalkscore explain [--lat LAT --lon LON] [--profile P]
openwalkscore grid    --bbox WEST,SOUTH,EAST,NORTH --cell 250 -o grid.geojson [--html map.html]
openwalkscore compare --point "Home,41.03,28.98" --point "Office,41.04,29.00" [--json]
openwalkscore profiles

Common options: --input FILE (offline .osm/.geojson/.json), --dem FILE.asc, --engine python|networkx, --timeout SECONDS, --endpoint URL (repeatable), --no-cache, --cache-dir DIR. Exit codes: 0 ok, 2 invalid input, 3 download failed.

explain without coordinates prints the method summary and the full profile.

Data sources and offline use

Overpass API (default). OverpassClient sends one query per area (amenities with out center, plus the walkable ways and their nodes) to public Overpass instances, trying overpass-api.de, overpass.kumi.systems and overpass.private.coffee in turn. It:

  • sets the generic User-Agent openwalkscore/<version>;
  • applies a network timeout (default 60 s) and retries HTTP 429/502/503/504 with back-off before failing over to the next mirror;
  • waits at least 1 s between requests from the same process;
  • caches responses on disk for 7 days (%LOCALAPPDATA%\openwalkscore\cache, ~/.cache/openwalkscore, or OPENWALKSCORE_CACHE_DIR);
  • refuses areas with a diagonal above 25 km. For a whole city, download an extract once (e.g. from Geofabrik, then clip with osmium) and pass it with data= / --input.

Please follow the Overpass usage policy: keep areas small and avoid parallel bulk jobs.

Offline input. data= (or --input) accepts:

  • .osm / .xml - OSM XML (JOSM, OSM API, osmium cat -f osm);
  • Overpass JSON ({"elements": [...]}) saved from overpass-turbo or the API;
  • GeoJSON - points become amenities, LineStrings with a highway property become the network (touching vertices are joined), polygons become amenities at their centroid. Tags are read from properties or properties.tags.

HTML maps

from openwalkscore import score_point, score_grid, write_html
write_html(score_point(40.4168, -3.7038), "madrid.html")
write_html(score_grid((-3.71, 40.41, -3.69, 40.425), 200), "madrid_grid.html")

The page loads Leaflet 1.9.4 from unpkg with Subresource Integrity hashes and shows OpenStreetMap tiles with the required attribution. Data is embedded as JSON with <, > and & escaped, and all OSM-supplied text (names, tags) is inserted with textContent, so feature names cannot inject markup or script.

Limitations

  • OSM completeness varies. Missing shops or footpaths lower the score; a score is a statement about the map as much as about the place. Compare places within regions of similar mapping quality.
  • Sidewalks mapped as separate footways add intersections and short segments; the density and block-length penalties are therefore lenient in such areas by design (they rarely trigger where sidewalks are mapped). Penalties near the edge of a local extract can be overstated if the extract does not cover analysis_radius_m.
  • No crossing safety, traffic speed, sidewalk width, lighting, opening hours, shade, crime or amenity quality is modelled.
  • The network is treated as undirected and of uniform walking speed; steps and steep grades only enter through the optional slope penalty.
  • Amenity counts are capped per category; a mall with 200 shops counts as at most 5 shops in the default profile.

Development

git clone https://github.com/YusufEminoglu/openwalkscore_sdk
cd openwalkscore_sdk
pip install -e ".[dev]"
python -m pytest            # no network needed: tests use hand-made OSM fixtures
python -m ruff check . && python -m ruff format --check src tests && python -m mypy src

Citing

If you use openwalkscore in research, please cite it via CITATION.cff:

Eminoğlu, Y. (2026). openwalkscore: open, transparent walkability scores from OpenStreetMap (version 0.1.0) [Software]. https://github.com/YusufEminoglu/openwalkscore_sdk

License and attribution

Code: MIT, © 2026 Yusuf Eminoğlu. See LICENSE.

Map data: © OpenStreetMap contributors, available under the Open Database License (ODbL). When you publish scores or maps derived from OSM data, credit OpenStreetMap accordingly.

Walk Score® is a registered trademark of its owner; this project is not affiliated with it.

Metadata

Release files for openwalkscore 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openwalkscore 0.1.1
File Size Uploaded Provenance Actions
openwalkscore-0.1.1.tar.gz 69.1 kB With provenance Details for openwalkscore-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for openwalkscore 0.1.1
File Interpreter ABI Platform Actions
openwalkscore-0.1.1-py3-none-any.whl Python 3 none any Details for openwalkscore-0.1.1-py3-none-any.whl

Total release size: 118.0 kB

Release files / openwalkscore-0.1.1.tar.gz

Download URL openwalkscore-0.1.1.tar.gz
Size 69.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3d9bc2b5c5ecdff7d211dd32389fd4d092fd59cd0c5ebe327609251db09b1356
BLAKE2b-256 checksum
How to use checksums
0cc53e086a6590f8dfd33355ea2d1d19f23006c5a1f78343b9a0567b6225e5bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / openwalkscore-0.1.1-py3-none-any.whl

Download URL openwalkscore-0.1.1-py3-none-any.whl
Size 49.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d3ca8f4b61e6a0cb6991584e8cafe223562461ccf67cbd87e32f38119b9bce47
BLAKE2b-256 checksum
How to use checksums
5ab3a4b45a76c1665ba7235fd733ff0345097bd8915528d99ab396447d19e588
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for openwalkscore 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for openwalkscore-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page