New: Boardroom MCP Engine!

Ready to put this into action?

Get the complete AI Integration Playbook β€” Practical AI implementation guide β€” prompt engineering, workflow automation, and ROI frameworks.

Article 106 Β· Part 11

Build Your First Useful Script

Turn a repeated chore into a small program with a preview, explicit boundaries, and a recovery path.

By Randy Salars Β· Published

On this page
  1. Write the contract before requesting code
  2. Use a complete, inspectable implementation
  3. Preview before creating output
  4. Understand the boundaries that make the script manageable
  5. Read the record of what happened
  6. Check more than the happy path
  7. Ask AI to explain the next change
  8. A reusable prompt
  9. For students: automate a chore you can still explain manually
  10. Practice: make a reviewed copy

Turn a repeated chore into a small program with a preview, explicit boundaries, and a recovery path.

A useful first script solves a problem you can describe precisely. It might clean a list, summarize a small table, or prepare copies of files with consistent names. The result should be easy to check by hand before you trust the program with more work.

For this lesson, imagine a folder containing draft notes named Field Notes.TXT and Map.MD. You want a new folder containing field-notes.txt and map.md, with the original contents preserved. The source files should remain where they are.

That narrow task contains several lessons that transfer to much larger systems. You must define allowed inputs, detect conflicting outputs, separate planning from action, and decide what to do when only part of the work succeeds.

Write the contract before requesting code

Our script accepts one input directory and one output directory. It copies ordinary .txt and .md files, converts names to lowercase, replaces spaces in the filename stem with hyphens, and preserves each file's bytes. It supports a deliberately small set of ASCII output names.

The output directory must be new and outside the input directory. Each input file may contain at most one mebibyte, and the folder may contain at most 100 entries. Subdirectories, symbolic links, unsupported names, and conflicting output names stop the operation.

These are teaching choices. A different project may need Unicode filenames, nested directories, or large files. Those capabilities require explicit design decisions; silently accepting everything would make the first script harder to reason about.

The default command produces a preview. Copying requires the exact identifier of a reviewed plan. The program calculates that identifier from the source and destination paths, proposed names, file sizes, and content hashes. If any of those planned details change before the apply command, the old identifier will no longer match.

Use a complete, inspectable implementation

Save the following as organize.py. It needs Python 3.9 or later and no third-party packages. Python's path objects provide the directory, file, and exclusive-creation operations used here; consult the official pathlib reference when adapting those operations.

import argparse
import hashlib
import json
from pathlib import Path
import re


def plan(source, destination):
    source = Path(source)
    if source.is_symlink() or not source.is_dir():
        raise ValueError("Source must be a real directory, not a symlink")
    source = source.resolve()
    destination = Path(destination)
    if destination.exists() or destination.is_symlink():
        raise ValueError("Destination must not already exist")
    destination = destination.resolve()
    if destination.is_relative_to(source):
        raise ValueError("Destination must be outside the source")
    if not destination.parent.is_dir():
        raise ValueError("Destination parent must already exist")
    paths = sorted(source.iterdir())
    if len(paths) > 100:
        raise ValueError("This lab accepts at most 100 files")
    entries, contents, names = [], {}, set()
    for path in paths:
        if path.is_symlink() or not path.is_file():
            raise ValueError("Only ordinary files are supported")
        name = path.stem.strip().lower().replace(" ", "-") + path.suffix.lower()
        if not re.fullmatch(r"[a-z0-9][a-z0-9_-]*\.(txt|md)", name):
            raise ValueError("Unsupported filename or extension")
        if name.casefold() in names:
            raise ValueError("Two source files map to the same destination")
        names.add(name.casefold())
        with path.open("rb") as handle:
            data = handle.read(1048577)
        if len(data) > 1048576:
            raise ValueError("Each file must be at most 1 MiB")
        digest = hashlib.sha256(data).hexdigest()
        entries.append({"source_name": path.name, "destination_name": name,
                        "bytes": len(data), "sha256": digest})
        contents[name] = data
    record = {"source": str(source), "destination": str(destination), "entries": entries}
    encoded = json.dumps(record, sort_keys=True).encode("utf-8")
    return record, hashlib.sha256(encoded).hexdigest(), contents


