
Township America Python SDK: PLSS Lookups in Python for GIS Scripts and Spatial Data Pipelines
Use the PLSS Python SDK to geocode legal descriptions in Python. Batch convert a CSV to coordinates and GeoJSON polygons, then load the result into GeoPandas and QGIS.
A GIS or spatial data pipeline almost never starts with coordinates. It starts with a column of legal descriptions: a parcel table, a lease schedule, or a permit export where every tract reads something like NWSE 12 152N 96W, 5th Principal Meridian. That text is precise and legally correct, and it is also opaque to GeoPandas, GDAL, ArcPy, or PostGIS until something turns it into latitude and longitude. Resolving those descriptions one row at a time, by pasting each into a browser tool, is fine for a handful and impossible for a table that refreshes on a schedule. This guide shows how to do the same conversion in Python with the PLSS Python SDK: install it, authenticate, run a single lookup, batch convert a CSV, and hand the coordinates and parcel boundaries to the GIS tools you already use.
What the Python SDK covers
The Township America REST API exposes four endpoints: single lookup, batch, autocomplete, and map tiles. The Python SDK wraps that API in a typed client so you work with Python objects instead of raw HTTP requests and JSON parsing. It resolves PLSS legal descriptions (state, principal meridian, township, range, section, and aliquot part) to coordinates across 30+ PLSS states and 37 principal meridians, down to the 1/256 aliquot part, which is about 2.5 acres. The conversion is calculated from official BLM survey data.
Two capabilities matter most for GIS work. First, every section and quarter-section lookup returns a full GeoJSON polygon of the actual BLM survey boundary, not just a centroid point, so you get real parcel geometry rather than an estimate. Second, the batch endpoint accepts up to 100 descriptions per request, which is what makes a scheduled pipeline practical.
Install and authenticate
The SDK is published on PyPI, so a standard install pulls it into any virtual environment:
pip install townshipamerica
Authentication uses an API key. Create one from the developer portal linked on the API page, then keep it out of source control by reading it from an environment variable. Here is a first lookup in a plain Python script:
import os
from townshipamerica import TownshipAmerica
client = TownshipAmerica(api_key=os.environ["TOWNSHIP_AMERICA_API_KEY"])
result = client.search("T4N R5E Sec 12 NE Indian Meridian")
centroid = result.centroid
if centroid is not None:
print(centroid.geometry.latitude, centroid.geometry.longitude)
The search call returns a typed feature collection. The centroid property gives you the representative point, and centroid.geometry carries the latitude and longitude. If the key is missing or a description cannot be resolved, the client raises a typed exception mapped to the HTTP status, so a pipeline can catch and log the failing row instead of stopping.
Batch convert a CSV of legal descriptions
Most pipelines begin with a file rather than a single string. Assume a CSV named parcels.csv with a legal_description column. The pattern is: read the column, send it to the batch endpoint in chunks of 100, and collect coordinates and polygon geometry for each row. Splitting into chunks keeps every request inside the batch limit.
import os
import csv
import json
from townshipamerica import TownshipAmerica
client = TownshipAmerica(api_key=os.environ["TOWNSHIP_AMERICA_API_KEY"])
def chunked(items, size=100):
for start in range(0, len(items), size):
yield items[start:start + size]
with open("parcels.csv", newline="") as f:
descriptions = [row["legal_description"] for row in csv.DictReader(f)]
rows = []
features = []
for chunk in chunked(descriptions):
for description, fc in zip(chunk, client.batch_search(chunk)):
centroid = fc.centroid
if centroid is None:
print("No match:", description)
continue
rows.append({
"legal_description": description,
"latitude": centroid.geometry.latitude,
"longitude": centroid.geometry.longitude,
})
features.append(fc)
From here you can write two artifacts. A flat CSV of coordinates is the right output when the next step is a spreadsheet, a database load, or a join back to the source table:
with open("parcels_coords.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["legal_description", "latitude", "longitude"])
writer.writeheader()
writer.writerows(rows)
A GeoJSON file is the right output when the next step is a map or a spatial join, because it preserves the survey polygon rather than reducing each parcel to a point:
collection = {
"type": "FeatureCollection",
"features": [
json.loads(fc.grid.model_dump_json())
for fc in features if fc.grid is not None
],
}
with open("parcels.geojson", "w") as f:
json.dump(collection, f)
Load the results into GeoPandas and QGIS
Once you have coordinates and geometry, the SDK is out of the way and the output flows into standard tools. GeoPandas reads the GeoJSON directly, and from there you can reproject, spatially join, or write a Shapefile:
import geopandas as gpd
gdf = gpd.read_file("parcels.geojson")
gdf = gdf.set_crs("EPSG:4326")
gdf.to_file("parcels.shp")
The Shapefile opens in QGIS or ArcGIS as parcel boundaries you can style, label, and overlay on imagery. Because the geometry is the BLM survey boundary and not a 640-acre square drawn around a centroid, the parcels line up with adjacent tracts the way the survey record does. That accuracy matters most where sections are irregular, which is common along survey correction lines and near water boundaries. For async pipelines, GeoPandas patterns at scale, and other production notes, the Python SDK advanced guide goes deeper than this walkthrough.
Python SDK, TypeScript SDK, or the REST API directly
The three access paths serve different contexts, so the right one depends on where the code runs.
- Use the Python SDK when the work lives in the Python data stack: a GeoPandas or GDAL pipeline, an ArcPy script, a notebook, or a scheduled ETL job. This is the natural fit for GIS and data science teams.
- Use the TypeScript SDK when the conversion happens in a web app, a serverless function, or a Node service. It gives the same endpoints with types suited to that runtime.
- Call the REST API directly when you work in a language without an SDK, or when a single lookup does not justify a dependency. A plain HTTPS request to the same endpoints returns the same JSON.
All three read from one API and one BLM-derived dataset, so results are consistent whichever you choose. The batch endpoint that powers this pipeline is metered per record on the API Build, Scale, and Enterprise tiers; see the API pricing and the developer overview for current limits.
If your input mixes Texas tracts with PLSS descriptions, the same API resolves the Texas Abstract, Block, and Survey grid alongside PLSS, so a pipeline covering both survey systems does not need a second source. Start with the Python SDK quick start, point it at a real column of legal descriptions, and let the coordinates land in the format your GIS already reads.