Migrating from Legacy Systems

Reskill a LabVIEW Developer to Python

Learn how a LabVIEW developer moves to Python in six weeks, with a concept map, a study plan, plug and phase code and the stumbles to expect.

JJulien Buteau
intermediate11 min readSeptember 23, 2026

A LabVIEW developer already has the hard part of a Python test engineer's job: they know the instruments, the fixtures, the limits and what a marginal DUT looks like on the bench. Only the language changes, and it's the smaller of the two things to learn. Six weeks of part-time study with the TofuPilot Framework turns a driver VI into a plug, a state machine into a procedure file and a TDMS folder into a dashboard.

Why This Works

The scarce part of a test engineer is domain knowledge, not syntax. Your LabVIEW developer knows which pogo pin lifts on fixture 3, why the 5 V rail reads high for the first 200 ms, and which SCPI query the old DMM answers with a trailing newline. None of that is in a certification exam and all of it transfers.

What doesn't transfer is the G-specific layer: the sequencer they built, the front panel, the error wire, the report VIs. On the TofuPilot Framework those four things are provided. The sequencer is procedure.yaml, limits are validators in the same file, the operator UI is declared, and results land in TofuPilot. The developer has to learn Python, and only Python.

The six weeks below assume a working LabVIEW developer with a few hours a week and no prior Python. A CLD with a computer science degree will finish in three.

For the manager, this is the cheapest option on the table. The developer keeps their salary and their fixture knowledge, the plant keeps the person who knows why station 3 needs a 200 ms settle, and nobody spends a quarter recruiting into a pool that skews senior and expensive. The cost is a few hours a week for six weeks and one bench instrument they can break without consequences.

Concept Map

Every LabVIEW concept has a Python or framework equivalent. Most of them are simpler.

LabVIEW / TestStandPython + TofuPilot FrameworkWhat changes
VIFunctionA def with parameters. No front panel, no connector pane
SubVIModuleA .py file you import. Reuse is import plugs.dmm
Front panelDeclared operator UIText inputs, checklists, images and progress bars are declared in procedure.yaml
Error clusterExceptionsraise instead of wiring. The framework records the error and fails the phase
State machineProcedure sequenceThe main: list in procedure.yaml, with depends_on for fan-out
Typedef clusterDataclass or dict@dataclass for structure, dict for anything JSON-shaped
TDMS fileTofuPilot runMeasurements upload. FPY, Cpk and histograms are already charts
DAQmxnidaqmxSame driver, Python binding
NI-VISA driver VIPyVISA plugA class with __init__, methods and close()
TestStand sequenceprocedure.yamlSteps become phases, limits become validators
Project (.lvproj)Git repositoryText files, diffable, reviewable

The LabVIEW migration guide walks the first nine rows in detail. If the plant runs TestStand on top, the TestStand migration guide covers the sequence file and the callbacks.

Two LabVIEW habits have no row because they have no equivalent. Highlight execution and probes become log.info() calls and a terminal, which feels like a loss for a week and then doesn't. The run arrow becomes a command. Both are covered in the stumbles table below.

A Six-Week Self-Study Path

Two to four hours a week, on a real instrument from the bench, not a tutorial. Every exercise produces a file the developer keeps, and by week 3 those files are the start of a real station.

WeekGoalExerciseDone when
1Python basics on the benchInstall Python, pip install pyvisa pyvisa-py, send *IDN? to the bench DMM from a scriptThe DMM answers from a .py file
2A plug instead of a driver VIWrap the DMM in a class with read_voltage() and close()The class replaces one driver VI on one station
3A phase with limitsWrite procedure.yaml with one phase and two validators, run it with tofupilot runPass and fail both show correctly in the terminal
4Sequence and operator UIAdd three phases, depends_on, and a checklist for the operatorThe sequence matches the old state machine step for step
5A real templateClone a TofuPilot template, run it with mock plugs, swap one plug for the bench instrumentThe template runs on real hardware
6Upload and read resultstofupilot login, tofupilot link, run with --upload, read Cpk in the dashboardCpk for one measurement is visible without a spreadsheet

Each row builds on the previous one. By week 4 the developer has a station folder they could hand to anyone on the team.

Pair them with whoever on the team writes Python, even a firmware engineer, for thirty minutes a week. The questions in weeks 1 and 2 are about the language and take a minute to answer in person and an hour to answer alone. From week 3 on, the questions are about the framework, and the Python docs plus tofupilot run cover most of them.

Weeks 1 to 2: A Plug Instead of a Driver VI

A LabVIEW driver VI is a front panel, a VISA session wire and an error cluster. In Python it's a class. The framework calls __init__ at setup, injects the instance into any phase that names it and calls close() at teardown. The PyVISA docs cover the resource manager and the @py backend.

plugs/dmm.py
# Replaces "DMM Read Voltage.vi": one VISA session, a mux channel, one query, one closeimport pyvisaclass Multimeter:    def __init__(self, address="TCPIP::192.168.1.100::INSTR"):        rm = pyvisa.ResourceManager("@py")        self.inst = rm.open_resource(address)        self.inst.timeout = 5000        self.inst.write("*RST")    def read_voltage(self, channel):        self.inst.write(f"ROUT:CLOS (@{channel})")        return float(self.inst.query(":MEAS:VOLT:DC?"))    def close(self):        self.inst.close()

The address comes from procedure.yaml through config, so the same plug serves three stations with three DMMs. That's the SubVI reuse a LabVIEW developer already expects, without the connector pane. The float() is the week 2 lesson: everything VISA returns is a string, and the conversion belongs in the plug.

