A concrete, ordered migration plan from LabVIEW to Python, with the library equivalents for the VIs you actually use, real code, and the four traps that stall most migrations halfway.

Migrating from LabVIEW to Python is a sequencing problem, not a language problem. The libraries exist and are mature: pyVISA for SCPI instruments, nidaqmx for NI DAQ hardware, numpy and pandas for analysis. What stalls migrations is doing them in the wrong order and losing the ability to check the new results against the old.
This guide gives the ordered plan, the library equivalents for the VIs you actually use, real code, and the four traps that leave teams running both stacks for two years.
Be precise about the boundary. Three things genuinely belong in LabVIEW:
Everything else, and for most teams that is 80 to 95 percent of the VIs, moves cleanly.
| LabVIEW | Python | Notes |
|---|---|---|
| VISA Read/Write/Open | pyvisa | Same underlying NI-VISA or Keysight IO layer |
| DAQmx VIs | nidaqmx | NI's own package, keeps your hardware |
| Waveform Chart / Graph | matplotlib, plotly | Static and interactive respectively |
| Array and signal analysis | numpy, scipy.signal | scipy is broader than LabVIEW's base analysis |
| Write to Measurement File | pandas.to_csv, nptdms | npTDMS reads and writes TDMS |
| Report Generation Toolkit | reportlab, WeasyPrint, python-docx | |
| Front panel | PyQt, Dash, Streamlit | Or drop it, see below |
| State machine / queued message | plain classes, enum, asyncio | |
| TCP/UDP VIs | socket, requests | |
| Serial (VISA) | pyserial or pyvisa | |
| Database Connectivity Toolkit | sqlalchemy, psycopg | |
| Call Library Function Node | ctypes, cffi | Same DLLs, called from Python |
Before writing any Python, make the LabVIEW application log every measurement it takes to CSV with a timestamp, if it does not already. You need a reference dataset.
This is the single highest-value hour in the whole migration. Without it, "does the new code agree with the old code" is unanswerable and every subsequent step becomes a matter of opinion.
This is mechanical. For each instrument, the LabVIEW VI is wrapping a SCPI string. Find the string, write the Python.
import pyvisa
class PowerSupply:
def __init__(self, resource: str):
self.rm = pyvisa.ResourceManager()
self.inst = self.rm.open_resource(resource)
self.inst.timeout = 5000
idn = self.inst.query("*IDN?").strip()
print(f"connected: {idn}")
def set_voltage(self, volts: float, current_limit: float = 1.0):
self.inst.write(f"VOLT {volts}")
self.inst.write(f"CURR {current_limit}")
def output(self, on: bool):
self.inst.write(f"OUTP {'ON' if on else 'OFF'}")
def measure_current(self) -> float:
return float(self.inst.query("MEAS:CURR?"))
def close(self):
self.output(False)
self.inst.close()Write one class per instrument, matching the operations your tests actually use. Do not try to wrap the whole SCPI command set. You need maybe eight methods per instrument.
Checkpoint: run each class standalone against the real instrument and confirm it returns sane values before going further.
Pick the simplest test in your suite. Port it end to end. Run it on the same DUT the LabVIEW version ran on, and diff the CSVs.
import pandas as pd
old = pd.read_csv("labview_run.csv")
new = pd.read_csv("python_run.csv")
delta = (new["VOUT"] - old["VOUT"]).abs()
print(f"max delta {delta.max():.6f} V, resolution 0.0001 V")
assert delta.max() < 0.0005, "results disagree beyond measurement resolution"Agreement to the measurement's own resolution is the pass criterion. Not "looks about right".
During migration you will need to call LabVIEW from Python or the reverse. Both work.
LabVIEW VI from Python: build the VI into a DLL with the Application Builder, then ctypes.CDLL("mytest.dll").
Python from LabVIEW: the Python Node, available in LabVIEW 2018 and later, calls a Python function directly. This is the easier direction and lets you migrate test by test inside the existing application shell.
Use the Python Node approach if your application has a big front panel you are not ready to replace. You get to move the logic without touching the UI.
Most test applications do not need one. The front panel exists because LabVIEW makes it free, not because operators use it.
Ask what the panel is actually for:
Batch by instrument family, not by test order. All the DMM tests, then all the scope tests. The instrument layer is already done, so each batch goes faster than the last.
Keep one LabVIEW seat until a full quarter of production has run on Python. Then drop the licences at renewal. See LabVIEW pricing for what that saves.
1. Rewriting instead of porting. The temptation is to fix the architecture while you migrate. Do not. Port the behaviour exactly, verify it matches, then refactor as a separate change with tests in place. Teams that combine the two cannot tell whether a discrepancy is a bug or an improvement.
2. Losing the reference. Repurposing the LabVIEW machine before the migration is complete. This is the one that turns a three-month migration into a year. The old system is your test oracle.
3. Timing loops written in Python. A while loop with time.sleep() is not how you acquire at 10 kHz. Use nidaqmx hardware timing, exactly as LabVIEW does under the hood.
task.timing.cfg_samp_clk_timing(
rate=10000,
sample_mode=nidaqmx.constants.AcquisitionType.CONTINUOUS,
samps_per_chan=10000,
)4. Underestimating the undocumented. There is always a magic delay, a retry, or a settling time that someone added in 2014 to fix an intermittent failure and never wrote down. You find these by diffing results, which is why step 1 matters.
Every migration hits at least one VI that works, matters, and cannot be read. A practical procedure:
Treat it as a black box and characterise it. Feed it a range of known inputs through a test harness and record the outputs. You now have a specification derived from behaviour rather than from reading the diagram.
Reimplement against that specification, not against the block diagram.
Compare across the full input range, including the edges and the invalid cases. Undocumented VIs frequently have special handling at boundaries, and that handling is often the reason they exist.
If it cannot be characterised, build it into a DLL and call it from Python with ctypes for now. Do not block the migration on one VI.
Point 4 is the escape hatch that keeps migrations moving. A single incomprehensible VI should never hold up the other ninety.
For a single validation station with six instruments and around forty tests:
| Phase | Work | Elapsed |
|---|---|---|
| 1 | Add CSV logging to the LabVIEW app, collect reference data | 1 day |
| 2 | Write instrument classes, verify each against hardware | 3 to 5 days |
| 3 | Port the simplest test, diff against reference | 1 day |
| 4 | Port remaining tests in batches of ten | 2 to 3 weeks |
| 5 | Replace or retire the front panel | 1 to 5 days |
| 6 | Parallel running, both stacks, same DUTs | 2 to 4 weeks |
| 7 | Cut over, keep LabVIEW seat until renewal |
Six to ten weeks elapsed, of which perhaps three are hands-on. The parallel running phase is mostly waiting, and it is not optional.
Scale phase 4 by test count. Everything else stays roughly constant, which is why migrating a second station takes a fraction of the time of the first.
The failure mode after a successful migration is a directory of forty scripts that only their authors can run. The structure below is boring on purpose, and it is what separates a migration that lasts from one that gets rewritten again in three years.
validation/
instruments/ # one module per instrument class, not per model
dmm.py # open, configure, read, close
psu.py
scope.py
measurements/ # the maths. Pure functions, no I/O, easy to test
regulation.py
efficiency.py
sequences/ # one file per test, calls instruments + measurements
line_regulation.py
thermal_soak.py
limits/
pmic_rev_c.yaml # limits as data, never hardcoded in the sequence
reports/
template.html
tests/ # runs with no hardware present
test_regulation.pyThree rules that carry most of the value:
Limits live in data, not code. A new silicon revision is a new YAML file, not a code change and not a rebuild. This is the single biggest maintainability win over the typical LabVIEW front panel with numbers typed into constants.
Measurement maths is pure functions. No instrument handles, no file writes. That makes it unit-testable with no hardware, which is the first time most validation maths has ever had a test.
One module per instrument class, not per model. A DMM is a DMM. Keep the model-specific SCPI differences inside the module and the sequences stay readable when the bench changes.
The objection to code is "I cannot run it without the instruments". You can, and it is the capability LabVIEW never gave you.
# tests/test_regulation.py
from measurements.regulation import load_regulation_pct
def test_load_regulation_typical():
assert load_regulation_pct(v_noload=3.310, v_fullload=3.288) == 0.665
def test_load_regulation_rejects_zero():
import pytest
with pytest.raises(ValueError):
load_regulation_pct(v_noload=0.0, v_fullload=3.288)For the instrument layer, a fake that returns canned responses lets the whole sequence run on a laptop:
class FakeDMM:
def __init__(self, readings): self._r = iter(readings)
def configure(self, *a, **k): pass
def read(self): return next(self._r)
def test_sequence_flags_out_of_spec():
dmm = FakeDMM([3.31, 3.29, 3.55]) # third point out of limits
result = run_line_regulation(dmm, limits=(3.20, 3.40))
assert result.verdict == "FAIL"
assert result.first_failure_index == 2This is what makes the migration pay off beyond the licence. A sequence that can be tested without hardware can be reviewed, changed confidently, and run in CI. See test sequencer comparison for how this fits alongside a formal sequencer, and automated test report generation for the output stage.
*OPC? where it is supported. Fixed sleeps are either too short and wrong or too long and slow.Most LabVIEW migrations stall in the middle, with half the tests ported and both stacks running. Generating the mechanical half is how you get through the middle quickly.
Connect your instruments. Pick the manufacturer and model, paste the VISA address (USB, LAN, GPIB, or serial), and the agent knows what is on your bench. No bench yet? Use a placeholder address, build the full automation, and swap in the real address when you are in the lab.
Tell the agent what to test, in plain English. For example, "run a VI sweep from 1 to 10 V in 1 V steps at 0.5 A load current," or "suggest the tests for a power-management device."
The agent builds the complete workflow in seconds. Instrument-aware automation appears on the canvas, with the generated scripts visible in a code panel you can inspect and edit.
Run it in your lab. Click Run and the status panel streams results step by step, with measured values inline (VOUT = 3.301 V, asserted 3.2 to 3.4 V, PASS). One click exports a structured PDF report, or the raw results as CSV.


