Python Quest

Track 15 · Lesson 1 of 11

Thinking in automation

Decide what is worth automating, describe its inputs and outputs, and build scripts that plan before they act.

About 35 minBeginner3 exercisesFree

You will be able to

  • Judge whether a repetitive task is a good candidate for automation
  • Describe an automation as inputs, steps and outputs before writing code
  • Separate planning from acting so a script can run in dry-run mode
  • Refuse to overwrite or destroy files without an explicit check

Every Monday, Hana in the accounts team of a small trading company downloads about forty scanned invoices. The scanner names them Invoice March 03.PDF, invoice march 04.pdf, INVOICE-March-05.pdf. She renames each one by hand to a consistent lower-case form so the archive sorts properly. It takes twenty minutes, she does it 50 times a year, and every few weeks she makes a typo that hides an invoice.

That is a perfect job for Python. But a script that renames forty files in a second can also ruin forty files in a second. This track is about automation you can trust: scripts that show you what they are about to do, never destroy data by accident, and can be run again safely.

What is worth automating?

A task is a good candidate when most of these are true:

QuestionGood sign
How often does it happen?Daily, weekly or monthly
Are the rules clear?You could write them as a checklist
Is it error-prone by hand?Copying, retyping, renaming, comparing
Are the inputs predictable?Same folder, same export format
What if it goes wrong?Mistakes are visible and recoverable

A task that needs judgement each time ("decide which customers deserve a discount") is a poor candidate. A task with clear rules ("rename every scan to lower case with underscores") is a good one.

Before writing code, describe the automation in three parts:

  • Inputs: what it reads (a folder of scans, a CSV export, a config file).
  • Steps: the rules, in order (clean each name, skip names already clean, rename).
  • Outputs: what changes or gets produced (renamed files, a report, a log of what happened).

Writing these down usually reveals the awkward questions early: what if two scans clean to the same name? What if the folder is empty?

Which task is the best candidate for automation?

Safe automation principles

These habits appear in every lesson of this track:

  1. Plan, then act. First build a list of the changes the script would make. Show it. Only apply it when asked. Running with only the plan is called a dry run.
  2. Never destroy originals. Prefer copying to moving, and moving to deleting. Never delete without an explicit confirmation.
  3. Check before you overwrite. If the target file already exists, stop or pick a new name.
  4. Record what happened. Print or log every change so you can explain (and undo) it later.
  5. Make re-runs safe. Running the script twice should not do the work twice or damage anything. This property is called idempotence.
  6. Fail loudly. If something unexpected turns up, stop with a clear message rather than guessing.
Plan first: build the list of renames without touching any file
def clean_name(name):
    return name.strip().lower().replace(" ", "_")

scans = ["Invoice March 03.PDF", "invoice_march_04.pdf", "Receipt Fuel.png"]

plan = []
for name in scans:
    new = clean_name(name)
    if new != name:
        plan.append((name, new))

for old, new in plan:
    print(f"WOULD RENAME {old} -> {new}")
print(len(plan), "of", len(scans), "files need renaming")

Try it: Add the name " Credit Note 7.pdf" (with leading spaces) to the list and run it. Which part of clean_name handles the spaces?

Nothing on disk changed: the program only built a plan, a list of (old, new) pairs. Notice that invoice_march_04.pdf is already clean, so it is left out of the plan. That matters later: when you run the script a second time, the plan is empty and nothing happens. That is idempotence in its simplest form.

Keeping the plan as data has another benefit: you can test the planning logic with plain lists, without creating any files.

A dry-run switch

The usual shape is one function that applies a plan, with a dry_run parameter that defaults to True. Forgetting the argument should give you the safe behaviour, not the destructive one.

Predict the output

def apply(plan, dry_run=True):
    done = []
    for old, new in plan:
        if dry_run:
            print("WOULD RENAME", old, "->", new)
        else:
            done.append(new)
            print("RENAMED", old, "->", new)
    return done

plan = [("A.txt", "a.txt")]
print(apply(plan))
print(apply(plan, dry_run=False))

Confirm before anything destructive

