Learn

GeoJSON with Python

Python has three tiers of GeoJSON support: the json stdlib for basic parsing, the geojson library for type-safe construction, and geopandas for spatial analysis pipelines.

The right tier depends on what you're doing. Parsing a file and iterating features — stdlib is enough. Constructing GeoJSON programmatically and catching structural mistakes before they reach an API — use the geojson library. Running spatial joins, reprojections, or bulk conversions — reach for geopandas.

Reading a GeoJSON file

Three ways, ordered from lightest dependency to heaviest.

stdlib json — no install required. Returns a plain Python dict. No type checking, no validation.

# stdlib json
import json
with open("features.geojson") as f:
    fc = json.load(f)
print(len(fc["features"]))  # number of features

geojson library — pip install geojson. Returns typed objects (FeatureCollection, Feature, Point, etc.) that inherit from dict. Attribute access works alongside key access.

# geojson library
import geojson
with open("features.geojson") as f:
    fc = geojson.load(f)
# fc is a typed FeatureCollection object

geopandas — pip install geopandas. Returns a GeoDataFrame — a pandas DataFrame with a geometry column. The right tool when you need column operations, spatial queries, or CRS management.

# geopandas
import geopandas as gpd
gdf = gpd.read_file("features.geojson")
print(gdf.shape)  # (rows, columns)

gpd.read_file accepts local paths, URLs, and .zip archives containing a Shapefile — not just GeoJSON.

Writing a FeatureCollection

# geojson library (recommended — validates output)
import geojson
point = geojson.Point((-0.1276, 51.5074))
feature = geojson.Feature(geometry=point, properties={"name": "London"})
fc = geojson.FeatureCollection([feature])
with open("output.geojson", "w") as f:
    geojson.dump(fc, f, indent=2)

geojson.Feature raises ValueError if you pass an unrecognised geometry type. That error surfaces immediately in your script — not silently downstream in a map renderer.

# geopandas
gdf.to_file("output.geojson", driver="GeoJSON")

gdf.to_file writes the active CRS to the file if one is set. If you've reprojected the data with gdf.to_crs(epsg=4326), the output coordinates will be in WGS 84 — which is what GeoJSON requires.

Filtering features by property

import geojson
with open("features.geojson") as f:
    fc = geojson.load(f)

parks = [
    feat for feat in fc["features"]
    if feat["properties"].get("category") == "park"
]
result = geojson.FeatureCollection(parks)

Using .get("category") instead of ["category"] avoids KeyError when the property is absent on some features — a common situation with real-world GeoJSON datasets where properties are inconsistent across features.

For more complex filtering — by geometry type, bounding box, or spatial relationship — geopandas is the right tool:

import geopandas as gpd

gdf = gpd.read_file("features.geojson")
parks = gdf[gdf["category"] == "park"]
parks.to_file("parks.geojson", driver="GeoJSON")

Shapely geometry to GeoJSON

from shapely.geometry import Polygon, mapping
import json

poly = Polygon([(-0.1750, 51.5030), (-0.1580, 51.5030),
                (-0.1580, 51.5130), (-0.1750, 51.5130)])

geojson_geom = mapping(poly)
print(json.dumps(geojson_geom, indent=2))
# {"type": "Polygon", "coordinates": [...]}

shapely.geometry.mapping converts any Shapely geometry to a Python dict that matches the GeoJSON geometry structure. The coordinates are in [longitude, latitude] order — the same order Shapely stores them. Wrap the result in a Feature to attach properties:

import geojson

feature = geojson.Feature(
    geometry=geojson_geom,
    properties={"name": "Hyde Park boundary"}
)

To go the other direction — GeoJSON dict to a Shapely geometry — use shapely.geometry.shape:

from shapely.geometry import shape

shapely_geom = shape(geojson_geom)
print(shapely_geom.area)  # in degrees² — reproject first for meaningful area

Folium: render GeoJSON on a map

import folium
import json

m = folium.Map(location=[51.5074, -0.1276], zoom_start=12)

with open("features.geojson") as f:
    data = json.load(f)

folium.GeoJson(data, name="features").add_to(m)
m.save("map.html")

folium.GeoJson accepts a Python dict, a file path, or a URL. The output is an HTML file with a Leaflet map embedded. Open it in any browser — no server required.

For per-feature styling, pass a style_function:

folium.GeoJson(
    data,
    style_function=lambda feature: {
        "fillColor": "#2d9e6b" if feature["properties"].get("type") == "park" else "#3388ff",
        "color": "#333",
        "weight": 1,
        "fillOpacity": 0.5,
    }
).add_to(m)

Watch out: json.dumps on a GeoJSON dict produces valid JSON but doesn't validate the GeoJSON structure. Use geojson.dumps from the geojson library — it raises ValueError on invalid geometry types or malformed coordinate arrays.

For converting Shapefiles before processing in Python, see Shapefile to GeoJSON. For GeoJSON format reference, see What is GeoJSON?.

For drawing and editing GeoJSON interactively rather than programmatically, Element Editor's playground accepts file upload and exports back to GeoJSON.

Continue reading

Mike Acler

Mike Acler

Design engineer and creator of Element Editor, an open-source map drawing and GeoJSON editing library for React. Writes about web cartography and GIS tooling.