Weeks 3 to 4: A Phase and Its Limits

In LabVIEW the limit lives in a comparison node next to the measurement, or in a limits table the front panel loads. In the framework it lives in procedure.yaml, and the phase only assigns the value.

procedure.yaml
34 lines
# Two rails with limits; the plug address is config, not codename: Controller Board FCTversion: 1.0.0unit:  serial_number:    default_value: "SN-0001"  part_number:    default_value: "CTRL-100"plugs:  - name: dmm    python: plugs.dmm:Multimeter    config:      address: "TCPIP::192.168.1.100::INSTR"main:  - name: Power Rails    python: phases.power_rails    measurements:      - name: rail_3v3        unit: V        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4      - name: rail_5v        unit: V        validators:          - operator: ">="            expected_value: 4.8          - operator: "<="            expected_value: 5.2
phases/power_rails.py
# No comparison node: the validators in procedure.yaml decide pass or faildef power_rails(measurements, dmm, log):    rail_3v3 = dmm.read_voltage(101)    rail_5v = dmm.read_voltage(102)    log.info(f"3V3 {rail_3v3:.3f} V, 5V {rail_5v:.3f} V")    measurements.rail_3v3 = rail_3v3    measurements.rail_5v = rail_5v

Run it with tofupilot run ./procedure.yaml. Then tighten one validator until it fails and run again. Seeing both outcomes in the terminal is the week 3 exit test.

Week 4 adds the rest of the old state machine. Fan-out after power-on is depends_on, and the front panel checklist becomes a declared component with no code behind it.

procedure.yaml
23 lines
# Week 4: fan-out after power-on, plus an operator checklist with no Python behind itmain:  - name: Power Rails    key: power_rails    python: phases.power_rails  - name: Firmware Handshake    key: firmware    python: phases.firmware    depends_on: [power_rails]  - name: Visual Check    key: visual    depends_on: [power_rails]    ui:      components:        - key: visual_checks          type: checklist          label: "Visual inspection"          required: true          options:            - label: "Status LED lit"              value: "led"            - label: "No solder bridges on J3"              value: "bridges"

Weeks 5 to 6: Run a Template, Upload, Read Cpk

Week 5 starts from a working station instead of a blank file. The IMU thermal calibration template is a good fit for a calibration engineer; the FCT fixture template for a PCBA line.

week5.sh
# A complete station with mock instruments, running in minutescurl -fsSL https://www.tofupilot.app/install | shgit clone https://github.com/tofupilot/template-framework-imu-thermal-calibrationcd template-framework-imu-thermal-calibrationtofupilot run ./procedure.yaml

Read procedure.yaml top to bottom, then swap one mock plug for the week 2 class. The phases don't change. That's the moment most LabVIEW developers stop worrying about the language.

Week 6 connects the station to the dashboard and runs twenty units.

week6.sh
# Link the folder to a procedure in the dashboard, then upload every runtofupilot logintofupilot linktofupilot run ./procedure.yaml --upload

Open the procedure in TofuPilot and find the Cpk and histogram for one measurement. Don't compute Cpk in Python. The dashboard already does it, and that's the point of week 6: the developer watches their measurements become the plant's yield numbers without writing a report VI.

The exit test for week 6 is a hand-over. Give the station folder to a colleague who has never seen it, and have them run twenty units with --upload using only the README. If they manage it, the developer has learned the part of Python that matters for a test station, and the station has stopped being a single point of failure. If they don't, the gap is usually a missing dependency in pyproject.toml or an instrument address hard-coded in a plug instead of config.

Common Stumbles

StumbleWhat the developer seesFix
IndentationIndentationError on a line that looks fineSpaces only, four per level. Set the editor to show whitespace
Exceptions vs the error wireA failing plug call stops the phase with a tracebackThat's the error wire. Catch with try and except only where you'd have handled the cluster, otherwise let it propagate
No run arrow"How do I run this?"tofupilot run ./procedure.yaml for the station, python -c for a plug on its own
Virtual environmentsModuleNotFoundError: pyvisa right after installing itThe framework builds a venv from pyproject.toml. Add the dependency there, not with a global pip install
Implicit parallelismExpects two unwired nodes to run at oncePython runs top to bottom. Use depends_on in procedure.yaml for fan-out
Everything is a stringTypeError when comparing a VISA reply to a limitConvert with float() at the plug boundary, never in the phase
Mutable referencesA list passed to a function changes outside itPython passes references. Copy with list(x) where you'd have branched a wire

The stumble that isn't in the table is reading. A LabVIEW developer is used to seeing the whole sequence as a diagram, and a folder of .py files looks like less information at first. It's the same information in a different shape, and it comes with something the diagram never had: a diff. After a month of git log on their own station, most developers stop asking for the picture.

The full list of what a Python test engineer should be able to do is in the Python Skills Checklist for Test Engineers. Use it as the week 6 exit interview.

Start With One Station

Pick the station with the highest volume, or the one only one person can open. Rebuild it in Python with the TofuPilot Framework and run it side by side with the LabVIEW version on the same units for two weeks. Compare the two result sets in TofuPilot before you retire anything.

install-and-run.sh
curl -fsSL https://www.tofupilot.app/install | shtofupilot run ./procedure.yaml

The step-by-step is in How to Migrate from LabVIEW to Python, and the framework is at https://tofupilot.com/products/framework. The Lab tier is free, and tofupilot run works with no account.

More Guides

Put this guide into practice