Quick Answer
GeoPandas can support many read/transform/write vector workflows, but it is not a general ArcPy replacement. Compare the target dataset, libraries, memory, licensing, ArcGIS-specific tools, output checks, and operating requirements before choosing a migration path.
- GeoPandas can be a useful option for some vector workflows, but performance must be measured on the target data and runtime
- Do not switch blindly when the workflow depends on topology, network analysis, licensing, or governance requirements
- Memory management remains important: choose chunking, Dask, database, or another path from the measured workload
- Migration timing depends on script complexity, dependencies, validation, team capability, and the operating boundary
A team may have a large ArcPy script estate, recurring flood-risk work, and meaningful licence or operating constraints. The correct migration decision starts with an inventory and a target run record, not a retained example.
A colleague may suggest GeoPandas because it is open source and has a different execution model. Retained speed claims require dataset, operation, machine, library, and verifier details. Memory limits, missing topology tools, and incomplete translations remain valid review questions. Before diving into the technical details, you might want to understand whether your current ArcGIS licences are being used efficiently.
This guide is retained technical context. It compares translation patterns, limitations, and checks that can help an organisation decide whether a bounded migration study is appropriate.
How do you migrate from ArcPy to GeoPandas?
Use a bounded sequence: inventory scripts and dependencies, select a pilot, translate one workflow, compare outputs, and document the handover. Simple data-loading and filtering may be easier to translate than workflows with topology or network dependencies, but the target data, runtime, review, and ownership decide the result. Migration Engine is in development; this article does not describe an available automatic translator.

The migration path: from complexity to clarity. Not a replacement—a transformation.
When NOT to Switch to GeoPandas
GeoPandas is not a drop-in replacement for ArcPy. If you rely on specific Esri capabilities, migration may create more problems than it solves.
ESRI TOPOLOGY RULES
Example: Utility network validation rules. “Pipes must not overlap,” “Valves must connect to exactly two pipes,” “Service areas must not have gaps.”
ArcPy's topology framework provides declarative rule definition and batch validation. GeoPandas has no equivalent. You can write custom validation logic using Shapely predicates, but it's procedural code, not declarative rules.
Verdict: If topology rules are core to your workflow, keep ArcPy for validation. Use GeoPandas for data preparation and analysis.
NETWORK ANALYST EXTENSION
Example: Routing emergency vehicles, service area analysis, vehicle routing problem with time windows.
ArcPy's Network Analyst is a sophisticated solver. Open-source alternatives exist (NetworkX, OSMnx, pgRouting), but they require different workflows and don't support all Esri network dataset features.
Verdict: Complex routing problems may justify ArcPy licensing. Simple routing can use OSMnx.
A Hybrid Architecture Pattern
When a workflow needs both open-source processing and ArcGIS-specific capabilities, a hybrid design can keep the specialised step explicit while the team tests the rest of the path:
Data Preparation: GeoPandas
Load, filter, transform, spatial joins—everything that's fast in GeoPandas.
Specialised Operations: ArcPy
Export to File Geodatabase, run topology validation or network routing, export results.
Post-Processing: GeoPandas
Load ArcPy results, join with other datasets, generate reports, export to cloud storage.
import geopandas as gpd
import arcpy
# Fast data prep in GeoPandas
parcels = gpd.read_parquet("s3://bucket/parcels.parquet")
filtered = parcels[parcels['zone'] == 'RESIDENTIAL']
# Export to FGDB for ArcPy topology check
filtered.to_file("temp.gdb", layer="parcels", driver="OpenFileGDB")
# Run Esri topology validation
arcpy.ValidateTopology_management("temp.gdb/topology")
errors = arcpy.da.SearchCursor("temp.gdb/topology_errors", ["SHAPE@", "RuleType"])
# Back to GeoPandas for reporting
error_gdf = gpd.GeoDataFrame.from_features([...])
error_gdf.to_parquet("topology_errors.parquet")Result: The retained example illustrates how to separate general processing from Esri-specific validation. It does not establish a current speed or licence saving. Record the target run, output comparison, licence terms, and ownership before drawing a conclusion. For more on GeoParquet and other cloud-native formats, see our guide to COG, GeoParquet, and STAC.
How Fast Is GeoPandas Compared to ArcPy?
Retained benchmark figures compare selected GeoPandas and ArcPy operations under a dated setup. They are not a general speed claim. Vectorisation, indexing, memory, data format, library versions, and implementation can change the result; re-run the selected operation before relying on it.

