Cannot Transform Naive Geometries to CRS: How to Fix It in GeoPandas
On this page
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 record of what its coordinates mean: the numbers exist and gdf.crs is None. Reprojection converts from one known system to another, so with no starting system to_crs() refuses. The usual ways a layer arrives naive are a shapefile without its .prj, a GeoDataFrame built from x/y columns without crs=, and an export that dropped the metadata, such as a CSV.
The error is the honest outcome. The same naive layer written to GeoJSON comes back claiming WGS 84 and reprojects to infinities without a word, and a guessed label does the same. This page reproduces both on Vienna's 23 districts with their .prj deleted.
Quick answer
Find out which system the coordinates are already in, label them with it, then convert. For Vienna's districts the provider's documentation says MGI / Austria GK East:
import geopandas as gpd
gdf = gpd.read_file("data/districts.shp") # the .prj went missing on the way
print(gdf.crs, gdf.total_bounds.round(0))
# the provider's documentation says MGI / Austria GK East: label, don't move
gdf = gdf.set_crs("EPSG:31256")
# Now reprojection has a known starting point
gdf = gdf.to_crs("EPSG:3857")
print(gdf.crs, gdf.total_bounds.round(0))
None [-11180. 331051. 18258. 353820.]
EPSG:3857 [1801353. 6126492. 1845400. 6160704.]
The difference between labelling and converting is the subject of set_crs vs to_crs; here the only hard part is knowing which label is true.
Why to_crs() failed, and the fix
Find the source CRS, label it, then reproject
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/districts.shp")
print(gdf.crs)
try:
gdf.to_crs("EPSG:4326")
except ValueError as exc:
print(exc)
None
Cannot transform naive geometries. Please set a crs on the object first.
Step 2: Find the true source CRS
The label has to come from evidence about the data's origin: the provider's documentation or metadata, a sibling .prj from the same export, or the project the file came out of. The coordinates help rule things out.
The ranges narrow it down without deciding it. These districts run from x β11,180 to 18,258 and y 331,051 to 353,820: metres, a false origin near the data, not degrees. That fits the Austrian Gauss-KrΓΌger zones, whose eastings are centred on zero β but it would fit other grids too, which is why the documentation, not the numbers, decides.
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/districts.shp")
if gdf.crs is None:
# Confirmed from the data provider: MGI / Austria GK East
gdf = gdf.set_crs("EPSG:31256")
print(gdf.crs)
EPSG:31256
The coordinates are unchanged; only the label is new, and with it to_crs() has somewhere to start.
Step 4: Reproject with to_crs()
Now that the source CRS is defined, transform the coordinates into your target CRS.
gdf_projected = gdf.to_crs("EPSG:4326")
Step 5: Verify the fix
After reprojecting, confirm the result looks correct.
print(gdf_projected.crs)
print(gdf_projected.total_bounds.round(4))
print(gdf_projected[["NAMEK", "geometry"]].head(3))
EPSG:4326
[16.1818 48.1179 16.5775 48.3227]
NAMEK geometry
0 Innere Stadt POLYGON ((16.36985 48.20058, 16.36984 48.20058...
1 Simmering POLYGON ((16.42599 48.18558, 16.42646 48.18547...
2 Neubau POLYGON ((16.33872 48.20854, 16.33901 48.20849...
The bounds run from 16.18 to 16.58Β°E and 48.12 to 48.32Β°N, which is Vienna, and the district names sit where they should. A wrong label would have converted just as quietly and landed somewhere else; Example 4 adds a stronger check, the city's area.
Vienna's districts with their .prj deleted
The data used on this page
Vienna's 23 district boundaries from the city's open-data WFS (CC BY 4.0, Stadt Wien β data.wien.gv.at), written as a shapefile in MGI / Austria GK East (EPSG:31256) β and then, to reproduce the problem, with its .prj deleted.
import os
from pathlib import Path
import geopandas as gpd
Path("data").mkdir(exist_ok=True)
(gpd.read_file("https://data.wien.gv.at/daten/geo?service=WFS&request=GetFeature&version=1.1.0"
"&srsName=EPSG:4326&outputFormat=json&typeName=ogdwien:BEZIRKSGRENZEOGD")
[["NAMEK", "BEZNR", "geometry"]].to_crs("EPSG:31256").to_file("data/districts.shp"))
os.remove("data/districts.prj")
print(sorted(p.name for p in Path("data").glob("districts.*")))
['districts.cpg', 'districts.dbf', 'districts.shp', 'districts.shx']
Example 1: A GeoJSON never loads naive β which is the trap
Written to GeoJSON, the naive layer does not come back naive. It comes back wrong:
naive = gdf_bng = gpd.read_file("data/districts.shp") # metres, no CRS
naive.to_file("data/exported_layer.geojson")
back = gpd.read_file("data/exported_layer.geojson")
print("read back as:", back.crs, back.total_bounds.round(0))
print("to_crs(3857):", back.to_crs("EPSG:3857").total_bounds)
for ext in ("gpkg", "fgb", "parquet"):
path = f"data/exported_layer.{ext}"
naive.to_parquet(path) if ext == "parquet" else naive.to_file(path)
again = gpd.read_parquet(path) if ext == "parquet" else gpd.read_file(path)
print(f"{ext:8} reads back as: {again.crs}")
read back as: EPSG:4326 [-11180. 331051. 18258. 353820.]
to_crs(3857): [inf inf inf inf]
gpkg reads back as: None
fgb reads back as: None
parquet reads back as: None
No error at all: the file claims to be degrees, the numbers are metres, and reprojecting them produces infinities because a "latitude" of 331,051 is off the planet. Other formats are honest about it β the same layer written to GeoPackage, FlatGeobuf or GeoParquet reads back with crs of None β so if a layer has lost its CRS, fix it before it is written to GeoJSON, not after.
Example 2: Create points from x/y columns with a CRS
Pass crs when you build the GeoDataFrame, and nothing is ever naive:
import pandas as pd
import geopandas as gpd
df = pd.DataFrame({
"site": ["Colosseum", "Pantheon"],
"lon": [12.4924, 12.4769],
"lat": [41.8902, 41.8986]
})
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, f"| {gdf_utm.geometry.iloc[0].distance(gdf_utm.geometry.iloc[1]):,.0f} m apart")
EPSG:4326
EPSG:32633 | 1,589 m apart
Example 3: Shapefile missing its .prj file
from pathlib import Path
import geopandas as gpd
shp_path = Path("data/districts.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:31256") # confirmed MGI / Austria GK East
districts_4326 = gdf.to_crs("EPSG:4326")
print("Final CRS:", districts_4326.crs)
PRJ exists: False
Loaded CRS: None
Final CRS: EPSG:4326
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
A CSV is the commonest way a CRS disappears: the geometry survives as text, the CRS does not.
import pandas as pd
gdf_ok = gpd.read_file("data/districts.shp").set_crs("EPSG:31256")
gdf_ok.to_csv("data/districts.csv", index=False) # geometry written as WKT
df = pd.read_csv("data/districts.csv")
restored = gpd.GeoDataFrame(df, geometry=gpd.GeoSeries.from_wkt(df["geometry"]))
print("after the CSV round trip:", restored.crs)
restored = restored.set_crs("EPSG:31256") # the coordinates never changed
print(f"{restored.to_crs('EPSG:3035').area.sum() / 1e6:.3f} kmΒ²")
after the CSV round trip: None
414.867 kmΒ²
When an earlier step strips CRS metadata, the coordinates are unchanged, so you reattach the original CRS with set_crs() and continue β and an area of 414.867 kmΒ², within 0.004 kmΒ² of the city's own figure for Vienna, is how you know the label is right.
Why the error is the good outcome
GeoPandas keeps a layer's geometry and its CRS in separate places, and a naive layer is one whose CRS slot is empty. Refusing to reproject it is the only safe response, because there is no conversion "from unknown". The dangerous cases on this page are the ones that do not raise: a GeoJSON round trip (Example 1) and a guessed label (below).
So when this error appears, the data has told you the truth about itself. Fixing it means finding a fact β which system the coordinates are in β not making the message go away.
Guessing a label, and replacing a wrong one
Do not guess the CRS
A guessed label silences the error and moves the problem downstream:
guessed = gpd.read_file("data/districts.shp").set_crs("EPSG:4326") # a guess, and wrong
print("to_crs(3857):", guessed.to_crs("EPSG:3857").total_bounds)
print("to_crs(3035):", guessed.to_crs("EPSG:3035").total_bounds)
to_crs(3857): [inf inf inf inf]
to_crs(3035): [inf inf inf inf]
Neither call raises. Confirm the real CRS from provider metadata, source documentation, known coordinate ranges, or a trusted GIS project before calling set_crs().
A wrong CRS is a different problem
set_crs() refuses to replace a CRS that is already there; replacing a wrong one needs allow_override=True, and is covered in set_crs vs to_crs. A naive layer needs neither.
Internal links
- 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.
Why does gdf.crs return None when the file loaded fine?
Geometry and projection are stored separately, so a layer can read perfectly and still have no CRS: a shapefile whose .prj is missing, a GeoDataFrame built from coordinate columns without crs=, or an export step that dropped the metadata. None is the state this error comes from β label it with set_crs() before anything else.
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.
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().