# Migrate a Homegrown Python Test Script to OpenHTF
URL: /guides/migrate-from-custom-scripts
Turn an existing Python test script — a main() with prints, ifs and a CSV writer — into an OpenHTF test in five refactoring steps, keeping behaviour identical while gaining measurements with limits, plugs, a JSON record and an operator UI.
Every hardware team has one: a `test.py` that grew from a bring-up script. It opens the instruments at the top, runs checks in order with `if value < limit: print("FAIL")`, and appends a row to a CSV. It works, and nobody wants to touch it. This guide converts such a script incrementally; each step leaves a working test.
Problems this script has and does not know it has: the PSU stays on if `dmm.query` raises; limits live in `if` statements no one else can read; the CSV has no units, no limits, no timestamps, no per-phase timing; a failed serial-number scan still writes a row.
Keep everything, move it into a phase, let OpenHTF run it. Behaviour is unchanged; you gain a test record and the console banner.
The module-level `dmm` and `psu` become plugs. `tearDown` fixes the "PSU left on" bug for free.
Addresses hard-coded here move to configuration in a later step; see the PyVISA guide.
One phase per logical step. Each is short, named, timed and individually reported.
`FAIL_AND_CONTINUE` reproduces the old "record the failure, keep going" behaviour.
Now the values, limits and units enter the record. The `if` statements disappear; OpenHTF computes the outcome.
Run with `ConsoleSummary` and a failure prints the value and the violated limit — see Output Callbacks.
A phase group makes `power_off` run after any failure in `main`. The CSV writer becomes `OutputToJSON` (one file per unit with everything) — and, if you want the CSV for an existing consumer, a ten-line custom callback keeps it.
Same instruments, same limits, same CSV — plus: the PSU always turns off; each phase is timed; every value has its limits and unit in a JSON record; a failed scan produces no bogus row; adding the Operator UI is three lines; sending records to a database is one callback.
Move VISA addresses and limits into a YAML file per station — Configuration.
Add a checkpoint before the slow phases.
Replace `time.sleep` polling with a monitor where a value must be watched.
Wrap the loop in `while True:` with the Operator UI so the station runs unit after unit.