How Do You Handle Large Datasets in GeoPandas?
Choose a memory strategy from the target workload. GeoPandas commonly works in memory, while ArcPy and other tools may stream or push work to a database. Use spatial chunks, Dask-GeoPandas, or DuckDB Spatial when the selected data and operation require it. Test peak memory, failure behaviour, and output equivalence instead of inferring from a general rule.
THE MEMORY WALL
Example: A national parcel dataset may exceed the available memory of a direct GeoDataFrame load. The correct comparison records feature count, disk size, geometry complexity, machine memory, chunking strategy, and failure behaviour for both implementations.
This isn't a bug. It's architectural. Pandas (and therefore GeoPandas) is designed for in-memory analytics. When data exceeds available RAM, you need different strategies.
Process data in spatial or attribute-based chunks. Works for operations that don't require cross-chunk analysis (filtering, attribute calculation, projection).
import geopandas as gpd
# Process by county to keep chunks manageable
counties = gpd.read_file("counties.shp")
results = []
for idx, county in counties.iterrows():
# Load only parcels in this county
parcels = gpd.read_file(
"national_parcels.gpkg",
mask=county.geometry, # Spatial filter
engine="pyogrio" # Fast driver
)
# Process chunk
parcels['area_m2'] = parcels.geometry.area
parcels['density'] = parcels['population'] / parcels['area_m2']
results.append(parcels)
# Combine results
final = gpd.GeoDataFrame(pd.concat(results, ignore_index=True))Dask-GeoPandas partitions data across multiple cores and can spill to disk when memory fills. Supports most GeoPandas operations with parallel execution.
import dask_geopandas as dgpd
# Read with Dask (lazy evaluation)
ddf = dgpd.read_parquet(
"parcels.parquet",
npartitions=32 # Split into 32 chunks
)
# Operations are lazy until compute()
ddf['area_m2'] = ddf.geometry.area
ddf['value_per_m2'] = ddf['assessed_value'] / ddf['area_m2']
# Trigger computation with parallel execution
result = ddf.compute() # Uses all CPU cores
# Or save directly without loading full result
ddf.to_parquet("processed_parcels.parquet")For pure analytical queries (no complex geometry operations), DuckDB Spatial provides SQL interface with excellent performance on large files.
import duckdb
con = duckdb.connect()
con.install_extension("spatial")
con.load_extension("spatial")
# Query a large parcel set without loading it into memory
result = con.execute("""
SELECT
county,
COUNT(*) as parcel_count,
AVG(ST_Area(geometry)) as avg_area_m2,
SUM(assessed_value) as total_value
FROM read_parquet('parcels.parquet')
WHERE land_use = 'RESIDENTIAL'
GROUP BY county
""").df()
print(result)Process data in spatial or attribute-based chunks. Works for operations that don't require cross-chunk analysis (filtering, attribute calculation, projection).
import geopandas as gpd
# Process by county to keep chunks manageable
counties = gpd.read_file("counties.shp")
results = []
for idx, county in counties.iterrows():
# Load only parcels in this county
parcels = gpd.read_file(
"national_parcels.gpkg",
mask=county.geometry, # Spatial filter
engine="pyogrio" # Fast driver
)
# Process chunk
parcels['area_m2'] = parcels.geometry.area
parcels['density'] = parcels['population'] / parcels['area_m2']
results.append(parcels)
# Combine results
final = gpd.GeoDataFrame(pd.concat(results, ignore_index=True))Dask-GeoPandas partitions data across multiple cores and can spill to disk when memory fills. Supports most GeoPandas operations with parallel execution.
import dask_geopandas as dgpd
# Read with Dask (lazy evaluation)
ddf = dgpd.read_parquet(
"parcels.parquet",
npartitions=32 # Split into 32 chunks
)
# Operations are lazy until compute()
ddf['area_m2'] = ddf.geometry.area
ddf['value_per_m2'] = ddf['assessed_value'] / ddf['area_m2']
# Trigger computation with parallel execution
result = ddf.compute() # Uses all CPU cores
# Or save directly without loading full result
ddf.to_parquet("processed_parcels.parquet")For pure analytical queries (no complex geometry operations), DuckDB Spatial provides SQL interface with excellent performance on large files.
import duckdb
con = duckdb.connect()
con.install_extension("spatial")
con.load_extension("spatial")
# Query a large parcel set without loading it into memory
result = con.execute("""
SELECT
county,
COUNT(*) as parcel_count,
AVG(ST_Area(geometry)) as avg_area_m2,
SUM(assessed_value) as total_value
FROM read_parquet('parcels.parquet')
WHERE land_use = 'RESIDENTIAL'
GROUP BY county
""").df()
print(result)| Dataset Size | Operation Type | Recommended Approach |
|---|---|---|
| < 1M features | Any | Standard GeoPandas |
| 1-10M features | Geometry operations | Dask-GeoPandas |
| 1-10M features | Analytical queries | DuckDB Spatial |
| Large selected workload | Complex spatial | Dask-GeoPandas + chunking |
| Very large selected workload | Any | PostGIS or BigQuery GIS |

