Cannot Transform Naive Geometries to CRS: How to Fix It in GeoPandas
Problem statement
This error appears when you call to_crs() on a GeoDataFrame that has no coordinate reference system defined:
ValueError: Cannot transform naive geometries. Please set a crs on the object first.
A geometry is "naive" when GeoPandas has no idea what its coordinates mean. The numbers exist, but there is no CRS metadata attached, so GeoPandas cannot reproject them to anything else.
In practice, this usually shows up when you:
- load a layer and immediately try
gdf.to_crs("EPSG:3857") - build a GeoDataFrame from
x/ycolumns without passing acrs - read a shapefile that is missing its
.prjfile - reproject data that lost its CRS in a previous export step
The error is raised because reprojection is a transformation between two known coordinate systems. If the source CRS is unknown (gdf.crs is None), there is no starting point to transform from.
Common causes are:
- the GeoDataFrame was created manually from coordinates and no
crswas assigned - a shapefile is missing its
.prjsidecar file - a source file was saved without projection metadata
- a GeoJSON file is assumed to be WGS84, but CRS was never set after loading
- an earlier export or conversion step dropped the CRS metadata
Quick answer
To fix the naive geometries error:
- Check the CRS with
gdf.crsand confirm it isNone - Find out the true source CRS of the existing coordinates from a reliable source
- Assign that CRS with
set_crs()(this only labels the coordinates, it does not move them) - Reproject to your target CRS with
to_crs()
import geopandas as gpd
gdf = gpd.read_file("data/points.geojson")
# gdf.crs is None: assign the confirmed source CRS first (metadata only)
gdf = gdf.set_crs("EPSG:4326")
# Now reprojection has a known starting point
gdf = gdf.to_crs("EPSG:3857")
print(gdf.crs)
Use set_crs() to define the CRS the coordinates already use. Use to_crs() only after the source CRS is correct. Do not guess the source CRS; verify it first.
Why to_crs() failed, and the fix
Step-by-step solution
Step 1: Confirm that gdf.crs is None
The error always traces back to a missing source CRS. Check it directly before doing anything else.
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
print(gdf.crs)
Possible results:
- valid CRS, for example:
EPSG:4326 - missing CRS:
None
If gdf.crs is None, GeoPandas has no projection information, and any to_crs() call will raise the naive geometries error.
Step 2: Find the true source CRS
Do not assign a CRS just to make the error go away. Find the coordinate system the data is actually in.
Useful checks:
- the original dataset documentation or metadata
- the layer properties in QGIS or another GIS tool
- the shapefile
.prjfile, if it exists - the data provider or the export settings used
- the coordinate ranges themselves (longitude/latitude values between roughly -180 and 180 suggest a geographic CRS such as WGS84)
For shapefiles, confirm the sidecar files exist together:
roads.shproads.shxroads.dbfroads.prj
If roads.prj is missing, GeoPandas reads the geometry but reports gdf.crs as None.
Step 3: Assign the source CRS with set_crs()
Once you know the real source CRS, label the existing coordinates with it.
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
if gdf.crs is None:
# Confirmed from the data provider that this layer is in WGS84
gdf = gdf.set_crs("EPSG:4326")
print(gdf.crs)
Important: set_crs() does not change coordinate values. It only labels the coordinates with the CRS they already use. After this step, gdf.crs is no longer None, so reprojection becomes possible.
Step 4: Reproject with to_crs()
Now that the source CRS is defined, transform the coordinates into your target CRS.
gdf = gdf.set_crs("EPSG:4326")
gdf_projected = gdf.to_crs("EPSG:3857")
The full workflow is:
- load the data
- check
gdf.crs - assign the source CRS with
set_crs()if it isNone - convert to the target CRS with
to_crs()
Skipping step 3 is exactly what triggers Cannot transform naive geometries.
Step 5: Verify the fix
After reprojecting, confirm the result looks correct.
print(gdf_projected.crs)
print(gdf_projected.total_bounds)
print(gdf_projected.head())
Check that:
gdf_projected.crsis no longerNone- the bounds look reasonable for the target CRS
- coordinates are in the expected range
For example:
EPSG:4326should usually have longitude and latitude rangesEPSG:3857should have meter-based values around thousands or millions
Code examples
Example 1: Assign WGS84, then reproject
import geopandas as gpd
gdf = gpd.read_file("data/points.geojson")
print("Before:", gdf.crs) # None
# Confirmed the coordinates are longitude/latitude in WGS84
gdf = gdf.set_crs("EPSG:4326")
# Reproject to Web Mercator for web mapping
gdf = gdf.to_crs("EPSG:3857")
print("After:", gdf.crs)
This is the canonical fix: label first with set_crs(), then transform with to_crs().
Example 2: Create points from x/y columns with a CRS
The cleanest fix is to never create naive geometries in the first place. Pass crs when you build the GeoDataFrame.
import pandas as pd
import geopandas as gpd
df = pd.DataFrame({
"site": ["A", "B"],
"lon": [12.4924, 12.4964],
"lat": [41.8902, 41.9028]
})
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df.lon, df.lat),
crs="EPSG:4326"
)
print(gdf.crs)
# Reprojection now works immediately
gdf_utm = gdf.to_crs("EPSG:32633")
print(gdf_utm.crs)
Without the crs="EPSG:4326" argument, gdf.crs would be None and to_crs() would raise the naive geometries error.
Example 3: Shapefile missing its .prj file
from pathlib import Path
import geopandas as gpd
shp_path = Path("data/roads.shp")
prj_path = shp_path.with_suffix(".prj")
print("PRJ exists:", prj_path.exists())
gdf = gpd.read_file(shp_path)
print("Loaded CRS:", gdf.crs) # likely None if .prj is missing
# Only assign after confirming the correct CRS from metadata or documentation
if gdf.crs is None:
gdf = gdf.set_crs("EPSG:32633") # confirmed UTM zone 33N
roads_4326 = gdf.to_crs("EPSG:4326")
print("Final CRS:", roads_4326.crs)
A missing .prj file is one of the most common reasons a shapefile loads naive.
Example 4: Fix a layer that lost its CRS during export
import geopandas as gpd
gdf = gpd.read_file("data/exported_layer.geojson")
if gdf.crs is None:
# A previous export dropped the CRS; the data is still WGS84
gdf = gdf.set_crs("EPSG:4326")
# Reproject to a projected CRS for area or distance work
gdf_projected = gdf.to_crs("EPSG:3857")
print(gdf_projected.crs)
When an earlier step strips CRS metadata, the coordinates are unchanged, so you reattach the original CRS with set_crs() and continue.
Explanation
"Naive" means the geometry has coordinates but no CRS. GeoPandas stores geometry and CRS metadata together, but they are not the same thing. A naive geometry is one where the metadata slot is empty (gdf.crs is None).
The two functions involved are easy to confuse:
set_crs()labels the existing coordinates. It tells GeoPandas which CRS the current coordinate values already use. It does not move any points.to_crs()transforms the coordinates from one CRS into another. It actually changes the numbers.
to_crs() cannot work on a naive geometry because transformation needs a known source. There is no way to convert "from unknown CRS to EPSG:3857". The fix is always to give GeoPandas the missing starting point first.
A common mistake is to reach for to_crs() when set_crs() is what you need:
# Wrong: gdf.crs is None, so there is nothing to transform from
gdf = gdf.to_crs("EPSG:4326") # raises the naive geometries error
# Right: label the coordinates with their true CRS first
gdf = gdf.set_crs("EPSG:4326")
If you then need a different CRS, reproject after labeling:
gdf = gdf.set_crs("EPSG:4326").to_crs("EPSG:3857")
Edge cases or notes
Do not guess the CRS
Assigning a CRS just to silence the error is dangerous. If you label data in meters as EPSG:4326, the code runs, but every later area, distance, buffer, and spatial join will be wrong. Confirm the real CRS from provider metadata, source documentation, known coordinate ranges, or a trusted GIS project before calling set_crs().
allow_override is for replacing an existing CRS
set_crs() refuses to overwrite a CRS that is already defined. If a layer has the wrong CRS attached (not missing, but incorrect), use allow_override=True, but only when you are certain of the true source CRS:
gdf = gdf.set_crs("EPSG:4326", allow_override=True)
For a purely naive geometry where gdf.crs is None, you do not need allow_override.
GeoJSON is usually WGS84
GeoJSON files generally use WGS84 longitude/latitude (EPSG:4326). This is a reasonable default to confirm, but still verify with print(gdf.crs) after loading, because exported data can be mislabeled.
Shapefile CRS lives in the .prj file
A shapefile stores projection information in a separate .prj sidecar file. If that file is missing or broken, GeoPandas reads the geometry but loads gdf.crs as None. Restoring or recreating the .prj, or assigning the confirmed CRS with set_crs(), resolves it.
set_crs() vs to_crs() in one line
If the source CRS is missing and you also need a different target CRS, chain them: gdf.set_crs("EPSG:4326").to_crs("EPSG:3857"). The first call labels, the second transforms.
Internal links
- CRS Not Found Error in GeoPandas: Causes and Fixes
- How to Fix CRS Mismatch in GeoPandas
- How to Reproject Spatial Data in Python (GeoPandas)
- Coordinate Reference Systems (CRS) Explained for Python GIS
- How to Read a CSV with Coordinates as a GeoDataFrame
FAQ
What does "naive geometries" mean in GeoPandas?
It means the geometry has coordinates but no CRS metadata, so gdf.crs is None. GeoPandas cannot interpret or reproject the coordinates until you assign their source CRS.
How do I fix Cannot transform naive geometries?
Confirm the true source CRS, assign it with set_crs() to label the existing coordinates, then call to_crs() to reproject. The error happens because to_crs() needs a known starting CRS.
Why not just use set_crs() for everything?
set_crs() only labels coordinates; it never moves them. If the data is already in the correct CRS and you want a different one, you must use to_crs() to actually transform the coordinates.
Can I assign a CRS if my shapefile is missing the .prj file?
Yes, but only if you know the correct CRS from a reliable source. Use set_crs() to define it, then reproject with to_crs() if needed. Do not guess the CRS.
Does set_crs() move my coordinates onto the map?
No. set_crs() only attaches CRS metadata to the coordinates you already have; the numeric values are untouched. If you accidentally label data with the wrong CRS, the points will plot in the wrong place even though no error is raised.
How can I check whether coordinates are likely WGS84 before assigning a CRS?
Inspect the coordinate ranges with gdf.total_bounds. Values roughly between -180 and 180 for x and -90 and 90 for y suggest geographic degrees such as EPSG:4326, while large values in the thousands or millions suggest a projected CRS in metres.
Can I read a .prj file and apply it without retyping the EPSG code?
Yes. If a sibling layer or QGIS reports the CRS as a WKT string, you can pass that WKT straight to set_crs(), for example gdf.set_crs(wkt_string). GeoPandas accepts any pyproj-understood input, including EPSG codes, WKT, or PROJ strings.
Why do I still get the naive geometries error after calling to_crs() twice?
Because to_crs() never sets a missing source CRS; it only transforms between two known ones. If gdf.crs is None, every to_crs() call raises the error until you first label the data with set_crs().