Python

Verified against ChatGPT · 2026-08-11

Write a one-off Python cleanup script that won't need a second one to undo it

Produces a one-off data cleanup or migration script with a mandatory dry-run mode and idempotency built in, for the kind of throwaway script that quietly becomes load-bearing the moment it touches real data.

ChatGPT4 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

Write a one-off Python script for a specific data cleanup job. Even though this is a "throwaway" script, it's going to run against real data exactly once (or a few times if something goes wrong the first time), so it needs a dry-run mode and idempotency, not just a happy-path implementation.

WHAT NEEDS TO HAPPEN
Find all user records where email is stored in mixed case and normalize them to lowercase, merging any resulting duplicate accounts by keeping the older one.

DATA SOURCE
PostgreSQL production database, connecting via a read-write service account with a 30-second statement timeout.

HOW MANY RECORDS, ROUGHLY
About 900,000 user rows total, expecting roughly 12,000 to actually need changes.

WHAT COUNTS AS SUCCESS
Every email is lowercase, no two active accounts share the same normalized email, and the older account wins any merge.

RULES
The script must support a `--dry-run` flag that runs the exact same selection and transformation logic as the real run and prints/logs what it would change, without writing anything — implement this by having both modes call the same function to decide what changes, with only the final write step gated behind the flag, so the dry-run output can never drift from what the real run would actually do. Make every write idempotent: if the script is run twice against the same data (because it was killed halfway through, or someone re-runs it out of caution), running it again should produce the same end state rather than double-applying a change or erroring out. If the record volume is large enough that holding everything in memory at once is questionable, process in batches with progress output, and make sure a crash partway through a batch doesn't leave that batch in a half-applied state. Log every record that gets modified (its identifier and what changed) to a file, not just a summary count, so there's a concrete audit trail if something needs to be manually reversed later.

WHAT NOT TO DO
Do not write this as an inline script with no functions — structure it as at least a `find_candidates()`, `apply_change(record)`, and `main()` so dry-run and real-run can share logic. Do not catch a broad exception around the whole batch loop just to keep it running past errors; catch specific expected exceptions per-record, log them, and let genuinely unexpected exceptions stop the script rather than silently skip records.

OUTPUT FORMAT
1. The full script.
2. One paragraph on how to verify the dry-run output before running for real.
3. One paragraph on what the audit log lets someone manually reverse if needed.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

Requiring dry-run and real-run to share the same selection/transformation function and differ only at the final write step closes the most common failure mode in hand-written cleanup scripts: when dry-run logic is written as a separate code path (a duplicated function, or an if/else branching much earlier than the write itself), it drifts from the real path the very first time someone edits one branch without the other, and the dry-run output stops being a trustworthy preview of what will actually happen — which defeats the entire point of having one. Making every write idempotent matters specifically because the stated context is a one-off script touching production data exactly once or twice, and the realistic failure scenario isn't a clean single run, it's the script getting killed by a timeout or a network blip partway through 900,000 rows and someone needing to just run it again — without idempotency, a naive re-run either double-merges accounts that already got merged or throws unhandled uniqueness errors on rows already processed, and the fix under production pressure is worse than the original bug. The per-record audit log requirement, rather than a summary count, is what actually makes a cleanup reversible: a count of "12,003 records updated" gives no way to identify which twelve thousand or what their prior values were, whereas a log of record-id-to-before/after gives someone a concrete list to manually or programmatically reverse if the success criteria turn out to have been mis-specified after the fact. Catching only specific expected exceptions per record, instead of wrapping the whole loop in a broad except, prevents the script from silently skipping records it doesn't know how to handle and reporting false success.

What you get back

def find_candidates(conn): ... def apply_change(record, dry_run): logger.info(f'{record.id}: {record.email} -> {record.email.lower()}'); if not dry_run: cursor.execute(...). Running with --dry-run against the 900k rows logged 12,014 planned changes to cleanup_audit.log with old/new email pairs; review that file for any unexpected merges before re-running without the flag.

Verified against

ChatGPT GPT-5.1 · 2026-08-11

Changelog

  • 2026-08-11 Initial publish, verified against ChatGPT GPT-5.1.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All Python prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY