← Back to Blog

One ArcGIS Pro toolbox in every project: the .pyt + Favorites pattern

One ArcGIS Pro toolbox in every project: the .pyt + Favorites pattern

Most GIS shops accumulate the same dozen scripts — an exporter, a thumbnail stamper, a service inventory — and then copy them, project by project, until five slightly different versions are drifting in five project folders. The fix isn’t more discipline. It’s one Python toolbox, pinned once through ArcGIS Pro Favorites, so it loads in every project — and a git pull is the entire update story.

This is the exact pattern we run at All Things Spatial. One toolbox, ATS-Tools.pyt, lives in a git repo. Every workstation pins it as a favorite once. After that, adding or fixing a tool is a commit on one machine and a pull on the next — the toolbox is already wired into Pro, so the new tool simply appears. Here is how it’s built and why each decision earns its place.

What we actually ship

A single .pyt with a handful of focused tools, each tagged into a fixed set of categories so the toolbox never sprawls:

ToolCategoryWhat it does
Generate Branded AGOL ThumbnailsAGOLStamp a consistent branded thumbnail on every item you own (whole account or selected folders).
Describe Feature ServicesAGOLInventory services — fields, types, domains, counts, spatial reference — to TXT / CSV / XLSX.
Export Pro Symbols to SVG/PNGSymbologyExtract symbols from a .stylx to SVG (and PNG) for web/CDN reuse.
Clone Items Cross-OrgMigrationAssess-then-migrate content between ArcGIS Online orgs with a per-item decision report.
Export Features to FolderData PrepExport a layer (optionally filtered) to Shapefile / CSV / GeoJSON / file geodatabase.
_Template — Copy MeTemplatesA skeleton tool with one of every common parameter type — clone it to start a new tool.

The categories are a controlled vocabulary — AGOL, Data Prep, Symbology, Analysis, Publishing, Migration, Templates — recorded in a CONVENTIONS.md in the repo. A new category is a deliberate decision, not whatever string someone typed that day. That one rule is what keeps a shared toolbox legible a year in.

Why a Python toolbox, not loose scripts or a model

A Python toolbox is just a .pyt file — plain text that defines a toolbox and one or more tools, and behaves exactly like any system geoprocessing tool inside Pro, ModelBuilder, and the Python window (What is a Python toolbox — Esri). Because it’s plain text, it is git-diffable: every change shows up as a clean line diff in review. We reserve binary .atbx toolboxes for workflows that genuinely need to stay visual in ModelBuilder; mature, code-shaped logic lives in the .pyt where it can be read, reviewed, and edited in seconds.

The structure: thin tools over a testable lib

Each tool class stays thin — it declares parameters and calls a function. The real work lives in a sibling lib/ package of pure-Python modules (ats_thumbnails.py, ats_describe.py, …) that import cleanly without arcpy. That split is the difference between logic you can unit-test on any machine and logic you can only run inside Pro.

import arcpy, os, sys

class Toolbox(object):
    def __init__(self):
        self.label = "ATS Tools"
        self.alias = "atsTools"
        self.tools = [ExportFeatures, GenerateBrandedThumbnails, DescribeFeatureServices]

class ExportFeatures(object):
    def __init__(self):
        self.label = "Export Features to Folder"
        self.description = "Export a layer to Shapefile / CSV / GeoJSON / FGDB."
        self.category = "Data Prep"          # controlled vocabulary only
        self.canRunInBackground = False

    def getParameterInfo(self):
        layer = arcpy.Parameter(displayName="Input layer", name="layer",
            datatype="GPFeatureLayer", parameterType="Required", direction="Input")
        out = arcpy.Parameter(displayName="Output folder", name="out_folder",
            datatype="DEFolder", parameterType="Required", direction="Input")
        return [layer, out]

    def isLicensed(self): return True
    def updateParameters(self, p): return
    def updateMessages(self, p): return

    def execute(self, parameters, messages):
        export = _import_lib("ats_export")        # lazy import + reload (below)
        export.run(parameters[0].valueAsText, parameters[1].valueAsText)
        messages.addMessage("Done.")

