How to Get Railway Lines and Stations From OpenStreetMap in Python

On this page

Problem statement

OpenStreetMap holds the most detailed open map of railways anywhere: main lines, metro, light rail and trams, with tracks, tunnels, stations and platforms. Extracting a rail network means knowing how it is modelled. Track is mapped as railway=rail, subway, light_rail or tram lines, often one way per track. Stations are nodes or areas with railway=station. The named services, such as Metro's Red Line, are route relations that group the tracks.

This page extracts the rail network of Washington, D.C. from the OpenStreetMap extract of 1 January 2026. It measures track length by type and how much of the Metro runs underground, lists the stations by type, and reads the route relations for the Metro lines.

Quick answer

Read the railway ways as lines and sum their length by type:

fp = (osmium.FileProcessor(str(PBF)).with_locations().with_filter(osmium.filter.EntityFilter(osmium.osm.WAY))
      .with_filter(osmium.filter.KeyFilter("railway"))
      .with_filter(osmium.filter.GeoInterfaceFilter(tags=["railway", "name", "tunnel", "service", "usage"])))
R = gpd.GeoDataFrame.from_features(fp, crs=4326)
R = gpd.clip(R[R.geom_type == "LineString"], DC).to_crs(32618)
R["km"] = R.length / 1000
print(f"{len(R):,} railway ways inside D.C.")
print(R.groupby("railway").km.sum().sort_values(ascending=False).head(8).round(1).to_string())
1,058 railway ways inside D.C.
railway
rail          136.9
subway        135.3
abandoned      23.1
platform       15.0
tram            9.5
disused         7.7
light_rail      1.7
turntable       0.1

Inside the District there are 1,058 railway ways. Main-line rail (136.9 km) and subway (135.3 km) dominate; the streetcar adds 9.5 km of tram. Platforms add 15.0 km of railway=platform ways that are not track at all, and abandoned and disused lines another 30.8 km.

Map of rail in D.C.: Metro lines in red, dashed where they run in tunnels through downtown and solid on the surface to the north-east and south-east, grey main-line tracks converging on Union Station, faint abandoned lines, and 52 stations as dots. Map of rail in D.C.: Metro lines in red, dashed where they run in tunnels through downtown and solid on the surface to the north-east and south-east, grey main-line tracks converging on Union Station, faint abandoned lines, and 52 stations as dots.
The rail network from OSM.

Main line, sidings and Metro, stations, route relations

1. Main line, sidings and the Metro underground

railway=rail includes yards and sidings, marked with service=*. The Metro is railway=subway, with tunnels tagged tunnel=yes:

rail = R[R.railway == "rail"]
print(f"rail: {rail.km.sum():.1f} km, of which yards, sidings and spurs (service=*): {rail.loc[rail.service.notna(), 'km'].sum():.1f} km")
sub = R[R.railway == "subway"]
under = sub.tunnel.isin(["yes", "building_passage"])
print(f"subway: {sub.km.sum():.1f} km of track ways; in tunnels: {sub.loc[under, 'km'].sum():.1f} km ({sub.loc[under, 'km'].sum() / sub.km.sum():.0%})")
rail: 136.9 km, of which yards, sidings and spurs (service=*): 47.7 km
subway: 135.3 km of track ways; in tunnels: 95.0 km (70%)

Of the 136.9 km of rail, 47.7 km are yards, sidings and spurs. Of the 135.3 km of Metro track ways, 95.0 km, 70%, run in tunnels: a map that hides tunnels loses most of the Metro inside the District.

Length of railway ways in Washington, D.C., by railway value. km of railway ways by typerail136.9 kmsubway135.3 kmabandoned23.1 kmplatform15.0 km (not track)tram (streetcar)9.5 km050 km100 km150 km1,058 railway ways inside D.C. spatialworkflow.io
Railway length by type.

2. Stations

Stations are tagged railway=station, with station=subway for the Metro and station=light_rail for the streetcar:

fp = (osmium.FileProcessor(str(PBF)).with_areas().with_filter(osmium.filter.TagFilter(("railway", "station")))
      .with_filter(osmium.filter.GeoInterfaceFilter(tags=["name", "station", "network"])))
S = gpd.GeoDataFrame.from_features(fp, crs=4326)
S = S[S.geom_type != "LineString"]
S = S[S.to_crs(32618).representative_point().to_crs(4326).within(DC.geometry.iloc[0])]
print(f"stations inside D.C.: {len(S)}; by type {S.station.fillna('(train)').value_counts().to_dict()}")
print("train stations:", sorted(S[S.station.isna()].name.dropna().unique()))
stations inside D.C.: 52; by type {'subway': 44, 'light_rail': 6, '(train)': 2}
train stations: ["L'Enfant", 'Washington Union Station']

The District has 52 stations: 44 tagged as Metro stations, 6 light-rail (streetcar) stops, and 2 train stations, Union Station and L'Enfant.

3. The Metro lines as route relations

Each Metro line is a route=subway relation listing its stops and tracks. DuckDB reads the relations and their tags directly:

con = duckdb.connect(); con.sql("INSTALL spatial; LOAD spatial")
routes = con.sql(f"""SELECT tags['name'] AS name, tags['colour'] AS colour, len(refs) AS members
                     FROM ST_ReadOSM('{PBF}') WHERE kind = 'relation' AND tags['route'] = 'subway' ORDER BY name""").df()
