How to Get Roads by Type From OpenStreetMap in Python
On this page
Problem statement
Every road in OpenStreetMap carries a highway tag that says what kind of road it is, from motorway down to residential, service and footway. That makes OSM the easiest open source for questions like "how many kilometres of each road class does this city have?" or "where are the main roads?". The answers depend on details that are easy to miss. Footpaths and service roads are highways too. Dual carriageways are mapped as two ways. Lengths have to be measured in a projected CRS, and a regional extract reaches beyond the city's boundary.
This page extracts every highway in Washington, D.C. from the OpenStreetMap extract of 1 January 2026. It measures length by class inside the District's boundary, groups the classes into a usable hierarchy, finds the longest named streets, and checks how often the maxspeed and lanes tags are filled in.
Quick answer
Read the highway ways as lines, clip to the boundary, and sum lengths in metres:
fp = (osmium.FileProcessor(str(PBF)).with_locations()
.with_filter(osmium.filter.EntityFilter(osmium.osm.WAY))
.with_filter(osmium.filter.KeyFilter("highway"))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["highway", "name", "maxspeed", "lanes", "oneway"])))
H = gpd.GeoDataFrame.from_features(fp, crs=4326)
H = gpd.clip(H[H.geom_type == "LineString"], DC).to_crs(32618)
H["km"] = H.length / 1000
km = H.groupby("highway").km.sum().sort_values(ascending=False)
print(f"{len(H):,} highway ways inside D.C., {H.km.sum():,.0f} km")
print(km.head(10).round(0).astype(int).to_string())
81,649 highway ways inside D.C., 6,356 km
highway
footway 2472
service 1439
residential 1042
tertiary 277
primary 265
secondary 211
cycleway 190
path 94
motorway 75
unclassified 65
Inside the District, 81,649 highway ways add up to 6,356 km. Footways are the longest class by far, 2,472 km, almost as long as service roads (1,439 km) and residential streets (1,042 km) together, because D.C.'s sidewalks are mapped as separate ways.
Group the classes, the longest streets, detail completeness
1. Group the classes
Thirty-odd highway values collapse into a few groups for mapping and statistics:
GROUP = {"motorway": "major road", "trunk": "major road", "primary": "major road", "secondary": "major road",
"tertiary": "minor road", "residential": "minor road", "unclassified": "minor road", "living_street": "minor road",
"service": "service", "footway": "path", "path": "path", "pedestrian": "path", "steps": "path", "cycleway": "path", "track": "path"}
H["group"] = H.highway.str.replace("_link", "", regex=False).map(GROUP).fillna("other")
print(H.groupby("group").km.sum().sort_values(ascending=False).round(0).astype(int).to_dict())
{'path': 2814, 'service': 1439, 'minor road': 1389, 'major road': 675, 'other': 39}
Grouped, paths account for 2,814 km, service roads 1,439 km, minor roads 1,389 km and major roads 675 km.
2. The longest named streets
A street is many ways, split wherever a tag changes. Sum by name:
streets = H[H.group.isin(["major road", "minor road"]) & H.name.notna()].groupby("name").km.sum().sort_values(ascending=False)
print(f"named streets: {len(streets):,}")
print(streets.head(6).round(1).to_string())
named streets: 1,901
name
Anacostia Freeway 24.3
16th Street Northwest 16.7
14th Street Northwest 11.7
New York Avenue Northeast 11.6
Massachusetts Avenue Northwest 11.2
Connecticut Avenue Northwest 9.9
The 1,901 named streets are led by the Anacostia Freeway at 24.3 km, a figure that counts both carriageways, and 16th Street Northwest at 16.7 km.
3. How complete the details are
maxspeed and lanes are optional. Measure their coverage by length on the major roads:
major = H[H.group == "major road"]
for tag in ("maxspeed", "lanes", "oneway"):
print(f"{tag:>9}: on {major.loc[major[tag].notna(), 'km'].sum() / major.km.sum():.0%} of major-road length")
print("maxspeed values:", major.maxspeed.value_counts().head(5).to_dict())
maxspeed: on 50% of major-road length
lanes: on 99% of major-road length
oneway: on 67% of major-road length
maxspeed values: {'25 mph': 1400, '30 mph': 574, '40 mph': 200, '35 mph': 164, '50 mph': 115}
lanes is on 99% of the major-road length and maxspeed on only 50%, almost all of it as strings in miles per hour, 25 mph most often.
Dual carriageways in D.C.
The data used on this page
The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 2026 (Β© OpenStreetMap contributors, ODbL), clipped to the District's boundary from the US Census Bureau's 2023 cartographic boundary files at 1:500,000. Lengths are measured in UTM zone 18N (EPSG:32618).
from pathlib import Path
import geopandas as gpd
import osmium
import requests
Path("data").mkdir(exist_ok=True)
def cached(path, url):
p = Path(path)
if not p.exists():
r = requests.get(url, timeout=600)
r.raise_for_status()
p.write_bytes(r.content)
return p
PBF = cached("data/district-of-columbia-260101.osm.pbf", "https://download.geofabrik.de/north-america/us/district-of-columbia-260101.osm.pbf")
C = gpd.read_file(cached("data/cb_2023_us_county_500k.zip", "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_county_500k.zip"))
DC = C[C.GEOID == "11001"].to_crs(4326)
print(f"D.C.: {DC.to_crs(32618).area.iloc[0] / 1e6:.1f} kmΒ²")
D.C.: 176.9 kmΒ²
The District's boundary covers 176.9 kmΒ², water included.
Example 1: dual carriageways
Divided roads are mapped as two one-way ways, one per direction, so their length counts twice. Measure how much of the major-road length is one-way:
ow = major.oneway.isin(["yes", "-1"])
print(f"one-way share of major-road length: {major.loc[ow, 'km'].sum() / major.km.sum():.0%}")
print(f"motorway and trunk: {H[H.highway.isin(['motorway', 'trunk'])].km.sum():.0f} km of ways, of which one-way {H[H.highway.isin(['motorway', 'trunk']) & H.oneway.isin(['yes', '-1'])].km.sum():.0f} km")
one-way share of major-road length: 62%
motorway and trunk: 118 km of ways, of which one-way 98 km
62% of the major-road length is one-way. Of the 118 km of motorway and trunk ways, 98 km are one-way carriageways, so the roads themselves are much shorter than the sum of their ways.
The highway key describes a role, not a size
The highway key describes a way's role in the network, not its physical size. motorway, trunk, primary, secondary and tertiary form the hierarchy of through roads. residential and unclassified are local streets. service covers driveways, parking aisles and alleys, and footway, cycleway, path and steps are for people on foot or bike. The _link suffix marks ramps and slip roads. In a dense city the non-car classes often add up to more length than the streets, because every sidewalk mapped as its own way counts.
Turning ways into kilometres needs three corrections. Lengths must be measured in metres in a projected CRS, here UTM 18N, not in degrees. The extract must be clipped to the area of interest, because a Geofabrik region includes a margin. And divided roads, mapped as one way per direction, count twice. That is right for lane-kilometres of carriageway, but wrong for "how long is this road". Attributes like maxspeed and lanes are added by mappers when they know them, so their coverage should be measured before they are used.
Pedestrian areas, proposed roads and speeds
- Areas:
highway=pedestriansquares can be polygons; read withwith_areas()if plazas matter. - Construction and proposed roads:
highway=constructionandhighway=proposedare not open roads; exclude them. - Routing: for a connected graph with nodes at intersections, use OSMnx rather than raw ways.
- Speeds:
maxspeedvalues are strings such as25 mph; see parsing maxspeed.
Internal links
- How to read an OpenStreetMap PBF file in Python β reading the extract
- How to build a street network graph in Python with OSMnx β roads as a graph
- How to map cycle lanes from OpenStreetMap in Python β the cycling network
FAQ
How do I get roads from OpenStreetMap by type?
Filter ways with the highway key and group by its value; motorway to tertiary are through roads, residential and unclassified local streets.
How do I calculate road length from OSM data?
Project the lines to a metric CRS such as UTM and sum geometry.length; clip to your boundary first.
Why are some roads counted twice?
Divided roads are mapped as two one-way ways, one for each direction.
What does highway=service mean?
Driveways, alleys, parking aisles and other access roads; usually excluded from street statistics.