Migrating from Legacy Systems

Migrate from LabVIEW to Python

Replace LabVIEW with Python for manufacturing test: concept mappings, code examples, and a migration path to the TofuPilot Framework or OpenHTF.

JJulien Buteau
intermediate14 min readMarch 14, 2026
LabVIEW voltage measurement, limit checking, and result recording mapped to Python code above a PCB test fixture and digital multimeter.
Validate one test before a wider rollout.
Existing test
Connect results
Compare one run
Roll out

LabVIEW is powerful, but it comes with $3,000-5,000/seat licensing, Windows-only deployment, and binary files that don't version-control well. Python gives you the same instrument control capabilities with zero license cost, cross-platform support, and native Git integration. This guide maps LabVIEW concepts to Python equivalents and shows you how to rebuild your test system with the TofuPilot Framework or OpenHTF.

Why Teams Migrate

Pain PointLabVIEWPython
License cost$3,160-4,840/seat/year (depending on edition)Free
DeploymentWindows onlyLinux, macOS, Windows
Version controlBinary .vi files, merge conflictsText files, Git-native
HiringLabVIEW developers are scarce and expensivePython developers are everywhere
CI/CDDifficult to integrateNative (pytest, GitHub Actions, etc.)
Package ecosystemNI packages + limited communitypip, PyPI, 400K+ packages
Code reviewRequires LabVIEW to viewAny text editor
CollaborationOne person per VI at a timeStandard Git workflow

LabVIEW to Python Concept Map

LabVIEW ConceptPython EquivalentNotes
VI (Virtual Instrument)Python functionSame idea: reusable, callable unit
SubVIFunction or class methodImport from a module
Front panelOperator UI declared in the procedureOr the TofuPilot dashboard for monitoring
Block diagramPython codeText instead of wires
Connector paneFunction signaturedef measure_voltage(channel: int) -> float
Error clusterException handlingtry/except instead of error wires
TypedefClass or dataclass@dataclass for structured data
Global variableModule-level variable or class attributeAvoid when possible
Property nodeProperty decorator@property on a class
State machineThe procedure sequenceDeclared, not hand-rolled
TDMS fileTofuPilotReplaces file-based logging
DAQmx driverPyVISA + pyvisa-pyOr the nidaqmx Python package
Instrument driverPlugA plain Python class
Test sequenceProcedure .yamlOr htf.Test(...) in OpenHTF
Parallel sequence executionexecution.slotsMulti-slot fixtures

Step 1: Set Up Your Python Environment

migration/setup.sh
python -m venv venvsource venv/bin/activate    # Linux/macOS# venv\Scripts\activate     # Windowspip install pyvisa pyvisa-py

Then install the TofuPilot CLI, which runs procedures and streams results:

migration/install_cli.sh
curl -fsSL https://www.tofupilot.app/install | sh

tofupilot run ./procedure.yaml executes a procedure on your machine without an account, so you can validate a migrated test before connecting anything.

Step 2: Convert SubVIs to Python Functions

A LabVIEW SubVI that reads voltage from a DMM becomes a Python function:

migration/instrument_functions.py
def measure_voltage(channel: int) -> float:    """Equivalent of a LabVIEW SubVI that reads voltage from a DMM.    In LabVIEW: SubVI with channel input (I32) and voltage output (DBL).    In Python: function with type hints.    """    readings = {1: 3.31, 2: 5.02, 3: 1.81}  # Replace with real reads    return readings.get(channel, 0.0)# Call it like you'd wire a SubVIrail_3v3 = measure_voltage(1)rail_5v0 = measure_voltage(2)

The function signature replaces the connector pane. Type hints (int, float) replace the LabVIEW data types.

Step 3: Replace the Error Cluster

LabVIEW uses an error cluster (status, code, source) wired through every VI. Python uses exceptions.

migration/error_handling.py
class InstrumentError(Exception):    """Replaces LabVIEW error cluster for instrument errors."""    def __init__(self, code: int, source: str, message: str):        self.code = code        self.source = source        super().__init__(f"[{code}] {source}: {message}")# LabVIEW: check error cluster at every node# Python: try/except catches errors from anywhere in the blocktry:    voltage = measure_voltage(1)    if voltage < 0:        raise InstrumentError(1001, "DMM", "Negative voltage reading")except InstrumentError as e:    print(f"Instrument error: {e}")    # Handle or re-raise

This is cleaner than wiring error clusters through every node. Exceptions propagate automatically until caught.

Step 4: Stop Building the Sequencer

LabVIEW state machines use a while loop plus a case structure, and most teams hand-roll a sequencer on top. You do not need to port that. Both Python hardware frameworks provide sequencing, limits and pass/fail evaluation.

The TofuPilot Framework

The procedure YAML replaces the state machine: it declares the order, the measurements and the limits. Phases are plain Python functions, and instrument drivers become plugs.

plugs/dmm.py
22 lines
class Multimeter:    """Replaces the LabVIEW instrument driver VIs.    __init__ = opening a VISA session in LabVIEW    methods  = the SubVIs for each measurement    close    = closing the session    """    def __init__(self):        self._voltages = iter([3.31, 5.02])        # Replace with:        # import pyvisa        # rm = pyvisa.ResourceManager("@py")        # self.instr = rm.open_resource("TCPIP::192.168.1.100::INSTR")    def read_voltage(self) -> float:        return next(self._voltages)        # Replace with: return float(self.instr.query(":MEAS:VOLT:DC?"))    def close(self):        pass        # Replace with: self.instr.close()
phases/power_rails.py
def power_rails(measurements, dmm):    """Replaces a LabVIEW sequence of Numeric Limit Tests."""    measurements.rail_3v3 = dmm.read_voltage()    measurements.rail_5v0 = dmm.read_voltage()
procedure.yaml
31 lines
name: PCBA Functional Testversion: 1.0.0unit:  serial_number:    default_value: "PCBA000001"  part_number:    default_value: "PCBA-100"plugs:  - name: dmm    python: plugs.dmm:Multimetermain:  - 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 5V0        unit: V        validators:          - operator: ">="            expected_value: 4.8          - operator: "<="            expected_value: 5.2

