Choosing QGIS or ArcGIS Pro for Heritage Compliance Workflows

Every heritage GIS programme eventually has to commit to a desktop platform, and the choice reverberates through licensing budgets, automation code, statutory reporting, and which specialists you can hire. QGIS and ArcGIS Pro will both produce a compliant constraint report or a scheduled-monument overlay; where they diverge is cost structure, the language your automation is written in, the reporting outputs they generate natively, and how cleanly they interoperate with the rest of a field-to-archive pipeline. This guide sets out the decision criteria for heritage compliance work specifically, rather than GIS in general. Its parent overview is Project Scoping & Data Governance.

QGIS versus ArcGIS Pro decision criteria A heritage compliance requirement is weighed against cost, automation language, reporting, and interoperability to select a platform. Platform decision for compliance work Compliance requirement cost, code, reporting, interop QGIS + PyQGIS open, scriptable ArcGIS Pro + arcpy licensed, integrated

Context & When to Use

The decision is rarely about raw capability — both platforms buffer a monument boundary and clip a constraint layer without breaking a sweat. It hinges on organisational fit. A small commercial unit or a grant-funded research project usually cannot justify a recurring per-seat ArcGIS Pro licence and benefits from QGIS’s zero licence cost and a PyQGIS automation stack that any developer can run on a CI runner without a licence server. A local-authority archaeologist embedded in an Esri estate — with the Historic Environment Record already in an enterprise geodatabase and colleagues publishing to ArcGIS Online — gains from staying inside that ecosystem, where arcpy scripts, statutory report templates, and data-sharing are pre-integrated. The honest trade-off: QGIS wins on cost and openness and portability; ArcGIS Pro wins on turnkey enterprise integration and vendor-backed statutory templates. Many heritage teams run a hybrid, doing field capture and automated validation in QGIS and final statutory submission in ArcGIS Pro where a planning authority mandates it.

Criterion QGIS 3.34 ArcGIS Pro 3.x
Licensing / cost Free, GPL; no per-seat or server licence Commercial per-seat; extensions priced separately
Automation language PyQGIS + Processing; runs headless, no licence server arcpy; requires a licensed install to execute
Statutory reporting outputs Report templates via QGIS layouts / Atlas; DIY Built-in report and layout templates, some UK-heritage aligned
Interoperability Native GeoPackage, PostGIS, GDAL formats Native FileGDB / enterprise geodatabase; reads OGC formats
Plugin / extension ecosystem Open plugin repository (QField, MMQGIS, etc.) Esri extensions + ArcGIS Marketplace
Enterprise / web integration GeoServer, QGIS Server, self-hosted ArcGIS Enterprise / Online, tightly integrated
Cross-platform Windows, macOS, Linux Windows only
Best fit Cost-sensitive units, research, Linux automation Esri-estate authorities, mandated Esri submissions

Implementation

The automation language is often the deciding factor, because a compliance pipeline lives or dies on being scriptable and schedulable. The same task — flag features that fall outside a scheduled-monument boundary — is shown in both stacks so the ergonomic difference is concrete.

# PyQGIS path — runs headless on any OS, no licence server
# QGIS==3.34 LTR
# PyQGIS 3.34 — select finds outside the monument boundary
from qgis.core import QgsProject, QgsVectorLayer
import processing

finds = QgsVectorLayer("/data/finds.gpkg|layer=finds", "finds", "ogr")
monument = QgsVectorLayer("/data/monument.gpkg|layer=boundary", "mon", "ogr")
# both must share the site CRS, e.g. EPSG:27700 — substitute your site's EPSG

result = processing.run("native:extractbylocation", {
    "INPUT": finds,
    "PREDICATE": [2],            # 2 = disjoint: features NOT within the boundary
    "INTERSECT": monument,
    "OUTPUT": "/data/finds_outside_monument.gpkg",
})
print(f"Flagged features written to: {result['OUTPUT']}")
# arcpy path — requires a licensed ArcGIS Pro install
# ArcGIS Pro==3.2 / arcpy
# arcpy (ArcGIS Pro 3.2) — same logic in the Esri stack
import arcpy
arcpy.env.overwriteOutput = True
# assumes both feature classes are in EPSG:27700 — substitute your site's EPSG

lyr = arcpy.management.MakeFeatureLayer("finds", "finds_lyr")
arcpy.management.SelectLayerByLocation(
    "finds_lyr", "WITHIN", "monument_boundary",
    invert_spatial_relationship="INVERT",   # keep those OUTSIDE the boundary
)
arcpy.management.CopyFeatures("finds_lyr", "finds_outside_monument")
print("Flagged features copied to finds_outside_monument")

The functional result is identical; the operational difference is that the PyQGIS script runs on a Linux CI runner with no licence, while the arcpy script needs a licensed, Windows-only ArcGIS Pro install to execute at all. For teams committing to versioned, automated pipelines, that constraint often decides the platform — see setting up version control for GIS heritage projects for how that automation is tracked. The same open-vs-licensed logic recurs at the storage layer, weighed in comparing GeoPackage and PostGIS for field-to-archive handoff.

Field note. Platform choice quietly sets your archive format, and formats outlive software. A project that stores everything in an ArcGIS file geodatabase hands a national repository a proprietary container that may need a licensed tool to open in twenty years, whereas a GeoPackage or PostGIS dump is openable with free tools indefinitely. Whichever desktop you pick, mandate an open archival format on deposit so the compliance record survives the software that made it.

Verification

Whichever platform runs the check, verify the flag count against a neutral, platform-independent baseline so the result does not depend on the tool that produced it:

-- PostgreSQL 16 / PostGIS 3.4 — ground-truth the same test independently
SELECT count(*) AS outside_monument
FROM finds f
LEFT JOIN monument_boundary m
  ON ST_Within(f.geom, m.geom)
WHERE m.gid IS NULL;

Expected: the outside_monument count matches the feature count in finds_outside_monument produced by either script above. A mismatch usually means the two platforms disagreed on a boundary-case geometry (a find exactly on the boundary line), where WITHIN and disjoint treat the edge differently — decide your on-boundary policy explicitly and apply it in all three.

Common Errors & Fixes

  • PyQGIS: QgsProcessingException: There was an error executing the algorithm with no output — the two layers are in different CRSs, so the spatial predicate silently matches nothing. Reproject both to the site EPSG before running native:extractbylocation.
  • arcpy: RuntimeError: ERROR 000824: The tool is not licensed — the operation needs an extension or product level the current licence lacks, or the licence server is unreachable. Confirm with arcpy.CheckProduct("ArcInfo") and check out any required extension before the call.
  • Counts differ between platforms on identical data — differing default tolerances or on-boundary handling. Snap geometries to a common grid and standardise on one predicate definition (for example, treat on-boundary as inside) so QGIS, ArcGIS Pro, and the PostGIS ground truth all agree.

Part of Heritage GIS Architecture & Fundamentals.