How Do You Translate ArcPy Code to GeoPandas?
Replace ArcPy cursors with GeoPandas DataFrames, geoprocessing tools with vectorised methods, and file geodatabases with GeoParquet or GeoPackage. Most ArcPy operations have direct GeoPandas equivalents: Buffer_analysis becomes .buffer(), SpatialJoin_analysis becomes gpd.sjoin(), Dissolve_management becomes .dissolve(). Here's the translation table for the 20 most common patterns.
PATTERN: READ DATA
cursor = arcpy.da.SearchCursor("parcels.shp", ["SHAPE@", "VALUE", "ZONE"])
for row in cursor:
geometry = row[0]
value = row[1]
zone = row[2]Cursor-based iteration
PATTERN: READ DATA
gdf = gpd.read_file("parcels.shp")
# Vectorised access (no loop needed)
areas = gdf.geometry.area
high_value = gdf[gdf['VALUE'] > 100000]Vectorised operations
PATTERN: BUFFER
arcpy.Buffer_analysis("roads.shp", "roads_buffered.shp", "50 METERS")PATTERN: BUFFER
roads = gpd.read_file("roads.shp")
roads_buffered = roads.copy()
roads_buffered['geometry'] = roads.geometry.buffer(50)
roads_buffered.to_file("roads_buffered.shp")PATTERN: SPATIAL JOIN
arcpy.SpatialJoin_analysis(
"parcels.shp",
"flood_zones.shp",
"parcels_flood_risk.shp",
"JOIN_ONE_TO_ONE",
"KEEP_ALL",
match_option="INTERSECT"
)PATTERN: SPATIAL JOIN
parcels = gpd.read_file("parcels.shp")
flood_zones = gpd.read_file("flood_zones.shp")
result = gpd.sjoin(
parcels,
flood_zones,
how="left", # KEEP_ALL
predicate="intersects" # INTERSECT
)
result.to_file("parcels_flood_risk.shp")PATTERN: DISSOLVE
arcpy.Dissolve_management(
"parcels.shp",
"parcels_by_zone.shp",
"ZONE",
[["VALUE", "SUM"], ["AREA", "SUM"]]
)PATTERN: DISSOLVE
parcels = gpd.read_file("parcels.shp")
dissolved = parcels.dissolve(
by='ZONE',
aggfunc={'VALUE': 'sum', 'AREA': 'sum'}
)
dissolved.to_file("parcels_by_zone.shp")| Operation | ArcPy | GeoPandas |
|---|---|---|
| Clip to boundary | Clip_analysis | gpd.clip(gdf, mask) |
| Reproject | Project_management | gdf.to_crs(epsg=4326) |
| Select by attribute | Select_analysis | gdf[gdf['field'] > 10] |
| Calculate area | CalculateField + SHAPE@AREA | gdf.geometry.area |
| Centroids | FeatureToPoint | gdf.geometry.centroid |
| Intersection | Intersect_analysis | gpd.overlay(gdf1, gdf2, 'intersection') |
| Union | Union_analysis | gpd.overlay(gdf1, gdf2, 'union') |
| Merge datasets | Merge_management | pd.concat([gdf1, gdf2]) |

