Back to blog
Ali KamalyAli Kamaly
August 14, 2026
13 min read
Instrument Automation

PyVISA Tutorial in 2026 (Control Any Lab Instrument from Python)

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 Tutorial in 2026 (Control Any Lab Instrument from Python)

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.

Install

pip install pyvisa pyvisa-py

pyvisa is the API. The second package is a backend, and you need exactly one backend. Three choices:

BackendInstallCoversNotes
pyvisa-pypip onlyLAN, serial, some USBNo installer, easiest start, limited GPIB
NI-VISAVendor installerEverythingSolid, large install, already present with NI stacks
Keysight IO LibrariesVendor installerEverythingSolid, 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.

Find your instrument

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")

Resource strings decoded

TCPIP0::192.168.1.42::inst0::INSTR
USB0::0x2A8D::0x1301::MY57200001::INSTR
GPIB0::22::INSTR
ASRL3::INSTR
PartMeaning
TCPIP0, USB0, GPIB0, ASRL3Interface type and board number
192.168.1.42IP address for LAN
0x2A8DUSB vendor ID (Keysight here)
0x1301USB product ID
MY57200001Serial number
inst0, 22LAN device name or GPIB primary address
INSTRResource class, almost always this

Common vendor IDs: 0x2A8D Keysight, 0x0957 older Agilent, 0x0699 Tektronix, 0x1AB1 Rigol, 0xF4EC Siglent.

Write, read, query

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 pending

Rule: 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.

A complete session

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.

Always check the error queue

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.

Binary waveform transfer

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.

The six traps

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 completes

Use 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.

Making it reusable

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.4

This is the point at which instrument code stops being a script and starts being maintainable. Eight methods per instrument is usually enough.

Talking to several instruments at once

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] = inst

Two 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()

Timeouts per operation, not per session

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.

Backends, and why the same script behaves differently on two machines

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.

BackendInstallCoversUse it when
NI-VISANI driver packageUSB, LAN, GPIB, serialGPIB is on the bench
Keysight IO LibrariesKeysight downloadUSB, LAN, GPIB, serialKeysight-heavy bench
pyvisa-pypip install pyvisa-pyLAN, USB, serial. GPIB partialNo 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 got

Always 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.

Closing sessions properly

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 raised

Three 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.

Common mistakes

  • Not knowing which backend you have. Print rm.visalib. See above.
  • Leaving the default timeout. It is often 2 seconds, which is far too short for a long acquisition and far too long for a discovery scan. Set it per operation.
  • Using `read()` after `write()` instead of `query()`. Same thing, more ways to get the ordering wrong.
  • Ignoring the termination character. read_termination and write_termination differ by instrument and by transport. If reads hang, this is the first thing to check.
  • Not checking `SYST:ERR?`. SCPI instruments do not raise. They queue the error and carry on.
  • Hard-coding resource strings. They change when a USB port or an IP changes. Use hostnames on LAN, and discover on USB.
  • Opening a new `ResourceManager` per instrument. One manager, several resources.
  • Assuming `query_binary_values` defaults match your instrument. Datatype, endianness, and container all need to match what you configured on the instrument.

Where TestFlow fits

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.

  1. 1

    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.

  2. 2

    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."

  3. 3

    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.

  4. 4

    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 TestFlow builder: a plain-English request on the left, the generated instrument workflow in the centre, and the live run with its streaming SCPI execution log on the right.
The TestFlow agent turning a plain-English request into a runnable workflow, then running it on the bench. Click to enlarge.
  • Vendor-neutral by design. One workflow drives Keysight, Tektronix, Rohde & Schwarz, NI, Rigol, Keithley, Anritsu, and more over standard VISA and SCPI.
  • Browser-based and shareable. Workflows live in your workspace, so a sequence built in one lab runs the same way in another.
  • Free version to start. Sign in at app.testflowinc.com and build your first workflow today; plans and quotes are on the pricing page.
Instrument vendors TestFlow drives over VISA and SCPI: Keysight, Tektronix, Rohde & Schwarz, NI, Keithley, Agilent, Anritsu, Siglent, Chroma, Fluke, Yokogawa, Kikusui, TDK-Lambda, ESPEC, Watlow, Pickering, Copper Mountain, inTEST, Thermonics, and Microchip
Works with the instruments already on your bench. Full list on the supported instruments page.

The step-by-step walkthrough, VISA address formats, and Test Planner prompts are all in the TestFlow product guide.

Frequently asked questions

What is PyVISA?

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.

Do I need NI-VISA to use PyVISA?

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.

What is a VISA resource string?

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.

What is the difference between write, read, and query in PyVISA?

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.

Why does my PyVISA script time out?

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.

Is PyVISA free?

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.

Ready to automate your lab?

Connect your instruments, describe a test in plain English, and TestFlow builds and runs it in minutes.

Tags

pyvisa tutorialpyvisapython instrument controlpyvisa scpicontrol instruments with pythonpyvisa example
Share this article:
Ali Kamaly

Article by

Ali Kamaly

Ali Kamaly is the Co-Founder and CEO of TestFlow, an AI-native platform for electronics test automation. He writes about test automation, lab validation, and the infrastructure behind modern hardware engineering.

Put it on your bench this week

A new way for testing, from specs to automated sequences, capture clean data, and accelerate your validation cycle.