Shapefile round trips: what three pipelines changed

Dataset Last tested 24 October 2026

All three configured pipelines changed long field names, while their null and empty-text results differed. Mapshaper changed the two tested integers beyond JavaScript’s exact integer range; PyShp and GDAL/OGR returned those integers unchanged. Every pipeline retained both point coordinates in all twenty fixtures, but that coordinate result did not guarantee unchanged attributes.

Read the change, not a tool score

The table combines twenty unchanged synthetic fixtures across three explicit pipelines. PyShp received declared DBF schemas, while Mapshaper and ogr2ogr inferred their schemas from GeoJSON. That difference matters: PyShp’s six-decimal field definition rounded one fixture, while the other configured pipelines retained those input values. The counts describe these configurations, not a ranking of software quality.

“Exact properties” compares the returned keys and values with the original properties. JSON booleans remain distinct from numbers; numeric values compare numerically. “Exact coordinates” compares the two returned point positions. “What changed” names the observed difference for every No. The full JSON and CSV retain the inputs, outputs, field definitions and per-run evidence.

FixturePipeline/versionExact propertiesExact coordinatesWhat changed
F01pyshp 3.1.6YesYesNone in returned properties or coordinates.
F01mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F01gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F02pyshp 3.1.6NoYesField population_total became population (10 characters).
F02mapshaper 0.7.97NoYesField population_total became population (10 characters).
F02gdal-ogr 3.11.5NoYesField population_total became population (10 characters).
F03pyshp 3.1.6NoYesBoth names became population; dictionary keys collided. Both positional values survived.
F03mapshaper 0.7.97NoYesFields became population and populati_1; both values survived.
F03gdal-ogr 3.11.5NoYesFields became population and populati_1; both values survived.
F04pyshp 3.1.6YesYesNone in returned properties or coordinates.
F04mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F04gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F05pyshp 3.1.6YesYesNone in returned properties or coordinates.
F05mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F05gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F06pyshp 3.1.6YesYesNone in returned properties or coordinates.
F06mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F06gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F07pyshp 3.1.6NoYesNull became the text None; empty text stayed empty.
F07mapshaper 0.7.97NoYesNull became empty text; null and empty text became indistinguishable.
F07gdal-ogr 3.11.5NoYesEmpty text became null; the original null stayed null.
F08pyshp 3.1.6YesYesNone in returned properties or coordinates.
F08mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F08gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F09pyshp 3.1.6YesYesNone in returned properties or coordinates.
F09mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F09gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F10pyshp 3.1.6NoYes1.23456789 became 1.234568; −0.0000004 became −0.0 under the six-decimal schema.
F10mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F10gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F11pyshp 3.1.6YesYesNone in returned properties or coordinates.
F11mapshaper 0.7.97NoYes9007199254740993→9007199254740992; 9007199254740995→9007199254740996.
F11gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F12pyshp 3.1.6YesYesNone in returned properties or coordinates.
F12mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F12gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F13pyshp 3.1.6YesYesNone in returned properties or coordinates.
F13mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F13gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F14pyshp 3.1.6YesYesNone in returned properties or coordinates.
F14mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F14gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F15pyshp 3.1.6NoYesThe 300-byte ASCII value was cut to 254 bytes.
F15mapshaper 0.7.97NoYesThe 300-byte ASCII value was cut to 254 bytes.
F15gdal-ogr 3.11.5NoYesThe 300-byte ASCII value was cut to 254 bytes.
F16pyshp 3.1.6NoYesTrailing spaces were removed; leading spaces survived.
F16mapshaper 0.7.97NoYesLeading and trailing spaces were removed.
F16gdal-ogr 3.11.5NoYesLeading and trailing spaces were removed.
F17pyshp 3.1.6NoYesNested objects became Python-style text representations.
F17mapshaper 0.7.97NoYesNested objects became null.
F17gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F18pyshp 3.1.6NoYesArrays became Python-style text representations.
F18mapshaper 0.7.97NoYesArrays became null.
F18gdal-ogr 3.11.5NoYesThe tags array field was dropped; generated FID values 0 and 1 appeared.
F19pyshp 3.1.6YesYesNone in returned properties or coordinates.
F19mapshaper 0.7.97YesYesNone in returned properties or coordinates.
F19gdal-ogr 3.11.5YesYesNone in returned properties or coordinates.
F20pyshp 3.1.6NoYesNumeric 42 became text 42; the existing text value survived.
F20mapshaper 0.7.97NoYesThe text value became null; numeric 42 survived.
F20gdal-ogr 3.11.5NoYesNumeric 42 became text 42; the existing text value survived.

Keep storage and dictionary projection separate

F03 exposes two different field-name outcomes. PyShp’s two long names both become population; its positional records still contain both values, but converting those records to a dictionary collapses the duplicate keys. That is a loss in the projected dictionary, not evidence that one value disappeared from the DBF file. Mapshaper and OGR instead return population and populati_1, retaining both values under changed names.

These are the kinds of changes to check before joining a returned table to another dataset. A new field name may be manageable if it is recorded deliberately. A silent collision is harder to spot because the dictionary still looks structurally valid. The original field definitions and PyShp positional records are retained so the different cases can be inspected1.

Nulls, numbers and text need separate checks

F07 starts with one null and one empty string. PyShp returns the null as the text None; Mapshaper returns both as empty strings; OGR returns both as null. Those are three distinct outcomes. A downstream expression that distinguishes “missing” from “present but blank” would receive different inputs even though the two points still occupy the same locations.

In F11, Mapshaper returns 9007199254740992 and 9007199254740996 for inputs 9007199254740993 and 9007199254740995. The other two pipelines retain the original integers. F15 is different: all three cut the 300-byte ASCII value to 254 bytes. Do not treat a numeric representation change and a width-limited text truncation as the same failure or assume that changing the geometry format alone fixes either2.

Examine nested and mixed values explicitly

The OGR round trip returns F17’s nested object unchanged, but its intermediate shapefile field is a String. That end-to-end result does not mean DBF acquired native nested-object storage. F18’s array field is unsupported in this configured OGR export: it disappears and generated FID values appear instead. The retained process warnings and returned schemas make those two cases distinguishable3.

Mixed values also depend on inference and declaration. For F20, PyShp and OGR return numeric 42 as text while preserving the existing text value. Mapshaper preserves 42 but returns the text value as null. If a field must retain one stable type, define and verify that requirement before selecting an export configuration.

Use the table viewer to inspect one fixture across all three implementations, then consult the method before reusing a result. The corpus has two synthetic points per fixture; it is deliberately small enough that each change can be explained directly.

Not covered on this page

Sources

  1. PyShp project documentation. ↩

  2. Mapshaper command reference and GDAL’s shapefile driver. ↩

  3. GDAL: ogr2ogr. ↩

Code on this page is original and MIT licensed.

How we test: methodology. Found a mistake? Corrections.