TA logoThe Technical Artist
Home/Python/First Pipeline Script
tutorial - starter
you build a tool
~45 minutes

Your First Pipeline Script

Startertutorial~4 min readreviewed 2026-07-05

You have a folder called textures containing files named rock albedo 2.png, Rock_Albedo_FINAL.png, and rockalbedo-old(1).png. By the end of this page you'll have a real command-line tool that fixes it - safely - and you'll have learned loops, functions, and paths through the fixing, not before it.

You walk away with

A working CLI tool (download download the finished script) with dry-run mode, collision detection, and an undo log - plus the three habits that separate pipeline scripts from pipeline accidents. If you've read Intro to Python, you're ready.

Why A Renamer, Of All Things

Because it's the truest first pipeline problem: messy inputs, a convention to enforce, files you must not destroy, and a payoff you can feel. Every concept it needs - walking folders, string surgery, dry runs - reappears in every tool you'll ever write. And when you're done, you'll actually use it, which beats any exercise about calculating fibonacci numbers.

Step 1 - See The Files Before Touching Them

Everything starts with pathlib. Make a scratch folder with some badly-named files (or download the finished script later and test on a copy), then:

Python
from pathlib import Path

folder = Path(r"D:/scratch/textures") # r"" so backslashes behave on Windows
for f in folder.iterdir():
 if f.is_file():
 print(f.stem, "|", f.suffix) # name without extension | extension

Run it. That loop - for each file, look at it - is the skeleton of every batch tool in existence. f.stem and f.suffix doing the splitting for you is why we use pathlib and not string chopping.

Step 2 - The Convention Function

Decide the target: T_Rock_Albedo_02.png - a T_ prefix, TitleCase words joined by underscores, numbers padded to two digits. Now write the function that converts one name. One name, not the folder - small functions you can test in isolation are the habit that keeps tools debuggable:

Python
import re

def convention(name, prefix="T_"):
 # split on spaces, dashes, underscores AND camelCase seams
 words = re.split(r"[\s\-_]+", re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", name))
 words = [w for w in words if w]

 if words and words[-1].isdigit(): # pad trailing numbers: 2 -> 02
 words[-1] = f"{int(words[-1]):02d}"

 new = "_".join(w if w.isdigit() else w.title() for w in words)
 return new if new.startswith(prefix) else prefix + new

# test it immediately, in the same file:
for test in ["rock albedo 2", "Rock_Albedo_FINAL", "rockAlbedo-old"]:
 print(test, "->", convention(test))

The regex looks scarier than it is: one split pattern for separators, one lookahead to cut camelCase at the seam. Steal them; everyone does. The important move is the last three lines - testing the function on hostile examples before it ever meets a real file.

Step 3 - Plan, Don't Act

Here's the habit that will one day save your job: the script's default mode changes nothing. It builds a plan and shows you:

Python
def plan_renames(folder, prefix="T_"):
 pairs = []
 for f in sorted(folder.iterdir()):
 if not f.is_file():
 continue
 new_name = convention(f.stem, prefix) + f.suffix.lower()
 if new_name != f.name: # skip already-correct files
 pairs.append((f, f.with_name(new_name)))
 return pairs

for old, new in plan_renames(folder):
 print(f"{old.name:<40} -> {new.name}")

Run it, read the plan, find the surprise. There's always a surprise - a file you forgot, a name the function mangles. Finding it in printout costs nothing. Finding it after renaming costs an afternoon and some trust.

Step 4 - Safety Rails

Two rails turn this from homework into a pipeline tool. Collision detection: if rock albedo 2.png and Rock_Albedo_02.png both map to the same new name, one would silently vanish. Refuse loudly instead:

Python
targets = [b.name for _, b in pairs]
dupes = {t for t in targets if targets.count(t) > 1}
if dupes:
 raise SystemExit(f"COLLISION: multiple files map to {sorted(dupes)}")

An undo log: when you do rename, write every old -> new pair to a text file first. It's four lines, and it converts "irreversible" into "annoying to reverse" - which, in pipeline work, is the whole religion:

Python
with open(folder / "_rename_log.txt", "a", encoding="utf-8") as fh:
 for old, new in pairs:
 old.rename(new)
 fh.write(f"{old.name} -> {new.name}\n")

Step 5 - Make It A Tool

A script becomes a tool when someone else can run it without reading it. argparse gives you a folder argument, a --prefix option, an --apply flag (dry-run stays the default - the safe thing should always be the lazy thing), and free --help text. The finished script assembles all five steps - compare it against yours; where they differ, one of us learned something.

Shell
python ta_batch_rename.py D:/textures # shows the plan
python ta_batch_rename.py D:/textures --apply # commits it
python ta_batch_rename.py --help # someone else's first command

The Challenge

Extend it - pick one, or all three

1 (warm-up): add --recursive so it walks subfolders too (rglob is your friend - mind the log file). 2 (real): add suffix detection - files containing "normal"/"nrm" get _N appended, "roughness" gets _R, so rock normal.png -> T_Rock_N.png. 3 (job-interview-grade): write ta_batch_rename_undo.py, which reads a rename log and reverses it. When all three work, you have a portfolio piece - write the breakdown.

Where next: run this same project inside a DCC with the Rosetta Stone, raid the snippet library for parts, or level the tool up with a watch folder (snippet library -> CLI & Automation).