Everything needed to drive a bench instrument from Python: backends, resource strings, the query and write distinction, binary transfers, error checking, and the traps that waste a first afternoon.

PyVISA is a Python package for controlling measurement instruments over USB, LAN, GPIB, and serial. It wraps the VISA standard so that one API drives a Keysight multimeter, a Tektronix oscilloscope, and a Rigol power supply without you caring which cable they are on.
This tutorial covers installation, finding instruments, resource strings, the write and query distinction, binary waveform transfer, and the six traps that reliably waste an engineer's first afternoon.
pip install pyvisa pyvisa-pypyvisa is the API. The second package is a backend, and you need exactly one backend. Three choices:
| Backend | Install | Covers | Notes |
|---|---|---|---|
pyvisa-py | pip only | LAN, serial, some USB | No installer, easiest start, limited GPIB |
| NI-VISA | Vendor installer | Everything | Solid, large install, already present with NI stacks |
| Keysight IO Libraries | Vendor installer | Everything | Solid, good USB and GPIB, includes Connection Expert |
Start with pyvisa-py over LAN. If a USB instrument will not enumerate or you need GPIB, install a vendor backend. See Keysight IO Libraries.
import pyvisa
rm = pyvisa.ResourceManager()
print(rm.list_resources())That prints resource strings. To see what each one is, ask it:
import pyvisa
rm = pyvisa.ResourceManager()
for resource in rm.list_resources():
try:
inst = rm.open_resource(resource)
inst.timeout = 2000
print(f"{resource}\n {inst.query('*IDN?').strip()}")
inst.close()
except Exception as exc:
print(f"{resource}\n no response: {exc}")*IDN? is universal. Every SCPI instrument answers it with manufacturer, model, serial number, and firmware version. If an instrument does not answer *IDN?, nothing else in this tutorial will work on it and the problem is the connection, not your code.
LAN instruments often do not appear in `list_resources()`. Discovery relies on mDNS or a configured list. Just address them directly:
scope = rm.open_resource("TCPIP0::192.168.1.50::inst0::INSTR")TCPIP0::192.168.1.42::inst0::INSTR
USB0::0x2A8D::0x1301::MY57200001::INSTR
GPIB0::22::INSTR
ASRL3::INSTR| Part | Meaning |
|---|---|
TCPIP0, USB0, GPIB0, ASRL3 | Interface type and board number |
192.168.1.42 | IP address for LAN |
0x2A8D | USB vendor ID (Keysight here) |
0x1301 | USB product ID |
MY57200001 | Serial number |
inst0, 22 | LAN device name or GPIB primary address |
INSTR | Resource class, almost always this |
Common vendor IDs: 0x2A8D Keysight, 0x0957 older Agilent, 0x0699 Tektronix, 0x1AB1 Rigol, 0xF4EC Siglent.
The single most important distinction in PyVISA.
inst.write("VOLT 3.3") # command, no response expected
value = inst.query("MEAS:VOLT?") # command + read response
raw = inst.read() # read only, when a response is pendingRule: if the SCPI string ends in `?`, use `query`. Otherwise use `write`.
Getting this wrong is the number one cause of mysterious timeouts. inst.write("MEAS:VOLT?") sends the query, the instrument puts a response in its output buffer, and nobody reads it. Your next query then reads that stale response and everything is off by one from that point on.
import pyvisa
rm = pyvisa.ResourceManager()
dmm = rm.open_resource("USB0::0x2A8D::0x1301::MY57200001::INSTR")
dmm.timeout = 10000 # milliseconds
try:
print(dmm.query("*IDN?").strip())
dmm.write("*RST")
dmm.write("*CLS")
dmm.write("CONF:VOLT:DC 10,0.0001") # 10 V range, 100 uV resolution
dmm.write("TRIG:SOUR IMM")
for _ in range(5):
print(f"{float(dmm.query('READ?')):.6f} V")
finally:
dmm.close()
rm.close()*RST puts the instrument in a known state. *CLS clears the status and error registers. Doing both at the start of every script eliminates an entire category of "it worked yesterday" problems caused by leftover configuration.
SCPI instruments do not raise exceptions. Send a command with a typo and the instrument records an error and carries on. Your script produces numbers that look fine and are wrong.
def check(inst):
problems = []
while True:
response = inst.query("SYST:ERR?").strip()
code = int(response.split(",", 1)[0])
if code == 0:
break
problems.append(response)
if len(problems) > 20:
break
if problems:
raise RuntimeError("instrument errors: " + "; ".join(problems))Call it after every configuration block. This function finds more real bugs than any other twelve lines you will write.
For anything larger than a few hundred points, ASCII transfer is slow enough to dominate your run time. Use binary.
scope.write("DATA:SOURCE CH1")
scope.write("DATA:ENC RIBINARY")
scope.write("DATA:WIDTH 2")
ymult = float(scope.query("WFMOUTPRE:YMULT?"))
yoff = float(scope.query("WFMOUTPRE:YOFF?"))
yzero = float(scope.query("WFMOUTPRE:YZERO?"))
xincr = float(scope.query("WFMOUTPRE:XINCR?"))
raw = scope.query_binary_values("CURVE?", datatype="h", is_big_endian=True)
volts = [(point - yoff) * ymult + yzero for point in raw]
times = [i * xincr for i in range(len(volts))]datatype follows Python's struct codes: "b" signed byte, "h" signed short, "f" float. It must match the DATA:WIDTH you set, and endianness must match the encoding. Get either wrong and you receive a plausible-looking waveform that is complete nonsense.
The scaling step is not optional. CURVE? returns digitiser levels, not volts.
1. Timeout too short. The default is 2000 ms. A DMM doing a high-resolution integration, or a scope doing a long acquisition, takes longer. Set inst.timeout generously, 10000 ms or more for slow operations.
2. Missing termination character. Serial and some LAN instruments need an explicit terminator.
inst.read_termination = "\n"
inst.write_termination = "\n"Symptom is a hang on the first query.
3. Querying a write-only command. inst.query("VOLT 3.3") sends a command that produces no response, then waits for one. Guaranteed timeout.
4. Not waiting for slow operations. Some commands return immediately but take time to complete. Use *OPC?, which blocks until the instrument finishes.
scope.write("AUTOSET EXECUTE")
scope.query("*OPC?") # blocks until autoset completesUse it deliberately rather than everywhere, because it costs a round trip.
5. Assuming SCPI is portable. It is standardised at the top of the tree and vendor-specific below it. *IDN?, *RST, and *CLS are universal. MEASUREMENT:IMMED:VALUE? is Tektronix and means nothing to Keysight. Always use the instrument's own programming guide.
6. Leaving instruments in a live state on error. Wrap in try/finally and disable outputs in the finally. A crashed script that leaves a supply at 12 V into a device rated for 3.3 V is an expensive bug.
Once past the first script, wrap each instrument in a class.
class Dmm:
def __init__(self, resource: str, timeout_ms: int = 10000):
self.inst = pyvisa.ResourceManager().open_resource(resource)
self.inst.timeout = timeout_ms
self.inst.write("*RST"); self.inst.write("*CLS")
def dc_volts(self, dc_range: float = 10, resolution: float = 1e-4) -> float:
self.inst.write(f"CONF:VOLT:DC {dc_range},{resolution}")
return float(self.inst.query("READ?"))
def __enter__(self): return self
def __exit__(self, *exc): self.inst.close()Now the test reads as the test rather than as SCPI:
with Dmm("USB0::0x2A8D::0x1301::MY57200001::INSTR") as dmm:
assert 3.2 <= dmm.dc_volts() <= 3.4This is the point at which instrument code stops being a script and starts being maintainable. Eight methods per instrument is usually enough.
Real tests use more than one instrument, and the pattern that scales is one ResourceManager and a dictionary keyed by role.
import pyvisa
ADDRESSES = {
"psu": "TCPIP0::192.168.1.42::inst0::INSTR",
"dmm": "USB0::0x2A8D::0x1301::MY57200001::INSTR",
"load": "GPIB0::8::INSTR",
}
rm = pyvisa.ResourceManager()
bench = {}
for role, address in ADDRESSES.items():
inst = rm.open_resource(address)
inst.timeout = 10000
inst.write("*RST"); inst.write("*CLS")
print(f"{role:6s} {inst.query('*IDN?').strip()}")
bench[role] = instTwo things this buys you. The addresses live in one place, so moving an instrument from USB to LAN is a one-line change. And printing every *IDN? at startup gives you a record of exactly which serial numbers produced the run, which matters when a result is questioned months later.
Close them properly in a finally, and disable anything that sources power first:
finally:
bench["psu"].write("OUTP OFF")
bench["load"].write("INP OFF")
for inst in bench.values():
inst.close()
rm.close()A single session timeout is a compromise: too short for a slow integration, too long to notice a hung instrument quickly. Set it per operation instead.
from contextlib import contextmanager
@contextmanager
def timeout(inst, ms: int):
previous = inst.timeout
inst.timeout = ms
try:
yield inst
finally:
inst.timeout = previous
dmm.timeout = 2000 # quick default, catches problems fast
with timeout(dmm, 60000): # this one operation is genuinely slow
dmm.write("CAL:ALL?")
result = dmm.read()A short default is the useful part. With a 30-second blanket timeout, a wrong command wastes 30 seconds per occurrence and hides in a long run. With 2 seconds, it fails fast and obviously.
The single most confusing part of pyVISA for newcomers. pyVISA is a wrapper; it does not talk to instruments itself. Underneath sits a backend, and which one you get is decided by a search order you did not choose.
| Backend | Install | Covers | Use it when |
|---|---|---|---|
| NI-VISA | NI driver package | USB, LAN, GPIB, serial | GPIB is on the bench |
| Keysight IO Libraries | Keysight download | USB, LAN, GPIB, serial | Keysight-heavy bench |
pyvisa-py | pip install pyvisa-py | LAN, USB, serial. GPIB partial | No vendor software allowed or wanted |
import pyvisa
rm = pyvisa.ResourceManager() # whichever backend is found first
rm = pyvisa.ResourceManager("@py") # explicitly pyvisa-py
rm = pyvisa.ResourceManager("/Library/Frameworks/VISA.framework/VISA") # explicit path
print(rm.visalib) # print what you actually gotAlways print rm.visalib when a script works on one machine and not another. Nine times out of ten the two machines resolved different backends, and the tenth is a firewall.
pyvisa-py is the one worth knowing: pure Python, pip-installable, no vendor driver, and it covers LAN and USB completely. For a LAN-only bench it removes the entire vendor-driver install from your setup, which makes the whole stack reproducible from a requirements.txt. See GPIB vs USB vs LAN for whether you need more than that.
The trap that produces "resource busy" errors an hour into a debugging session. A VISA session held by a crashed script can block the next one, and on some backends it survives the interpreter exiting.
import pyvisa
from contextlib import closing
rm = pyvisa.ResourceManager()
with closing(rm.open_resource("TCPIP0::192.168.1.42::inst0::INSTR")) as inst:
inst.timeout = 10000
print(inst.query("*IDN?"))
# session closed even if the block raisedThree habits: use a context manager or a finally, close the ResourceManager at the end of a long-running process, and if you do hit a stuck session, power-cycle the instrument's interface rather than hunting for the process. On LAN instruments, most have a "close all connections" option in the web interface, which is faster than either.
rm.visalib. See above.read_termination and write_termination differ by instrument and by transport. If reads hang, this is the first thing to check.PyVISA solves the transport problem. What it does not solve is knowing which SCPI commands a given instrument needs for a given measurement, which is where most of the time goes.
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.
A Python package that talks to measurement instruments over USB, LAN, GPIB, and serial using the VISA standard. It is a wrapper over a VISA backend, giving you a uniform Python interface regardless of the physical interface.
You need a VISA backend, and NI-VISA is one option. Keysight IO Libraries Suite is another, and pyvisa-py is a pure-Python backend that needs no installer. pyvisa-py covers LAN and serial well but has narrower GPIB and USB support.
The address that identifies an instrument, for example TCPIP0::192.168.1.42::inst0::INSTR for a LAN instrument or USB0::0x2A8D::0x1301::MY57200001::INSTR for USB. It encodes the interface, the address, and the resource class.
write sends a command and expects no response. read waits for a response. query is write followed by read, and is what you use for any command ending in a question mark. Using write on a query leaves the response in the buffer and corrupts your next read.
Usually because you queried a command the instrument does not support, or wrote a query without reading it, leaving the buffer out of step. Check SYST:ERR? and confirm the command against the instrument's programming guide, not a generic SCPI reference.
Yes. PyVISA is open source under an MIT licence, and pyvisa-py is free. NI-VISA and Keysight IO Libraries Suite are also free downloads, though they are proprietary.
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.