Automating Repeated Steps in Python
Last updated on 2026-10-20 | Edit this page
Overview
Questions
- When is it worth automating a processing workflow?
- How do I turn a sequence of steps into a function, and run it for many inputs?
- How do I make an automated workflow trustworthy?
Objectives
- Write a function with parameters and defaults that packages a geoprocessing workflow
- Test the function against a known result before using it on new inputs
- Use loops and
groupbyto run a workflow for many distances, layers or counties, and collect the results - Check inputs, report what each run did, and keep one failure from stopping a long loop
When to automate
The Geoprocessing episode answered one question: how many people in Lafayette and West Lafayette live within 1 mile of a supermarket? Real projects rarely stop at one answer. A reviewer asks about half a mile; a partner asks about parks; next year’s ACS data arrives; a colleague wants the same numbers for another county. Each request means running the same steps again with one input changed.
Doing that by hand is slow, but the bigger problem is mistakes: a step skipped, an old layer selected in a drop-down list, a distance typed as 1690 instead of 1609. Automation means writing the steps down once, in a form the computer can repeat exactly, with the parts that change made into parameters.
A useful rule of thumb: the first time, do it by hand and understand it; the second time, notice you are repeating yourself; the third time, automate. A one-off task is often not worth automating, but it is still worth recording (see the Introduction).
Levels of automation
| Level | Python | QGIS | Good for |
|---|---|---|---|
| Record | The notebook itself | Processing > History; a tool’s Advanced > Copy as Python Command | Seeing exactly what was run, with which settings |
| Repeat one tool | A for loop around one function call |
Run as Batch Process (one row per run) | The same tool on many inputs or settings |
| Package several steps | A function with parameters | A model in the Model Designer | A workflow you will run again with different inputs |
| Run for every feature | A loop over groupby groups or rows |
The Iterate button (green arrows) next to an input layer | One output per county, per city, per study area |
| Run without you | A script run on a schedule or a server |
qgis_process on the command line |
Regular updates, very large jobs |
This episode covers the first four. The two tools mirror each other closely: a Python function and a QGIS model are both “these steps, with these inputs”; a loop and a batch run are both “again, for each of these”.
Writing steps you can trust
Automated steps run without anyone watching, so they need habits that manual work can do without:
- Test on a known answer. Before running a function or model on new inputs, run it on a case you have already worked out by hand. Here: 77,473 people within 1 mile of a supermarket, from the Geoprocessing episode. If it does not match, find out why before going further.
- Make the parameters explicit. Every value that might change (a distance, a layer, a column) becomes a parameter with a clear name and, if there is a usual value, a default. Values that should not change (the projected CRS, the number of buffer segments) stay inside.
- Check inputs and fail loudly. A workflow that silently buffers in degrees is worse than one that stops with a message. Check the things that commonly go wrong (CRS, expected columns, row counts) at the start, or make the workflow fix them itself (the QGIS model reprojects every input).
- Report what happened. Print or log the number of rows read, codes replaced, features joined and features written. These counts are your evidence that each run worked.
-
Never overwrite inputs. Write results to new layers
or files with systematic names (
near_400.gpkg,near_800.gpkg), so a rerun cannot destroy the raw data or an earlier result. - Keep one failure from stopping everything, but do not hide it. When looping over 92 counties, catch an error for one county, record it and move on, then read the list of failures at the end.
Summaries over many units
Running the same summary for every county raises a classic question: how do you combine tract values into a county value? Add up counts first, then compute rates from the sums (households without a car divided by all households). Averaging tract percentages gives a small rural tract the same weight as a large urban one and answers a different question. Medians cannot be added up at all: a county median income must come from the county-level ACS table, not from the tract medians.
The same automation in QGIS
The Automating Repeated Steps in QGIS episode builds the same workflow as a Model Designer model and runs it as a batch, with the same results: 6.9%, 23.2% and 67.8% of residents within 400 m, 800 m and 1 mile of a supermarket.
The workflow in code
The function, the test and the loop at the heart of notebook 4a:
PYTHON
def pop_within(facilities, distance, tracts=tracts, boundary=city, column="total_pop"):
"""People inside `boundary` within `distance` meters of `facilities` (areal weighting)."""
t = tracts.copy()
t["tract_m2"] = t.area
in_boundary = gpd.clip(t, boundary)
zone = gpd.GeoDataFrame(geometry=[facilities.buffer(distance).union_all()], crs=t.crs)
near = gpd.overlay(in_boundary, zone, how="intersection")
people_near = (near.area / near["tract_m2"] * near[column]).sum()
people_all = (in_boundary.area / in_boundary["tract_m2"] * in_boundary[column]).sum()
return round(people_near), round(people_all), round(people_near / people_all * 100, 1)
assert pop_within(supermarkets, 1609)[0] == 77473 # the Geoprocessing episode's answer
rows = []
for d in [400, 800, 1609, 3218]:
near, total, share = pop_within(supermarkets, d)
rows.append({"distance_m": d, "people_within": near, "share_pct": share})
pd.DataFrame(rows)
Notebook 4b wraps the Census cleaning of the Cleaning episode in a
clean_acs() function, runs it on all 1,696 Indiana tracts,
and summarizes all 92 counties with a loop (and with
groupby().apply()).
Challenge 1: Parameter or not?
You are turning the park access analysis into a function. Which of these should be parameters, which should have defaults, and which should stay fixed inside the function?
- The parks layer
- The walking distance (800 m)
- The projected CRS (EPSG:26916)
- The population column (
total_pop) - The number of segments used to draw buffer circles
- A parameter without a default: the function is about some set of parks or facilities.
- A parameter with a default of 800: the usual case, easy to change for a sensitivity test.
- Fixed (or checked): it is a technical requirement, not a choice the user should make each time. For another state you would change it once, or make the function choose a suitable CRS.
- A parameter with a default of
total_pop, so the same function can count children or households. - Fixed: it affects the result only slightly and should be the same in every run so results are comparable.
Challenge 2: The average of percentages
A loop over Indiana’s counties computes each county’s share of
households without a car as the mean of its tracts’
pct_no_vehicle. For Tippecanoe County this gives 10.3%, but
notebook 4b reports 9.7%. Which is right, and why do they differ?
9.7% is the share of the county’s households: the sum of households without a car divided by the sum of households. The mean of 42 tract percentages (10.3%) gives every tract the same weight, so small tracts near campus with very high shares pull it up. It answers “what is the typical tract like?”, not “what share of the county’s households have no car?”. Add counts first, then divide.
Challenge 3: A silent failure
A colleague’s loop over 92 counties uses
try: ... except: pass so that “nothing stops the loop”. The
output table has 88 rows and no error appeared. What went wrong with the
design, and how would you change it?
except: pass hides every error, so four counties failed
without a trace. Catch the error, but record it: print
the county and the message, or append it to a list of failures, and
check the count at the end (assert len(table) == 92, or
report how many failed). Then fix the cause and rerun.
- Automate work you will repeat; record work you will not
- A function packages steps; parameters are the parts that change, defaults the usual values
- Test on a known answer, check inputs, and report counts so every run leaves evidence
- Loops (or
groupby) run the same workflow for many inputs and collect the results in one table - Combine units by adding counts first, then computing rates
- Catch errors in long loops, but record them and check the totals at the end
Notebooks
| Notebook | What you will do |
|---|---|
| 4a. Functions and Loops | Turn the Geoprocessing episode’s areal weighting into a function, test it, and loop over distances and facility layers |
| 4b. Many Counties | Clean the ACS table for all of Indiana with one function, explain the rows that do not join, summarize 92 counties and save a map per county |