Skip to main content
Technical Deep-Dive

From ArcPy to GeoPandas: A Technical Migration Guide

How to migrate ArcPy scripts to GeoPandas—including when NOT to switch, memory management for massive datasets, and the hybrid architecture we use when GeoPandas isn't enough.

PUBLISHEDJAN 2025
CATEGORYTECHNICAL
AUTHORAXIS SPATIAL
Sumi-e ink painting showing transformation between two code styles

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.

Japanese ink illustration showing transformation from tangled legacy code to clean modern implementation

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:

1

Data Preparation: GeoPandas

Load, filter, transform, spatial joins—everything that's fast in GeoPandas.

2

Specialised Operations: ArcPy

Export to File Geodatabase, run topology validation or network routing, export results.

3

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.

BENCHMARK_SUITE.exe
1/3

TEST_01

SPATIAL JOIN
10,000 parcels × 250 flood zones
ArcPy (SpatialJoin_analysis)847 sec

Cursor-based iteration, File GDB locks

GeoPandas (gpd.sjoin)11.3 sec

Vectorised with spatial index

75×
faster

TEST_02

BUFFER + DISSOLVE
50,000 road segments, 50m buffer
ArcPy (Buffer → Dissolve)47 min

Sequential file operations

GeoPandas (buffer → unary_union)38 sec

Chained vectorised ops

75×
faster

TEST_03

ATTRIBUTE CALC
1M parcels, area calculation
ArcPy (da.UpdateCursor)32 min

Row-by-row cursor iteration

GeoPandas (vectorised)6.7 sec

NumPy array operations

287×
faster
Use to navigate
Performance benchmark visualization comparing ArcPy and GeoPandas execution times

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.

Memory Management 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")
ResultRetained illustration: confirm the dataset, machine, peak memory, runtime, and output checks before reuse.

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 SizeOperation TypeRecommended Approach
< 1M featuresAnyStandard GeoPandas
1-10M featuresGeometry operationsDask-GeoPandas
1-10M featuresAnalytical queriesDuckDB Spatial
Large selected workloadComplex spatialDask-GeoPandas + chunking
Very large selected workloadAnyPostGIS or BigQuery GIS
Memory management strategies for large geospatial datasets

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")
OperationArcPyGeoPandas
Clip to boundaryClip_analysisgpd.clip(gdf, mask)
ReprojectProject_managementgdf.to_crs(epsg=4326)
Select by attributeSelect_analysisgdf[gdf['field'] > 10]
Calculate areaCalculateField + SHAPE@AREAgdf.geometry.area
CentroidsFeatureToPointgdf.geometry.centroid
IntersectionIntersect_analysisgpd.overlay(gdf1, gdf2, 'intersection')
UnionUnion_analysisgpd.overlay(gdf1, gdf2, 'union')
Merge datasetsMerge_managementpd.concat([gdf1, gdf2])
Code translation patterns from ArcPy to GeoPandas

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)

Read FGDBRetained split
Buffer operationRetained split
Spatial joinRetained split
Cursor iterationRetained split
Export to ExcelRetained split

GeoPandas (retained run)

Read ParquetRetained split
Buffer operationRetained split
Spatial joinRetained split
Vectorised calculationRetained split
Export to ExcelRetained split

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.

NEXT STEP

Discuss an ArcPy migration workflow

Share the workflow, dependencies, data boundary, and verification question. The next step may be a bounded migration study, a hybrid path, or no migration.