def apply(record, plan_id, contents):
    destination = Path(record["destination"])
    destination.mkdir()  # Exclusive creation: existing output is rejected.
    journal = destination / "manifest.jsonl"
    with journal.open("x", encoding="utf-8") as log:
        log.write(json.dumps({"event": "planned", "plan_id": plan_id, **record}) + "\n")
        log.flush()
        try:
            for entry in record["entries"]:
                name = entry["destination_name"]
                with (destination / name).open("xb") as output:
                    output.write(contents[name])
                log.write(json.dumps({"event": "copied", **entry}) + "\n")
                log.flush()
            log.write(json.dumps({"event": "complete"}) + "\n")
        except OSError as exc:
            log.write(json.dumps({"event": "failed", "error_type": type(exc).__name__}) + "\n")
            raise


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source")
    parser.add_argument("destination")
    parser.add_argument("--approve", help="Exact plan ID printed by a reviewed preview")
    args = parser.parse_args()
    try:
        record, plan_id, contents = plan(args.source, args.destination)
        print(json.dumps({"plan_id": plan_id, **record}, indent=2))
        if args.approve is None:
            print("Preview only; no output directory created")
        elif args.approve != plan_id:
            raise ValueError("Plan changed or approval ID does not match")
        elif not record["entries"]:
            print("Empty input; nothing written")
        else:
            apply(record, plan_id, contents)
            print("Copy complete; source files unchanged")
    except (ValueError, OSError) as exc:
        raise SystemExit(f"Stopped: {exc}") from None


if __name__ == "__main__":
    main()

Preview before creating output

Create a new folder named sample-input beside the script. Put two small, disposable text files inside it: Field Notes.TXT containing Walk route, and Map.MD containing North trail. Do not create organized-output yet.

Run this command from the directory containing the script:

python3 organize.py sample-input organized-output

On systems where the interpreter is named python, use that name instead of python3. The output is a JSON plan followed by a preview-only message. Read the absolute source and destination paths first. A correct filename transformation is irrelevant if the program is pointing at the wrong folder.

Next, inspect the proposed names and sizes. The two destination names should be field-notes.txt and map.md. The reported byte counts depend on whether your editor added a final newline. That variation is expected; the hash represents the actual bytes in your files.

To apply the reviewed plan, repeat the command with --approve followed by the exact plan_id printed in your preview. For example, the command begins python3 organize.py sample-input organized-output --approve; append your own printed identifier after a space. An identifier from someone else's example will not work because the paths and content are part of the plan.

The script prints the freshly computed plan again before comparing identifiers. If it reports a mismatch, inspect the new plan. Do not automate acceptance of whatever identifier appears next: that would remove the human review the exercise is intended to teach.

Understand the boundaries that make the script manageable

The program reads validated contents into memory before writing. Its limits keep that choice reasonable for a small exercise: at most roughly 100 MiB of file content, plus overhead. Streaming larger files would require a different design for ensuring the reviewed content is the content later copied.

The naming rule is intentionally restrictive. Field Notes.TXT becomes field-notes.txt. A PDF stops the entire plan. Two names that normalize to the same destination also stop the plan before an output directory is created. On a filesystem that distinguishes uppercase and lowercase filenames, Map.md and map.md provide an easy collision example.

The script never overwrites an existing output directory. Individual files also use exclusive creation. These checks turn a potentially destructive naming mistake into a visible failure.

This is a single-user learning script for a folder that is not changing concurrently. It is not hardened against a hostile process swapping paths while it runs. A content hash supports change detection; it is not an authorization system or a defense against every filesystem race.

