Faragiri

Learn Scripting and automation Python

A folder of scans tidied in twelve lines, the case-insensitive glob that catches half the files, and the one flag that separates a useful script from a bad afternoon.

4Sections
3Min read
4Depth

Before the first rename

What the script has checked, and what it has not

Every yes below is one line in the script. The unknown row is the one that decides whether the run is safe, and no script can answer it for you.

  1. YesEvery file was foundSuffix compared in lower case, not matched by globYes
  2. YesNo two files want the same new nameUnique new names counted against the file countYes
  3. YesNothing has been written yetThe plan printed, and the disk untouchedYes
  4. YesThe external tool succeededcheck=True, so a non-zero return raisesYes
  5. Not checkedThe new names are the ones you wantedjpeg stayed jpeg, and only you know if that is rightNot checked
  6. NoThe originals are recoverableNothing here made a copy, and rename is not a backupNo

The third state is why this figure is a checklist. Nobody looked at the fifth row, and a script that printed a plan and had it approved is the only thing that turns it into a yes.

A script that renames files should print what it would do and change nothing,
until you have read the list. On a folder of scans the plan looks like this,
produced by python3 tidy.py scans with no other argument:

  • would rename 'IMG_0041 copy.JPG' -> 'img_0041.jpg'
  • would rename 'IMG_0042.jpeg' -> 'img_0042.jpeg'
  • would rename 'IMG_0043.JPG' -> 'img_0043.jpg'

Three things are visible in that list that would not have been visible
afterwards. The suffix is lowercased but jpeg is not turned into jpg, so two
naming schemes survive. The word copy is stripped. And nothing collided, which
the script checked by comparing the number of unique new names against the
number of files, before touching the disk.

Finding the files, and the trap in the pattern

Path("scans").rglob("*.JPG") answers two of the three images,
because glob patterns are case sensitive and one file is .jpeg in
lower case. That is the single most common bug in a tidying script and it is
silent: the run reports success and half the folder is untouched.

The reliable version tests the suffix rather than the pattern:
if p.suffix.lower() in {".jpg", ".jpeg"}, which caught all three.
Reach for iterdir and a comparison when correctness matters, and
keep rglob for the cases where you control the names.

Shelling out, and the argument people leave off

When the job needs a real tool, subprocess.run is the way, and
it takes a list rather than a string, which is what keeps a filename containing
a space from becoming two arguments.
subprocess.run([...], capture_output=True, text=True) gives back an
object with returncode and stdout as a string, and a
successful call answered pixelWidth: 4096 on the last line.

The argument to add is check=True. Without it a failing command
answers 1 and the script carries on cheerfully;
subprocess.run(["false"], check=True) raises
CalledProcessError instead, which is what you want when the next
step assumes the last one worked. A tidying script that ignores return codes
will happily delete the originals after a conversion that failed.

What to run in 2026

pathlib is the standard library answer for anything touching a
filesystem, free, and it has been the recommended one for years.
p.suffix, p.stem, p.with_name and
p.rename replaced string surgery on paths with
os.path.splitext and manual joining, which is where the bug with
the space in the filename used to live.

launchd is what puts the script on a schedule on a Mac, free
with the system, and it replaced cron there: a plist in
~/Library/LaunchAgents runs on an interval or at login and, unlike
cron, restarts the job after a reboot. Whatever schedules it, remember what
changes: a scheduled run has a different working directory and a much smaller
PATH than your shell, so use absolute paths for everything, including the
interpreter.

Where this stalls people

The stall is a script that worked interactively and does nothing on a
schedule, with no error anywhere. It is almost always one of three things: the
working directory is not what you assumed, so a relative path found nothing; the
interpreter on the scheduler's PATH is not the one with your packages; or the
output went to a log nobody configured and the failure is invisible.

All three are fixed by the same habit. Take no arguments from the
environment: absolute path to the interpreter, absolute path to the folder,
absolute path to the log. Then run it once from a shell with the environment
emptied, before trusting it to run at three in the morning. Which interpreter
that absolute path should point at is
pip, venv, and
keeping projects apart
, what to write into that log is
what to record so the
next failure is diagnosable
, and the modules doing the work are
what ships
with Python, and saves you a dependency
, all inside
readable, everywhere, and the usual
first language
.

Alongside this

Above this