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) raisesCalledProcessError 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 andp.rename replaced string surgery on paths withos.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.