The Favorites pattern

Here is the move that makes one toolbox show up in every project. ArcGIS Pro lets you mark a toolbox as a favorite and add favorites to every new project automatically (Manage favorites — Esri). A favorite stores a path to the live file — so it always points at whatever your last git pull left on disk. Set it once per machine:

  1. In the Catalog pane, open the Computer tab and browse to your repo’s …\toolbox\ATS-Tools.pyt.
  2. Right-click the .pytAdd To Favorites.
  3. Switch to the Favorites tab, right-click ATS-ToolsAdd To New Projects (a pin appears).
  4. Done. Every new project now opens with the toolbox already loaded — no per-project importing.

And the update story, in full:

# machine A — add or fix a tool
git commit -am "describe-services: add domain export" && git push

# machine B — the favorite already points at this file
git pull        # next project you open has the new tool. No re-pin.
One per-machine catch: favorites are stored in %AppData%\Esri\ArcGISPro\Favorites\Favorites.json — per Windows profile, and not in your repo. So the pin is a one-time setup on each workstation. It also stores an absolute path, so every machine must mount the repo at the same path (or point the favorite at a UNC share like \\server\gis\ats-scripts). We keep a tiny stdlib-only check_favorites.py in the repo that reads Favorites.json and confirms the pin is wired — a 5-second verification on a new machine.

The reload gotcha

One trap will cost you twenty minutes the first time. Catalog > Refresh reloads the .pyt, but Pro caches the sibling lib/ modules — so your code edit in ats_export.py appears to do nothing. The fix is to importlib.reload() the lib module inside the import helper every tool uses:

def _import_lib(name):
    """Import a sibling lib/ module, reloading so edits land without a full Pro restart."""
    libdir = os.path.join(os.path.dirname(__file__), "lib")
    if libdir not in sys.path:
        sys.path.insert(0, libdir)
    import importlib
    mod = importlib.import_module(name)
    return importlib.reload(mod)        # pick up lib/ edits on the next run

Operator note: after you first add those reload calls, the first pickup still needs a Pro restart (to load the .pyt that contains them). Every edit after that lands on a re-run.

Guardrails we bake in

A few small conventions keep a shared toolbox safe to hand to anyone:

Dry run defaults ON for any tool that writes to a portal. The user has to consciously turn it off before anything is mutated:

dry = arcpy.Parameter(displayName="Dry run (preview only — do not write to the portal)",
    name="dry_run", datatype="GPBoolean", parameterType="Optional", direction="Input")
dry.value = True                         # default ON
# … later, in execute() …
if not dry_run:
    item.update(thumbnail=str(path))     # the only line that touches the portal

No credentials in code. Tools that talk to ArcGIS Online use the active Pro sign-in; cross-org tools use named keyring profiles entered once, never strings in a file:

from arcgis.gis import GIS
gis = GIS("pro")        # the signed-in Pro session — nothing secret on disk

Lazy imports. Heavy dependencies (PIL, arcgis, openpyxl) are imported inside execute(), not at the top of the .pyt. The toolbox always loads fast, and a missing dependency in one environment never crashes the whole toolbox — only the tool that needs it.

Why this is worth the setup

Your house tooling is a product surface. When every project on every machine has the same vetted tools a pull away — with dry-run guards, no secrets on disk, and a clean line diff on every change — you stop maintaining five copies of one script and start compounding one good toolbox. That’s the difference between scripts you happen to have and tooling you actually run.

References

Want your team’s scripts to work like this?

We turn a folder of one-off ArcPy scripts into one shared, version-controlled Pro toolbox — thin tools over a tested library, dry-run guards, no secrets on disk, pinned into every project.

Book a free intro call