Some jobs really do need to delete files, such as clearing out temporary exports. Make the script ask first, and treat anything other than an exact "yes" as no:

Ask for confirmation; the default answer is no
old_exports = ["export_0101.tmp", "export_0102.tmp", "export_0103.tmp"]

print("These files would be deleted:")
for name in old_exports:
    print("  ", name)

answer = input("Type yes to delete them: ")
if answer.strip().lower() == "yes":
    print("Deleting", len(old_exports), "files")
else:
    print("Nothing deleted.")
Input given to the program: "no"

Try it: Change the input to Yes (with a capital Y) and run it again. Then try y. Which answers count as confirmation?

Guided practice · ●●difficulty 2 of 5 · about 6 min

Plan the renames

Write plan_renames(names) that takes a list of file names and returns the plan: a list of (old, new) tuples for the names that need renaming.

The clean form of a name is: surrounding spaces removed, all lower case, and every remaining space replaced by an underscore. Names that are already clean must not appear in the plan. Keep the original order.

plan_renames(["Q1 Report.PDF", "budget.xlsx", "Team Photo.jpg"])
# [("Q1 Report.PDF", "q1_report.pdf"), ("Team Photo.jpg", "team_photo.jpg")]
plan_renames([])   # []

Do not touch any files: this function only plans.

Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.

Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type

Output

Run your code to see its output, or Check to test it against the requirements.

The solution becomes available after you pass, or after all three hints and two checks.

Challenge · ●●●difficulty 3 of 5 · about 10 min

Apply a plan with a dry-run switch

The working folder contains a scans/ folder with Invoice March.pdf and Receipt 01.png.

Write apply_plan(plan, folder, dry_run=True). For each (old, new) pair in plan:

  • in a dry run, rename nothing and add the message "WOULD RENAME old -> new";
  • otherwise rename folder/old to folder/new and add "RENAMED old -> new".

Return the list of messages. dry_run must default to True, so a call without it never changes anything.

plan = [("Invoice March.pdf", "invoice_march.pdf")]
apply_plan(plan, "scans")
# ["WOULD RENAME Invoice March.pdf -> invoice_march.pdf"]   (nothing renamed)
apply_plan(plan, "scans", dry_run=False)
# ["RENAMED Invoice March.pdf -> invoice_march.pdf"]

Use pathlib.Path ((Path(folder) / old).rename(...)). Do not call the function at the top level of your program.

Files in your working folder: scans/Invoice March.pdfscans/Receipt 01.png

scans/Invoice March.pdf

scanned invoice for March

scans/Receipt 01.png

scanned receipt 01

Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.

Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type

Output

Run your code to see its output, or Check to test it against the requirements.

The solution becomes available after you pass, or after all three hints and two checks.

Edge cases · ●●●difficulty 3 of 5 · about 8 min

Never overwrite on rename

On Linux and macOS, renaming a file onto an existing name silently replaces the existing file. (On Windows, Path.rename raises FileExistsError instead, so the same script behaves differently on different computers. Checking first gives one predictable behaviour everywhere.) Write rename_safely(folder, old, new) that renames folder/old to folder/new only when it is safe, and returns a status string:

SituationReturnFiles
old and new are the same name"same"unchanged
folder/old does not exist"missing"unchanged
folder/new already exists"exists"unchanged
otherwise"renamed"renamed

Check the situations in that order.

rename_safely("docs", "draft.txt", "final.txt")   # "renamed"
rename_safely("docs", "draft.txt", "final.txt")   # "missing"  (it was already renamed)
Files in your working folder: docs/draft.txt

docs/draft.txt

Quarterly plan, draft 3

Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.

Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type

Output

Run your code to see its output, or Check to test it against the requirements.

The solution becomes available after you pass, or after all three hints and two checks.

Summary

  • Automate tasks that are frequent, rule-based and error-prone by hand; describe them as inputs, steps and outputs first.
  • Build a plan (a list of changes) before acting, and offer a dry run that reports the plan without changing anything.
  • Make dry_run=True the default so the destructive path has to be requested.
  • Check before overwriting, ask before deleting, and return or record a status for every action.
  • Skip work that is already done so the script can be re-run safely.