How to Read a Shapefile in Python with GeoPandas
On this page
Problem statement
Opening a shapefile in GeoPandas is gpd.read_file("roads.shp"). The useful decisions come just before and just after that call: whether all of the shapefile's files are there, whether you need the whole layer or only some columns and rows, whether to read it out of the ZIP it arrived in, and what the format has already done to the data β ten-character field names, a text encoding, a coordinate system stored in a separate file.
The examples use one of the most common downloads of all, Geofabrik's OpenStreetMap extract for Rutland, a ZIP of 18 shapefiles, and HM Land Registry's parcels for the same county.
Quick answer
Use geopandas.read_file() and pass it the path to the .shp file:
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
print(type(gdf).__name__, len(gdf), "roads")
print(gdf.head(3))
GeoDataFrame 7292 roads
osm_id code ... tunnel geometry
0 2484492 5112 ... F LINESTRING (-0.72461 52.59642, -0.72452 52.596...
1 2484493 5112 ... F LINESTRING (-0.64671 52.59353, -0.6459 52.5934...
2 2956385 5113 ... F LINESTRING (-0.63426 52.59321, -0.63372 52.593...
[3 rows x 11 columns]
It returns a GeoDataFrame, with the attributes from the .dbf, the geometry from the .shp and the CRS from the .prj. A read that fails, or succeeds with something missing, is GeoPandas not reading a shapefile.
Shapefile components
Check the companion files, read, mind the paths
Confirm the shapefile components are present
Before you try to open a shapefile with GeoPandas, check which of its files are in the folder:
from pathlib import Path
parts = sorted(p.suffix for p in Path("data").glob("roads.*"))
print(parts)
print("required .shp/.shx/.dbf present:", {".shp", ".shx", ".dbf"} <= set(parts), "| .prj:", ".prj" in parts)
['.cpg', '.dbf', '.prj', '.shp', '.shx']
required .shp/.shx/.dbf present: True | .prj: True
At minimum, look for .shp (geometry), .shx (shape index) and .dbf (attribute table), and a .prj for the coordinate reference system. Geofabrik also ships a .cpg, which names the text encoding of the .dbf β the one that matters for any name with an accent or a curly apostrophe.
Read and inspect the shapefile
gdf = gpd.read_file("data/roads.shp")
print("columns:", gdf.columns.tolist())
print("geometry column:", gdf.geometry.name)
print("geometry types:", gdf.geom_type.value_counts().to_dict())
print("CRS:", gdf.crs)
print("rows:", len(gdf))
columns: ['osm_id', 'code', 'fclass', 'name', 'ref', 'oneway', 'maxspeed', 'layer', 'bridge', 'tunnel', 'geometry']
geometry column: geometry
geometry types: {'LineString': 7292}
CRS: EPSG:4326
rows: 7292
The CRS came from the .prj: Geofabrik's extracts are in WGS 84, so distances and areas need a projected CRS first.
Work with file paths safely
Path handling is a common source of errors, especially on Windows, where "C:\new\roads.shp" contains a newline escape. Use a raw string (r"C:\gis\roads.shp"), forward slashes, or pathlib, which is cross-platform and easier to maintain:
shapefile_path = Path("data") / "roads.shp"
print(shapefile_path, "->", len(gpd.read_file(shapefile_path)), "features")
data/roads.shp -> 7292 features
Rutland roads read from folders and ZIPs
The data used on this page
Geofabrik's OpenStreetMap extract for Rutland of 1 January 2026 (Β© OpenStreetMap contributors, ODbL), a ZIP of 18 shapefiles. The setup extracts the roads and buildings under short names, and saves HM Land Registry's INSPIRE parcels for the county as a shapefile and a GeoPackage for the speed comparison (Β© Crown copyright and database rights; OGL).
import shutil
import time
import warnings
import zipfile
from pathlib import Path
import geopandas as gpd
import requests
from osgeo import gdal
gdal.UseExceptions()
Path("data").mkdir(exist_ok=True)
if not Path("data/rutland.shp.zip").exists():
session = requests.Session()
Path("data/rutland.shp.zip").write_bytes(session.get(
"https://download.geofabrik.de/europe/united-kingdom/england/rutland-260101-free.shp.zip", timeout=300).content)
Path("data/Rutland.zip").write_bytes(session.get(
"https://use-land-property-data.service.gov.uk/datasets/inspire/download/Rutland.zip", timeout=300).content)
with zipfile.ZipFile("data/rutland.shp.zip") as z:
for layer, short in (("gis_osm_roads_free_1", "roads"), ("gis_osm_buildings_a_free_1", "buildings")):
for suffix in (".shp", ".shx", ".dbf", ".prj", ".cpg"):
Path(f"data/{short}{suffix}").write_bytes(z.read(layer + suffix))
print(len([n for n in z.namelist() if n.endswith(".shp")]), "shapefiles in the ZIP")
if not Path("data/parcels.gpkg").exists():
gdal.VectorTranslate("data/parcels.gpkg", "/vsizip/data/Rutland.zip/Land_Registry_Cadastral_Parcels.gml",
layerName="parcels", SQLStatement="SELECT INSPIREID FROM PREDEFINED")
gdal.VectorTranslate("data/parcels.shp", "data/parcels.gpkg")
18 shapefiles in the ZIP
Example 1: Check that the file exists before reading
for name in ("roads.shp", "land_use.shp"):
shapefile_path = Path("data") / name
try:
if not shapefile_path.exists():
raise FileNotFoundError(f"Shapefile not found: {shapefile_path}")
print(f"Loaded {len(gpd.read_file(shapefile_path))} features from {name}")
except FileNotFoundError as exc:
print("FileNotFoundError:", exc)
try:
gpd.read_file("data/land_use.shp")
except Exception as exc:
print("without the check:", type(exc).__name__, str(exc)[:70])
Loaded 7292 features from roads.shp
FileNotFoundError: Shapefile not found: data/land_use.shp
without the check: DataSourceError data/land_use.shp: No such file or directory
The check turns the library's error into your own message, naming the path you actually meant.
Example 2: Read only some columns or rows, or straight from the ZIP
subset = gpd.read_file("data/roads.shp", columns=["fclass", "name"], rows=100)
print("columns= and rows=:", subset.shape, subset.columns.tolist())
primary = gpd.read_file("data/roads.shp", where="fclass IN ('trunk', 'primary')")
print("where=:", len(primary), "trunk and primary roads")
zipped = gpd.read_file("zip://data/rutland.shp.zip!gis_osm_roads_free_1.shp")
print("from the ZIP, no extraction:", len(zipped), "roads")
columns= and rows=: (100, 3) ['fclass', 'name', 'geometry']
where=: 261 trunk and primary roads
from the ZIP, no extraction: 7292 roads
columns=, rows= and where= are passed to the reader, so only what you ask for is read. The zip://β¦!inner.shp form reads a shapefile from inside an archive; for a ZIP holding several shapefiles, the part after ! picks one.
Example 3: Field names that got truncated
wide = gpd.read_file("data/roads.shp", rows=5)
wide["speed_limit_mph"] = 30
wide["surface_material"] = "asphalt"
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
wide.to_file("out_wide.shp")
print("written:", [c for c in wide.columns if c != "geometry"][-2:])
print("read back:", [c for c in gpd.read_file("out_wide.shp").columns if c != "geometry"][-2:])
print("warnings:", len(caught), "|", " ".join(str(caught[0].message).split())[:80] if caught else "")
written: ['speed_limit_mph', 'surface_material']
read back: ['speed_limi', 'surface_ma']
warnings: 3 | Column names longer than 10 characters will be truncated when saved to ESRI Shap
The .dbf allows ten characters per field name, so speed_limit_mph and surface_material came back shortened. That is the format, not GeoPandas β and why the Geofabrik columns are all ten characters or fewer.
Example 4: Is a shapefile slower than a GeoPackage?
for path in ("data/parcels.shp", "data/parcels.gpkg"):
files = list(Path("data").glob("parcels.*")) if path.endswith(".shp") else [Path(path)]
size = sum(f.stat().st_size for f in files if f.suffix != ".gpkg" or path.endswith(".gpkg"))
start = time.perf_counter()
rows = len(gpd.read_file(path))
print(f"{Path(path).suffix:6} {size / 1e6:5.1f} MB {time.perf_counter() - start:.2f} s {rows:,} parcels")
.shp 15.2 MB 0.06 s 21,253 parcels
.gpkg 18.5 MB 0.05 s 21,253 parcels
For 21,253 parcels with one attribute, there was no meaningful difference β both read in a few hundredths of a second, and the GeoPackage was the larger file. The shapefile's costs are elsewhere: ten-character names, a 2 GB limit, a .dbf that stores every value as fixed-width text, and five files to keep together.
Reading less, and what the format already did
read_file hands the path to GDAL through pyogrio and gets back the whole layer unless told otherwise. The arguments in Example 2 are passed to GDAL, not applied afterwards in pandas: columns= reads only those fields from the .dbf, rows= stops after that many features, where= filters in GDAL's SQL, and a zip://β¦!layer.shp path reads straight out of the archive. For a large layer that is the difference between reading what you need and reading everything.
What the read cannot undo is what the format did when the file was written. The .dbf holds ten characters per field name, so a column written as speed_limit_mph comes back as speed_limi, and nothing on the reading side can recover the rest (Example 3). None of this made the format slow to read: 21,253 parcels came back about as fast as from a GeoPackage, both in a fraction of a second (Example 4). The format's costs are in writing and keeping data, not in reading it.
Missing files, encodings and zipped shapefiles
A companion file is missing
Only a missing .shx stops the read; a missing .dbf or .prj lets it succeed without the attributes or the CRS. The experiment, and the SHAPE_RESTORE_SHX fix, are in GeoPandas not reading a shapefile.
Text looks garbled
The .cpg names the .dbf's encoding; without it GDAL assumes Latin-1, and UTF-8 names such as Rutland's CaffΓ¨ Nero come out as mojibake. Pass encoding= β shapefile encoding errors covers finding the right one.
Reading zipped or network-stored shapefiles
The zip:// path in example 2 reads from an archive without extracting it. While debugging a failure, though, extract to a local folder first, so you can see β and list β the files the shapefile is made of.
Internal links
- How to Export GeoJSON in Python with GeoPandas
- How to Reproject Spatial Data in Python (GeoPandas)
- How to Perform a Spatial Join in Python (GeoPandas)
FAQ
Can GeoPandas read a .shp file directly?
Yes. Use geopandas.read_file() and pass the path to the .shp file. GeoPandas will load it as a GeoDataFrame if the required companion files are present.
What files are required for a shapefile to work correctly?
The key files are:
.shp.shx.dbf
In practice only a missing .shx stopped GeoPandas reading; a missing .dbf gave geometry with no attributes, and a missing .prj gave no CRS, both silently.
Why is the CRS missing after I load a shapefile?
Usually because the shapefile has no .prj file. Without it the roads loaded with every row and column and crs None β no error, no warning. If you know the correct CRS, assign it with set_crs().
Why are my shapefile column names shortened?
This is a standard shapefile limitation. The format has older field naming constraints, so long attribute names are often truncated when the shapefile is created.
Can GeoPandas read a shapefile from inside a ZIP archive without extracting it?
Yes. Prefix the path with the zip:// scheme and name the shapefile inside after a !, for example gpd.read_file("zip://data/rutland.shp.zip!gis_osm_roads_free_1.shp"), which read all 7,292 Rutland roads straight from Geofabrik's archive.
How do I read only some columns or rows from a large shapefile?
Pass columns= to load specific attribute fields and rows= to limit the number of features, for example gpd.read_file("data/parcels.shp", columns=["id", "name"], rows=100). This is faster than loading the full file and dropping columns afterward.
How do I fix garbled characters in a shapefile's attribute table?
Specify the source encoding when reading, for example gpd.read_file("data/buildings.shp", encoding="utf-8"). Without a .cpg file GDAL assumes ISO-8859-1, so a UTF-8 .dbf read as CaffΓΒ¨ Nero; older shapefiles may instead need a regional code page such as cp1252.
Why does my shapefile load far more slowly than a GeoPackage?
It may not be the format: 21,253 Rutland parcels read in 0.06 s as a shapefile and 0.05 s as a GeoPackage. Check that the reader is pyogrio (the default in current GeoPandas) and that you are not reading columns and rows you do not need β columns=, rows= and where= limit the read. Convert to GeoPackage for the format's other advantages rather than for speed alone.