Each validators block is the direct equivalent of a LabVIEW Numeric Limit Test, stored as data rather than buried in wires. The plug is injected into the phase by parameter name.

Two things migrating teams usually want, which the framework provides without extra work:

  • The front panel. Operator inputs, checklists, images, sliders and progress bars are declared in the procedure, so you do not rebuild a UI.
  • Parallel sequence execution. If your LabVIEW system tested several UUTs at once, declare fixture slots and the engine runs them concurrently:
procedure.yaml
execution:  slots:    - name: Slot 1    - name: Slot 2    - name: Slot 3    - name: Slot 4

Each slot uploads its own run, with its own unit and outcome.

OpenHTF

If you prefer everything in Python with no separate YAML layer, OpenHTF attaches measurements and plugs to phase functions with decorators.

migration/openhtf_test.py
57 lines
import openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import uploadclass InstrumentPlug(BasePlug):    """Replaces LabVIEW instrument driver VIs.    setUp = equivalent of opening a VISA session in LabVIEW    methods = equivalent of SubVIs for each measurement    tearDown = equivalent of closing the session    """    def setUp(self):        self._voltages = iter([3.31, 5.02])        # Replace with: rm = pyvisa.ResourceManager("@py")        # self.instr = rm.open_resource("TCPIP::192.168.1.100::INSTR")    def read_voltage(self) -> float:        return next(self._voltages)        # Replace with: return float(self.instr.query(":MEAS:VOLT:DC?"))    def tearDown(self):        pass        # Replace with: self.instr.close()@htf.measures(    htf.Measurement("rail_3v3")    .in_range(3.2, 3.4)    .with_units(units.VOLT)    .doc("3.3V rail"),    htf.Measurement("rail_5v0")    .in_range(4.8, 5.2)    .with_units(units.VOLT)    .doc("5.0V rail"),)@htf.plug(instr=InstrumentPlug)def test_power_rails(test, instr):    """Replaces a LabVIEW test sequence with Numeric Limit Tests."""    test.measurements.rail_3v3 = instr.read_voltage()    test.measurements.rail_5v0 = instr.read_voltage()def main():    test = htf.Test(        test_power_rails,        procedure_id="FCT-001",        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    test.execute(test_start=lambda: input("Scan serial number: "))if __name__ == "__main__":    main()

Install with pip install "tofupilot[openhtf]", or skip the callback and run the suite with tofupilot run.

Which one

SituationChoice
You tested several UUTs at once in LabVIEWTofuPilot Framework (multi-slot)
Operators used the LabVIEW front panel heavilyTofuPilot Framework (declarative UI)
You want the test plan readable by non-programmersTofuPilot Framework (YAML procedure)
You want one pure-Python paradigm, no YAMLOpenHTF
You already have OpenHTF elsewhere in the plantOpenHTF, for consistency

Both run natively on the TofuPilot CLI and upload to the same dashboard.

Step 5: Replace TDMS with TofuPilot

LabVIEW writes test data to TDMS files. You then need to build your own analytics tools to read them. TofuPilot replaces this entire pipeline.

LabVIEW (TDMS)TofuPilot
Write TDMS file after each testAutomatic upload
Build custom analytics toolsDashboard with FPY, Cpk, control charts
Manual data aggregation across stationsAutomatic multi-station aggregation
File server for TDMS storageCloud storage with API access
Custom report generationBuilt-in reports and exports
migration/run_and_upload.sh
tofupilot run ./procedure.yaml --upload

Migration Checklist

StepActionLabVIEW Equivalent
1Install Python + venv, then the TofuPilot CLIInstall LabVIEW
2pip install pyvisa pyvisa-pyInstall NI packages
3Write a plug class per instrumentCreate instrument driver VIs
4Write phase functions, one per test stepCreate the test sequence
5Declare measurements and limits in procedure.yamlConfigure Numeric Limit Tests
6tofupilot run ./procedure.yamlRun the VI
7Compare results against LabVIEW on the same unitsValidate measurement values
8Link the procedure and add --uploadConfigure TDMS logging

Common Gotchas

GotchaFix
"I miss the front panel"Declare operator UI components in the procedure: text inputs, checklists, images, sliders, progress. No frontend code.
"My NI hardware needs NI drivers"Use the nidaqmx Python package for DAQmx hardware. For GPIB, install NI-VISA runtime.
"LabVIEW handles parallelism automatically"Declare independent phases and fixture slots; the engine schedules them. Use depends_on where order matters.
"My team doesn't know Python"Python has a gentler learning curve than LabVIEW. Most engineers learn it in days, not weeks.
"We have years of LabVIEW code"Migrate incrementally. Start with new tests in Python. Convert existing tests one at a time, validating each against LabVIEW results.
"One instrument, several parallel phases"Give each phase its own instrument, or serialize the sharing phases with depends_on.

More Guides

Put this guide into practice