What Replaces ArcPy Spatial Analyst for Raster Processing?
Rasterio handles raster I/O, while NumPy and xarray perform raster algebra. This combination replaces arcpy.sa (Spatial Analyst). It's more explicit than Spatial Analyst's Map Algebra, but offers better performance, native cloud integration (COG, S3), and works without ArcGIS licensing. For raster operations, Rasterio is the GeoPandas equivalent. It's lower-level than Spatial Analyst (more explicit NumPy operations), but offers better performance and cloud integration. For raster-specific workflows, see our ArcPy to Rasterio migration guide for detailed code patterns covering slope, aspect, zonal statistics, and Cloud-Optimized GeoTIFF output.
PATTERN: READ RASTER, APPLY CALCULATION
from arcpy.sa import *
dem = Raster("dem.tif")
slope = Slope(dem, "DEGREE")
slope.save("slope.tif")RASTERIO + NUMPY
import rasterio
import numpy as np
from rasterio.transform import Affine
with rasterio.open("dem.tif") as src:
dem = src.read(1)
transform = src.transform
# Calculate slope using gradient
dy, dx = np.gradient(dem, transform[0])
slope = np.degrees(np.arctan(np.sqrt(dx**2 + dy**2)))
# Write output
profile = src.profile
with rasterio.open("slope.tif", "w", **profile) as dst:
dst.write(slope, 1)PATTERN: EXTRACT RASTER VALUES TO POINTS
from arcpy.sa import ExtractValuesToPoints
ExtractValuesToPoints(
"points.shp",
"elevation.tif",
"points_with_elev.shp"
)RASTERIO + GEOPANDAS
import geopandas as gpd
import rasterio
points = gpd.read_file("points.shp")
with rasterio.open("elevation.tif") as src:
coords = [(p.x, p.y) for p in points.geometry]
points['elevation'] = [v[0] for v in src.sample(coords)]
points.to_file("points_with_elev.shp")CLOUD-NATIVE RASTERS: COG
Rasterio reads Cloud-Optimized GeoTIFFs (COG) directly from S3/Azure without downloading the full file. ArcPy requires local file access or slow streaming.
with rasterio.open("s3://bucket/elevation.tif") as src:
# Read only a 1km² window (fast range request)
window = src.window(xmin, ymin, xmax, ymax)
data = src.read(1, window=window)Performance: A windowed cloud-native raster read can avoid downloading unused data, but timing depends on storage, network, block layout, cache, and implementation. Re-run the selected read before using a timing claim.
How Long Does It Take to Migrate from ArcPy to GeoPandas?
A migration plan should separate inventory, pilot translation, parallel validation, training, and rollout. Timing depends on script complexity, Esri-specific dependencies, data access, review capacity, and operating ownership. Simple scripts may be easier to translate; complex workflows may need a hybrid architecture. If your team needs to build Python skills first, see our guide to training GIS teams for workflow automation.
- Inventory all ArcPy scripts: list file paths, what they do, how often they run
- Measure current performance: run time, memory usage, failure rate
- Identify dependencies: which scripts use Network Analyst, Topology, or other Esri-specific tools?
- Calculate licensing costs: ArcGIS Pro licences, Spatial Analyst, Network Analyst extensions
- Prioritise by measured value, frequency, risk, and dependency rather than a retained ROI claim
- Select pilot: moderate complexity, no Esri-specific dependencies, measurable performance
- Translate using patterns above: test each operation with approved target data
- Benchmark rigorously: run both versions 5 times, measure mean/std deviation
- Validate outputs: geometry checks (ST_Equals), attribute comparison, visual inspection
- Document translation: which ArcPy functions map to which GeoPandas patterns
- Run both ArcPy and GeoPandas versions in a controlled test or approved parallel environment
- Alert on output divergence: automated geometry and attribute comparison
- Monitor memory usage: identify scripts that need Dask or chunking
- Train team: pair programming sessions, code review, documentation
- Build internal library: reusable functions for common operations
- Migrate remaining scripts systematically (priority order from audit)
- Implement hybrid architecture for Esri-dependent workflows
- Create monitoring dashboard: script run times, success rates, memory usage
- Retire a source path only after documented output, access, support, and rollback checks
- Review licence use and terms; do not infer savings without a current model
Complete Workflow Translation: Real Example
Here is a retained workflow illustration: identify parcels in flood zones, calculate risk scores, and export a review dataset. It is not a current client result or a deployment record.
ORIGINAL ARCPY VERSION (RETAINED ILLUSTRATION)
import arcpy
import os
# Setup
arcpy.env.workspace = "C:/data/flood_risk.gdb"
arcpy.env.overwriteOutput = True
# Read parcels and flood zones
parcels = "parcels"
flood_zones = "FEMA_flood_zones"
# Buffer flood zones by 50m for transition zone
print("Buffering flood zones...")
arcpy.Buffer_analysis(flood_zones, "flood_buffered", "50 METERS")
# Spatial join to find at-risk parcels
print("Identifying at-risk parcels...")
arcpy.SpatialJoin_analysis(
parcels,
"flood_buffered",
"parcels_at_risk",
"JOIN_ONE_TO_ONE",
"KEEP_ALL",
match_option="INTERSECT"
)
# Calculate risk score
print("Calculating risk scores...")
arcpy.AddField_management("parcels_at_risk", "RISK_SCORE", "DOUBLE")
arcpy.AddField_management("parcels_at_risk", "RISK_CATEGORY", "TEXT")
cursor = arcpy.da.UpdateCursor(
"parcels_at_risk",
["SHAPE@AREA", "ASSESSED_VALUE", "FLOOD_ZONE", "RISK_SCORE", "RISK_CATEGORY"]
)
for row in cursor:
area = row[0]
value = row[1]
zone = row[2]
# Risk calculation
if zone == "A": # High risk
risk = (value / area) * 1.5
category = "HIGH"
elif zone == "X": # Moderate
risk = (value / area) * 0.8
category = "MODERATE"
else:
risk = 0
category = "LOW"
row[3] = risk
row[4] = category
cursor.updateRow(row)
del cursor
# Export to Excel for underwriting
print("Exporting results...")
arcpy.conversion.TableToExcel("parcels_at_risk", "C:/output/flood_risk_report.xlsx")
print("Complete!")Retained run illustration; runtime, memory, inputs, and comparison require verification
MIGRATED GEOPANDAS VERSION (38 SECONDS)
import geopandas as gpd
import pandas as pd
# Read data (compare format and access performance on the target run)
parcels = gpd.read_parquet("s3://bucket/parcels.parquet")
flood_zones = gpd.read_parquet("s3://bucket/flood_zones.parquet")
# Buffer flood zones (vectorised operation)
flood_buffered = flood_zones.copy()
flood_buffered['geometry'] = flood_zones.geometry.buffer(50)
# Spatial join (uses spatial index automatically)
at_risk = gpd.sjoin(
parcels,
flood_buffered[['geometry', 'FLOOD_ZONE']],
how='inner',
predicate='intersects'
)
# Calculate risk scores (fully vectorised - no cursor)
def calculate_risk(row):
value_density = row['ASSESSED_VALUE'] / row.geometry.area
if row['FLOOD_ZONE'] == 'A':
return value_density * 1.5, 'HIGH'
elif row['FLOOD_ZONE'] == 'X':
return value_density * 0.8, 'MODERATE'
else:
return 0, 'LOW'
# Vectorised apply
at_risk[['RISK_SCORE', 'RISK_CATEGORY']] = at_risk.apply(
calculate_risk,
axis=1,
result_type='expand'
)
# Export to Excel (Pandas integration)
at_risk.drop(columns='geometry').to_excel(
"s3://bucket/output/flood_risk_report.xlsx",
index=False,
engine='openpyxl'
)
print("Complete!")Retained run illustration; runtime, memory, dataset, and comparison require verification
RETAINED PERFORMANCE ILLUSTRATION: WHERE TIME GOES
ArcPy (retained run)
GeoPandas (retained run)
Key insight: Changing format, access pattern, and vectorisation can change the run profile. Reproduce the selected workflow and record the data, machine, libraries, runtime, memory, and output checks before drawing a performance conclusion.
ArcPy to GeoPandas migration isn't “better” or “worse”—it's a trade-off that makes sense for specific workflows.
If you run automated workflows on large datasets, don't need Esri-specific tools like topology rules or Network Analyst, and want to compare licensing and platform options, GeoPandas may be a suitable candidate. The retained benchmark illustrations are not a general performance claim; run the target operation before choosing a migration path. Once validated, you can automate the workflows that previously required manual intervention.
If you rely heavily on topology validation, network routing, or editing workflows in ArcGIS Pro—a hybrid architecture gives you GeoPandas performance for data processing while retaining ArcPy for specialised operations.
The decision is practical, not ideological. Model the current licence cost first, measure the selected operation against its ArcPy baseline, and evaluate whether the migration investment makes sense.
Frequently Asked Questions
Is GeoPandas faster than ArcPy?
Some dated comparisons show faster results for selected GeoPandas operations, but they are not a general performance claim. Re-run the target operation with documented data, libraries, machine, configuration, and validation.
Can GeoPandas read ESRI geodatabases?
Yes, GeoPandas can read File Geodatabases (.gdb) using the OpenFileGDB driver through Fiona/GDAL. Use gpd.read_file('path/to/data.gdb', layer='layer_name') to read specific layers. For better performance, consider converting to GeoParquet format.
What are the limitations of GeoPandas compared to ArcPy?
GeoPandas does not provide every Esri-specific feature and its memory model may not suit a large workload. Consider a hybrid architecture, chunking, Dask-GeoPandas, DuckDB Spatial, PostGIS, or another path after checking the target operation and data.
How do I handle large datasets in GeoPandas?
Choose Dask-GeoPandas, DuckDB Spatial, PostGIS, BigQuery GIS, or another path from the measured memory, query, and validation requirements. Do not use a retained feature-count threshold as a universal rule.
How long does it take to migrate from ArcPy to GeoPandas?
A migration can use an Audit, Pilot, controlled parallel validation, and a staged rollout. Timing depends on the workflow, data, dependencies, team, and review boundary. Simple scripts may be easier to translate; complex workflows require a separate plan.
Is GeoPandas a replacement for ArcPy?
GeoPandas can support some spatial joins, buffers, overlays, file I/O, and attribute-processing workflows, but it does not replace every ArcPy or Esri-specific feature. A hybrid architecture may be appropriate when topology, network analysis, or other dependencies remain. Migration Engine is in development; document the candidate, test, and reviewer before committing to a rewrite.
Skip the Manual Work
If you followed this guide, you now have working code. But you also now own this code.
When you migrate many scripts manually, you can introduce inconsistent coding styles and edge-case failures. Later maintenance of custom code can be harder than the original licensing problem, so use shared checks, documentation, and ownership.
A standardised review and handover path can reduce avoidable variation.
A future workflow tool could help produce consistent scaffolding, tests, logging, and handover records. Migration Engine is in development; no fixed automation split or current autonomous product claim is made here.
Get Workflow Automation Insights
Monthly tips on automating GIS workflows, open-source tools, and lessons from enterprise deployments. No spam.

