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 Point | LabVIEW | Python |
|---|---|---|
| License cost | $3,160-4,840/seat/year (depending on edition) | Free |
| Deployment | Windows only | Linux, macOS, Windows |
| Version control | Binary .vi files, merge conflicts | Text files, Git-native |
| Hiring | LabVIEW developers are scarce and expensive | Python developers are everywhere |
| CI/CD | Difficult to integrate | Native (pytest, GitHub Actions, etc.) |
| Package ecosystem | NI packages + limited community | pip, PyPI, 400K+ packages |
| Code review | Requires LabVIEW to view | Any text editor |
| Collaboration | One person per VI at a time | Standard Git workflow |
LabVIEW to Python Concept Map
| LabVIEW Concept | Python Equivalent | Notes |
|---|---|---|
| VI (Virtual Instrument) | Python function | Same idea: reusable, callable unit |
| SubVI | Function or class method | Import from a module |
| Front panel | Operator UI declared in the procedure | Or the TofuPilot dashboard for monitoring |
| Block diagram | Python code | Text instead of wires |
| Connector pane | Function signature | def measure_voltage(channel: int) -> float |
| Error cluster | Exception handling | try/except instead of error wires |
| Typedef | Class or dataclass | @dataclass for structured data |
| Global variable | Module-level variable or class attribute | Avoid when possible |
| Property node | Property decorator | @property on a class |
| State machine | The procedure sequence | Declared, not hand-rolled |
| TDMS file | TofuPilot | Replaces file-based logging |
| DAQmx driver | PyVISA + pyvisa-py | Or the nidaqmx Python package |
| Instrument driver | Plug | A plain Python class |
| Test sequence | Procedure .yaml | Or htf.Test(...) in OpenHTF |
| Parallel sequence execution | execution.slots | Multi-slot fixtures |
Step 1: Set Up Your Python Environment
python -m venv venvsource venv/bin/activate # Linux/macOS# venv\Scripts\activate # Windowspip install pyvisa pyvisa-pyThen install the TofuPilot CLI, which runs procedures and streams results:
curl -fsSL https://www.tofupilot.app/install | shtofupilot 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:
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.
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-raiseThis 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.py22 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()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.yaml31 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.2Each 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:
execution: slots: - name: Slot 1 - name: Slot 2 - name: Slot 3 - name: Slot 4Each 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.py57 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
| Situation | Choice |
|---|---|
| You tested several UUTs at once in LabVIEW | TofuPilot Framework (multi-slot) |
| Operators used the LabVIEW front panel heavily | TofuPilot Framework (declarative UI) |
| You want the test plan readable by non-programmers | TofuPilot Framework (YAML procedure) |
| You want one pure-Python paradigm, no YAML | OpenHTF |
| You already have OpenHTF elsewhere in the plant | OpenHTF, 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 test | Automatic upload |
| Build custom analytics tools | Dashboard with FPY, Cpk, control charts |
| Manual data aggregation across stations | Automatic multi-station aggregation |
| File server for TDMS storage | Cloud storage with API access |
| Custom report generation | Built-in reports and exports |
tofupilot run ./procedure.yaml --uploadMigration Checklist
| Step | Action | LabVIEW Equivalent |
|---|---|---|
| 1 | Install Python + venv, then the TofuPilot CLI | Install LabVIEW |
| 2 | pip install pyvisa pyvisa-py | Install NI packages |
| 3 | Write a plug class per instrument | Create instrument driver VIs |
| 4 | Write phase functions, one per test step | Create the test sequence |
| 5 | Declare measurements and limits in procedure.yaml | Configure Numeric Limit Tests |
| 6 | tofupilot run ./procedure.yaml | Run the VI |
| 7 | Compare results against LabVIEW on the same units | Validate measurement values |
| 8 | Link the procedure and add --upload | Configure TDMS logging |
Common Gotchas
| Gotcha | Fix |
|---|---|
| "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. |

