Why Are My Shapefile Column Names Truncated? How to Fix It
On this page
Problem statement
Saving a GeoDataFrame as a shapefile cuts every column name to 10 characters, because the attribute table is a dBASE file. It shows up as:
- a warning is printed about field names being truncated
- column names like
BusinessNamebecomeBusinessNaafter saving - several long names that start the same way collapse into numbered stubs like
LocalAut_1,LocalAut_2 - the saved shapefile has different column names than the GeoDataFrame in memory
The cause is the format, not GeoPandas:
- the shapefile format stores attributes in a dBASE (
.dbf) table - the dBASE format limits field names to 10 characters
- it also limits text values to 254 characters and supports only a few data types
- any name longer than 10 characters is shortened on write, and the shortened names can collide
The examples below use a real table with exactly this problem: the Food Standards Agency's register of Rutland's food businesses, whose columns have names like LocalAuthorityBusinessID and Scores.ConfidenceInManagement.
Quick answer
Truncated column names are a hard limit of the shapefile format, not a GeoPandas bug. You have two practical options:
- Keep the shapefile, but rename your columns to 10 characters or fewer before saving so you control the result.
- Save to a modern format like GeoPackage (
.gpkg) or GeoJSON, which do not truncate field names.
The simplest fix is to stop using shapefiles for output. Save a GeoPackage instead:
food.to_file("output/food.gpkg", driver="GPKG")
print(gpd.read_file("output/food.gpkg").columns.tolist()[:6])
['FHRSID', 'LocalAuthorityBusinessID', 'BusinessName', 'BusinessType', 'BusinessTypeID', 'AddressLine2']
A GeoPackage keeps LocalAuthorityBusinessID, Scores.ConfidenceInManagement and every other long name exactly as they are.
Read the warning, then rename or change format
1. Reproduce and read the truncation warning
When you write a shapefile with long column names, GeoPandas warns once, and GDAL then reports each name it changed:
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
food.to_file("output/food.shp")
print(len(caught), "warnings, the first three:")
for warning in caught[:3]:
print(" ", warning.message)
20 warnings, the first three:
Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.
Normalized/laundered field name: 'LocalAuthorityBusinessID' to 'LocalAutho'
Normalized/laundered field name: 'BusinessName' to 'BusinessNa'
Each "laundered" line tells you exactly which column was shortened and what it became. In a script those lines scroll past; in a scheduled job they may go nowhere at all.
2. See what the names became
Reading the shapefile back shows the damage. The columns come back in the same order, so a side-by-side view is a zip:
back = gpd.read_file("output/food.shp")
for long, short in zip(food.columns, back.columns):
if long != short:
print(f"{long:30} -> {short}")
LocalAuthorityBusinessID -> LocalAutho
BusinessName -> BusinessNa
BusinessType -> BusinessTy
BusinessTypeID -> Business_1
AddressLine2 -> AddressLin
AddressLine3 -> AddressL_1
RatingValue -> RatingValu
LocalAuthorityCode -> LocalAut_1
LocalAuthorityName -> LocalAut_2
LocalAuthorityWebSite -> LocalAut_3
LocalAuthorityEmailAddress -> LocalAut_4
NewRatingPending -> NewRatingP
Scores.Hygiene -> Scores.Hyg
Scores.Structural -> Scores.Str
Scores.ConfidenceInManagement -> Scores.Con
Geocode.Longitude -> Geocode.Lo
Geocode.Latitude -> Geocode.La
AddressLine4 -> AddressL_2
AddressLine1 -> AddressL_3
Five columns begin LocalAuthority, and they became LocalAutho, then LocalAut_1 to LocalAut_4 β in column order, not in any order a reader could guess. Worse, the four address columns sit in the order the API delivered them, AddressLine2, AddressLine3, AddressLine4, AddressLine1, so AddressLine1 became AddressL_3 and AddressLine2 became AddressLin. Anyone opening that shapefile would read the second address line as the first.
3. Check which columns are too long, and which will collide
Before saving, list every column whose name exceeds the 10-character limit, and every group that would share a 10-character stem:
too_long = [c for c in food.columns if c != food.geometry.name and len(c) > 10]
stems = pd.Series(too_long).groupby(lambda i: too_long[i][:10]).apply(list)
print(len(too_long), "names too long;", "colliding stems:", stems[stems.str.len() > 1].to_dict())
19 names too long; colliding stems: {'AddressLin': ['AddressLine2', 'AddressLine3', 'AddressLine4', 'AddressLine1'], 'BusinessTy': ['BusinessType', 'BusinessTypeID'], 'LocalAutho': ['LocalAuthorityBusinessID', 'LocalAuthorityCode', 'LocalAuthorityName', 'LocalAuthorityWebSite', 'LocalAuthorityEmailAddress']}
If the first list is empty, your shapefile export will keep the names as written.
4. Option A: rename columns to 10 characters or fewer
If you must produce a shapefile, rename the long columns yourself so you control the short names, and keep the lookup so you can recover the originals:
rename_map = {
"LocalAuthorityBusinessID": "la_bus_id", "BusinessName": "name", "BusinessType": "type",
"BusinessTypeID": "type_id", "AddressLine1": "addr1", "AddressLine2": "addr2",
"AddressLine3": "addr3", "AddressLine4": "addr4", "RatingValue": "rating",
"LocalAuthorityCode": "la_code", "LocalAuthorityName": "la_name", "LocalAuthorityWebSite": "la_web",
"LocalAuthorityEmailAddress": "la_email", "NewRatingPending": "pending", "Scores.Hygiene": "sc_hygiene",
"Scores.Structural": "sc_struct", "Scores.ConfidenceInManagement": "sc_manage",
"Geocode.Longitude": "lon", "Geocode.Latitude": "lat",
}
assert all(len(short) <= 10 for short in rename_map.values())
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
food.rename(columns=rename_map).to_file("output/food_renamed.shp")
print(len(caught), "warnings |", gpd.read_file("output/food_renamed.shp").columns.tolist()[:8])
0 warnings | ['FHRSID', 'la_bus_id', 'name', 'type', 'type_id', 'addr2', 'addr3', 'PostCode']
Because you chose the short names, there are no collisions, no warnings, and addr1 really is the first address line.
5. Option B: switch to a format without the limit
The cleanest fix is to stop saving shapefiles and use GeoPackage or GeoJSON. Both keep full-length names:
food.to_file("output/food.geojson", driver="GeoJSON")
for name in ("output/food.gpkg", "output/food.geojson"):
columns = gpd.read_file(name).columns
print(f"{name:22} longest name kept: {max(columns, key=len)} ({len(max(columns, key=len))} characters)")
output/food.gpkg longest name kept: Scores.ConfidenceInManagement (29 characters)
output/food.geojson longest name kept: Scores.ConfidenceInManagement (29 characters)
Keeping the full names of Rutland's FSA columns
The data used on this page
The Food Standards Agency's food hygiene rating data for Rutland (Open Government Licence), flattened from the API's JSON with pandas.json_normalize, which is where names like Scores.Hygiene come from. Businesses without coordinates are left out.
import json
import warnings
from pathlib import Path
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
Path("output").mkdir(exist_ok=True)
if not Path("data/food.json").exists():
Path("data/food.json").write_bytes(requests.get(
# the FSA register file for Rutland as saved on 9 October 2026 (updated daily); live:
# ratings.food.gov.uk/api/open-data-files/FHRS879en-GB.json
"https://data.spatialworkflow.io/fhrs-879-2026-10-09.json", timeout=120).content)
found = json.loads(Path("data/food.json").read_text())["FHRSEstablishment"]["EstablishmentCollection"]
records = pd.json_normalize(found if isinstance(found, list) else found["EstablishmentDetail"])
records = records.drop(columns=[c for c in ("Geocode", "Scores") if c in records]) # empty objects
x = pd.to_numeric(records["Geocode.Longitude"], errors="coerce")
y = pd.to_numeric(records["Geocode.Latitude"], errors="coerce")
located = x.notna()
food = gpd.GeoDataFrame(records[located].reset_index(drop=True),
geometry=gpd.points_from_xy(x[located], y[located]), crs=4326)
print(len(food), "located businesses,", len(food.columns) - 1, "attribute columns")
338 located businesses, 24 attribute columns
Example 1: Keep a lookup and restore the original names after reading
Path("output/food_fields.json").write_text(json.dumps(rename_map, indent=2))
restored = gpd.read_file("output/food_renamed.shp")
restore_map = {short: long for long, short in json.loads(Path("output/food_fields.json").read_text()).items()}
restored = restored.rename(columns=restore_map)
print(restored.columns.tolist()[:6])
print("all names back:", set(restored.columns) == set(food.columns))
['FHRSID', 'LocalAuthorityBusinessID', 'BusinessName', 'BusinessType', 'BusinessTypeID', 'AddressLine2']
all names back: True
Ship the JSON lookup with the shapefile. It is the only record of what sc_manage meant.
Example 2: The other dBASE limits, measured
Names are not the only thing a .dbf shortens. Here are a long text value, a date-time, a float and a very wide table:
limits = food[["BusinessName", "PostCode", "RatingDate", "geometry"]].copy()
district = limits["PostCode"].str.split().str[0]
limits["district_names"] = district.map(limits.groupby(district)["BusinessName"].agg("; ".join))
limits["rated"] = pd.to_datetime(limits["RatingDate"])
limits["third"] = 1 / 3
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
limits.drop(columns="RatingDate").to_file("output/limits.shp")
back = gpd.read_file("output/limits.shp")
print("longest text written:", int(limits["district_names"].str.len().max()), "| read back:", int(back["district_n"].str.len().max()))
print("values over 254 characters:", int((limits["district_names"].str.len() > 254).sum()),
"| truncation warnings:", sum("truncated" in str(w.message) for w in caught))
print("date-time:", limits["rated"].iloc[0], "->", repr(back["rated"].iloc[0]), back["rated"].dtype)
print("float: ", repr(limits["third"].iloc[0]), "->", repr(float(back["third"].iloc[0])))
wide = pd.concat([food[["geometry"]], pd.DataFrame({f"f{i:03d}": np.arange(len(food)) for i in range(300)})], axis=1)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
wide.to_file("output/wide.shp")
print("300 fields written, read back:", len(gpd.read_file("output/wide.shp").columns) - 1,
"|", next(str(w.message) for w in caught if "field" in str(w.message)))
longest text written: 7669 | read back: 254
values over 254 characters: 336 | truncation warnings: 2
date-time: 2025-03-05 00:00:00 -> '2025/03/05 00:00:00' str
float: np.float64(0.3333333333333333) -> 0.333333333333333
300 fields written, read back: 300 | Creating a 256th field, but some DBF readers might only support 255 fields
Every business name in a postcode district, joined into one field, ran to thousands of characters, and the shapefile kept 254 of them β for 336 of the 338 rows, with just two warnings, because GDAL stops repeating a warning after the first few. The date-time came back as a string, and the float lost its last digit. The 300-column table was written in full: GDAL warns at the 256th field that some DBF readers stop at 255, so the file you send may open with columns missing in someone else's software.
The 1980s format behind the .dbf
A shapefile is not a single file. It is a small set of files with the same base name, and attribute data lives in the .dbf file. The .dbf format is dBASE, a database format from the 1980s.
dBASE was designed long before modern naming conventions, and it imposes several fixed limits:
- field names are stored in an 11-byte header slot, which works out to a maximum of 10 usable characters
- text fields hold at most 254 characters
- field types are limited to a small set β text, number, date, logical β and numbers have fixed width and precision
- the classic format stops at 255 fields, which GDAL will exceed with a warning
When you call gdf.to_file("out.shp"), GDAL's shapefile driver has to fit your column names into that 10-character slot. Any longer name is cut, and when two cut names match, GDAL keeps the first and rewrites the later ones as a shorter stem plus _1, _2 and so on, in column order. That is where the numbered and misleading names come from. Matching ignores case, so RatingValue and ratingvalue collide too.
This is a property of the format, so no GeoPandas setting removes it. The only real choices are to shorten the names yourself or to use a format that was not built around dBASE.
The other dBASE limits
The 255-field limit
Field count: GDAL wrote all 300 columns of example 2, with a warning at the 256th that some DBF readers only support 255. The file is valid for GDAL and may lose columns in other software. GeoPackage has no practical field-count limit for normal use.
Field type limits
Data types: The .dbf format supports only a few field types. A date-time came back from the shapefile as text, a float lost its last significant digit, and text over 254 characters was cut. GeoPackage and GeoJSON preserve richer types.
Encoding
Text encoding: GDAL writes a .cpg file saying UTF-8, and names such as "Cafe Terryβs" survive the round trip. Shapefiles from older software may have no .cpg or a different code page, and then non-ASCII text is decoded wrongly β keep the .cpg with the other files.
Mixed-case names
Case sensitivity: Laundering compares names without regard to case, so RatingValue and ratingvalue become RatingValu and ratingva_1. Use lowercase short names to avoid surprises.
Silent collisions
Duplicate short names: Collisions are resolved in column order, so the same data with its columns in a different order produces different names. Always check the saved column names after writing a shapefile β or, better, choose them yourself.
Internal links
- How to Read a Shapefile in Python with GeoPandas
- GeoPandas Not Reading Shapefile: Common Causes and Fixes
- How to Read and Write GeoPackage Files in Python
- How to Export GeoJSON in Python with GeoPandas
- How to Batch Process Multiple Shapefiles in Python
FAQ
Why does GeoPandas shorten my column names when I save a shapefile?
Because the shapefile attribute table uses the dBASE (.dbf) format, which limits field names to 10 characters. GeoPandas truncates longer names to fit the format and warns you when it does.
Can I turn off the 10-character limit in GeoPandas?
No. The limit comes from the shapefile format itself, not from GeoPandas. To keep full-length names, rename your columns to 10 characters or fewer, or save to GeoPackage or GeoJSON instead.
How do I keep my original long names when I have to deliver a shapefile?
Build a rename map from long to short names, apply it with gdf.rename(columns=...), and save the mapping to a file like JSON. You can re-apply the inverse mapping after reading the shapefile back.
Which format should I use instead of a shapefile?
GeoPackage (.gpkg) is the best general-purpose choice: one file, full-length names, and rich data types. GeoJSON is a good option when you need a text-based format for the web.
How can I detect which columns will collide before saving a shapefile?
Map each column to its 10-character form and look for duplicates, ignoring case: build [c[:10].lower() for c in gdf.columns] and check whether any short name appears more than once. If duplicates exist, rename those columns deliberately before writing so you control the result.
Does saving to GeoPackage have any field name length limit?
No. GeoPackage stores attributes in SQLite, so it keeps full-length names like median_household_income and has no practical 10-character or 254-field limit. Save it with gdf.to_file("out.gpkg", driver="GPKG").
Will the geometry column name also get truncated in a shapefile?
The geometry itself is stored separately from the .dbf attribute table, so the active geometry column is not written as a truncated attribute field. Only the attribute column names are subject to the 10-character dBASE limit.
My field names are short enough but the values are still cut off. Why?
That is a separate dBASE limit on field width and type, not the 10-character name limit; long text values and high-precision numbers can be truncated or coerced on write. Save to GeoPackage or GeoJSON, which preserve longer text and richer numeric types.