The step-by-step walkthrough, VISA address formats, and Test Planner prompts are all in the TestFlow product guide.
For instrument control, data acquisition, sequencing, and reporting, yes. For hard real-time execution on an NI real-time target, and for FPGA work through LabVIEW FPGA, no. Scope your migration around that boundary.
Yes. The nidaqmx package is NI's own Python API and talks to the NI-DAQmx driver directly. You keep your DAQ hardware and drop the LabVIEW licence.
For a single test station with a handful of instruments, days. For a production test system with hundreds of sequences and custom hardware, quarters. The variable is not code volume, it is how much undocumented behaviour is buried in the VIs.
For operator UIs, PyQt or Dash. For engineering dashboards, Streamlit or Grafana. For most test applications the honest answer is that the front panel was not needed and a log plus a report replaces it.
Yes. Build the VI into a DLL with the LabVIEW Application Builder and call it with ctypes, or use the LabVIEW Python Node in the other direction. This lets both stacks run during a phased migration.
For SCPI instrument control the bottleneck is the instrument and the bus, not the language, so Python is equivalent. For tight acquisition loops use nidaqmx hardware timing rather than a Python loop, exactly as you would in LabVIEW.
Connect your instruments, describe a test in plain English, and TestFlow builds and runs it in minutes.
A new way for testing, from specs to automated sequences, capture clean data, and accelerate your validation cycle.