print(f"route=subway relations: {len(routes)}")
print(routes.to_string(index=False))
route=subway relations: 14
                                                   name  colour  members
WMATA Blue Line: Downtown Largo → Franconia–Springfield #009CDE      150
WMATA Blue Line: Franconia–Springfield → Downtown Largo #009CDE      154
            WMATA Green Line: Branch Avenue → Greenbelt #00B140       94
            WMATA Green Line: Greenbelt → Branch Avenue #00B140       94
 WMATA Orange Line: New Carrollton → Vienna/Fairfax–GMU #ED8B00      142
 WMATA Orange Line: Vienna/Fairfax–GMU → New Carrollton #ED8B00      121
                 WMATA Red Line: Glenmont → Shady Grove #BF0D3E      131
                 WMATA Red Line: Shady Grove → Glenmont #BF0D3E      133
            WMATA Silver Line: Ashburn → Downtown Largo #919D9D      204
            WMATA Silver Line: Ashburn → New Carrollton #919D9D      197
            WMATA Silver Line: Downtown Largo → Ashburn #919D9D      205
            WMATA Silver Line: New Carrollton → Ashburn #919D9D      200
    WMATA Yellow Line: Huntington → Mount Vernon Square #FFD100       65
    WMATA Yellow Line: Mount Vernon Square → Huntington #FFD100       67

The 14 route=subway relations are the six Metro lines, one relation per direction, with extra variants for the Silver Line to Downtown Largo and New Carrollton; the Yellow Line's pair runs between Huntington and Mount Vernon Square. Each carries its colour, for example #BF0D3E for the Red Line.

Abandoned and disused lines

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.

from pathlib import Path

import duckdb
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("boundary loaded")
boundary loaded

The District boundary is loaded for clipping.

Example 1: abandoned and disused lines

Old railways stay in OSM with lifecycle values such as abandoned and disused. Measure them separately:

old = R[R.railway.isin(["abandoned", "disused", "razed"])]
print(f"abandoned or disused railway: {old.km.sum():.1f} km in {len(old)} ways")
print(old.groupby("railway").km.sum().round(1).to_dict())
abandoned or disused railway: 30.8 km in 77 ways
{'abandoned': 23.1, 'disused': 7.7}

30.8 km of railway in 77 ways are abandoned or disused lines that stay in OSM for their history.

How OpenStreetMap models a rail system: track, stations and route relations. three layers of a rail systemroute relationsroute=subway, one per direction14 for the Metrostationsrailway=station, station=*52 inside D.C.trackrailway=rail, subway, light_rail70% of Metro in tunnelsInfrastructure below, service on top spatialworkflow.io
Track, stations and routes.

Infrastructure separated from service

Stations inside Washington, D.C. in OpenStreetMap, by type. stations inside D.C.counttagsMetro44railway=station, station=subwaystreetcar6railway=station, station=light_railtrain2railway=station (Union Station,L'Enfant)Nodes and areas, one point each spatialworkflow.io
Stations by type.

OpenStreetMap separates infrastructure from service. The infrastructure is the track: each railway=* way is a stretch of track, or of a track group on simpler maps, with tags for its type (rail, subway, light_rail, tram, narrow_gauge), its role (usage=main, service=siding, yard or spur), and its situation (tunnel=yes, bridge=yes). Stations are features of their own, tagged railway=station with public_transport=station, and platforms are separate again. The service, a named line that trains run along, is a route relation that lists the tracks and stops in order, one per direction in the public transport scheme.

That modelling explains the usual surprises. Track length is not route length, because double track counts twice and yards add kilometres no passenger travels. A station count depends on which tags are used, since old and new public transport tagging coexist. Underground lines are in the map but drawn with tunnel=yes and often a negative layer, so a map that hides tunnels makes most of the Metro disappear from downtown. Lifecycle values such as abandoned and disused keep old lines in the data, useful for history but to be excluded from today's network.

Route geometry, platforms and level crossings

Railway data from OpenStreetMap: traps and fixes. railway trapsSymptomFixMetro vanishes downtowninclude tunnel=yes wayslength far too longtracks, yards and platformsold lines on the mapdrop abandoned, disusedneed a named lineread route relationsD.C. rail, OSM 1 January 2026 spatialworkflow.io
Railway traps.
  • Route geometry: assembling a line's shape from its relation means ordering member ways; for timetabled services, GTFS shapes are often easier.
  • Platforms: railway=platform and public_transport=platform are separate features, useful for station footprints.
  • Level crossings: nodes tagged railway=level_crossing mark where roads cross track at grade.
  • Extract edges: lines crossing the boundary are cut by the extract and again by the clip.

FAQ

How do I get railway lines from OpenStreetMap?

Filter ways with the railway key and group by its value: rail, subway, light_rail and tram are track; exclude abandoned and disused.

How do I find train stations in OSM?

Look for railway=station; station=subway and station=light_rail mark metro and light-rail stations.

Why is the railway length so much longer than the line?

Each track is mapped separately, and yards and sidings add length.

How do I get a metro line like the Red Line?

Read the route=subway relations; each lists the tracks and stops of one line, often one relation per direction.