Read the record of what happened

The output directory includes manifest.jsonl, a text file containing one JSON record per line. Its first record describes the plan. Later records identify files copied successfully. A final complete record indicates that the normal copy loop finished.

If an ordinary file-write error occurs during copying, the program attempts to add a failed record and stops. A partially created output directory remains available for inspection. The unchanged source folder remains the basis for a fresh attempt.

There are limits to this journal. A power failure may interrupt both copying and logging. If opening the journal itself fails, an empty output directory may remain. Flushing a Python file buffer is not a complete durability guarantee. Therefore, never infer that a missing event proves the corresponding filesystem action could not have started.

For this exercise, recovery is deliberately straightforward: inspect the partial output, keep the source files, fix the cause, and preview a new destination directory. Avoid adding automatic deletion or an in-place resume feature until you can specify the behavior of every partial state.

Check more than the happy path

During preparation, the local example passed checks for preview without output creation, correct copies and names, unchanged sources, collision rejection, refusal to reuse an existing destination, unsupported files, and empty input. An injected failure during the second copy left the first copy and a failure record; a new destination then completed from the same unchanged sources.

Those results support the stated learning scenario. They do not establish compatibility with every operating system, network drive, or concurrent process.

You can organize your own checks around observable outcomes:

ScenarioExpected resultEvidence to inspect
Preview two valid filesNo output directoryDirectory listing and displayed plan
Apply reviewed planTwo copies plus journalNames, contents, and final event
Input changes after previewOld approval rejectedNew plan identifier and no new output
Two names collidePlanning stopsError and absent output directory
Destination already existsOperation stopsExisting files remain unchanged
Copy fails partwayPartial output remainsJournal, copied file, intact source

A success message is useful, but compare contents too. For larger examples, independently computed hashes can help. For two tiny notes, opening both source and destination files is easy and informative.

Ask AI to explain the next change

Suppose you want to add .csv support. The extension check is only one question. Will the script preserve CSV bytes without reading the format? Yes, in this design. Will changing filenames affect another program that expects the old name? Possibly. Should a file containing sensitive records enter the learning folder? That is a separate input decision.

Ask the assistant to describe the behavioral change, affected checks, and any new failure modes before editing. Keep the change small enough that its diff can be reviewed in one sitting.

A reusable prompt

Help me build a script for [specific repeated task]. Define accepted inputs, exact outputs, and rejection conditions first. Default to a preview. Use disposable inputs and a new output location. Include complete code, explain every filesystem write, and show how the script handles name collisions, changed inputs, and partial failure. State which checks were actually executed and which remain proposed.

For students: automate a chore you can still explain manually

Good student projects include preparing copies of public-domain reading notes, standardizing synthetic lab filenames, or converting a small teacher-provided dataset. Use material you are allowed to process, and follow the assignment's AI rules.

Describe the manual process first. Then show how each rule became a validation or transformation step. A small script with clear rejection behavior demonstrates more understanding than a large script whose side effects you cannot identify.

Do not use the exercise on the only copy of a dissertation folder, a class submission directory, or shared research data. Create a tiny representative sample and demonstrate recovery before proposing broader use.

Practice: make a reviewed copy

Run the preview and apply sequence on two disposable files. Record the source and destination names, inspect the journal, and confirm that source contents remain unchanged. Then use a fresh destination name for a second preview, edit one input file, and attempt approval with the old identifier. Explain the rejection.

Completion check: You can identify every location the script writes, show that preview creates no output, demonstrate one rejected plan, and recover from a partial output using the intact source files.

Stretch: Propose support for nested folders. Before coding, define how relative paths, duplicate names, depth limits, symbolic links, and partial recovery would work. The design is the deliverable until those choices are explicit.

Get the AI Dispatch

Weekly insights on ai & technology β€” delivered to your inbox. No spam, unsubscribe any time.

Want to choose specific topics? Customize your interests