Five Python libraries for driving lab instruments, the same measurement written in each, and a clear answer on which to pick depending on whether you want a transport layer or a framework.

PyVISA is the transport layer, and the other libraries are frameworks built on top of it. That distinction settles most of the confusion in this comparison: you are not choosing between five equivalent options, you are choosing how much structure you want above the same underlying plumbing.
This post writes the same measurement in each library, compares them on driver coverage and workflow fit, and gives a clear recommendation by situation.
your test code
|
PyMeasure / QCoDeS / InstrumentKit / tm_devices <- optional framework layer
|
PyVISA <- transport
|
NI-VISA / Keysight IO Libraries / pyvisa-py <- backend
|
USB / LAN / GPIB / serialEverything except tm_devices in some configurations goes through PyVISA. So the question is not "which library talks to instruments" but "how much of the structure above the wire do I want provided".
The task: configure a DMM for DC volts and take a reading.
PyVISA
import pyvisa
rm = pyvisa.ResourceManager()
dmm = rm.open_resource("USB0::0x2A8D::0x1301::MY57200001::INSTR")
dmm.write("CONF:VOLT:DC 10,0.0001")
voltage = float(dmm.query("READ?"))You write the SCPI. Total control, zero help.
PyMeasure
from pymeasure.instruments.keysight import Keysight34465A
dmm = Keysight34465A("USB0::0x2A8D::0x1301::MY57200001::INSTR")
dmm.mode = "DCV"
voltage = dmm.voltageTyped properties. The SCPI is inside the instrument class. Autocomplete works, and an invalid mode raises rather than silently queueing an instrument error.
QCoDeS
from qcodes.instrument_drivers.Keysight import Keysight34465A
dmm = Keysight34465A("dmm", "USB0::0x2A8D::0x1301::MY57200001::INSTR")
voltage = dmm.volt()Parameters are callables, and everything measured is automatically recorded into QCoDeS's dataset and database.
InstrumentKit
import instruments as ik
dmm = ik.generic_scpi.SCPIMultimeter.open_visa("USB0::0x2A8D::...::INSTR")
dmm.mode = dmm.Mode.voltage_dc
voltage = dmm.measure()Units-aware through pint, so voltage carries volts as a unit rather than being a bare float.
tm_devices (Tektronix)
from tm_devices import DeviceManager
with DeviceManager() as dm:
dmm = dm.add_dmm("192.168.1.60")
voltage = dmm.commands.measure.voltage.dc.query()Typed access to the full command tree, Tektronix hardware only.
| PyVISA | PyMeasure | QCoDeS | InstrumentKit | tm_devices | |
|---|---|---|---|---|---|
| Layer | Transport | Framework | Framework | Framework | Vendor framework |
| Licence | MIT | MIT | MIT | AGPL | Apache |
| Instrument drivers | None, you write SCPI | Large collection | Large collection | Moderate | Tektronix only |
| Typed properties | No | Yes | Yes | Yes | Yes |
| Units handling | No | Partial | Partial | Yes, via pint | No |
| Experiment runner | No | Yes | Yes | No | No |
| Live plotting | No | Yes | Via plottr | No | No |
| Data storage | No | CSV | SQLite database | No | No |
| Learning curve | Low | Moderate | Steep | Low | Low |
| Best for | Anything | Lab experiments | Research sweeps | Small scripts | Tektronix benches |
This covers most industrial validation work. The PyVISA tutorial is the starting point, and wrapping each instrument in a small class of your own gives you most of what a framework provides, tailored to what you actually do.
PyMeasure hits a good balance. The Procedure and Worker model handles long-running experiments with graceful abort properly, which is genuinely hard to do by hand.
QCoDeS is excellent at what it targets and heavy for a pass and fail production test. Its concepts, station, parameter, measurement context, take real time to learn.
Note the AGPL licence. For internal tooling this is normally fine, but check it against your organisation's policy before it reaches a product.
It is Tektronix's own supported package and strictly better than their older OpenChoice utilities for automation. Use it for Tektronix hardware and PyVISA for everything else on the same bench.
The most common reason teams pick a framework is "it has a driver for my instrument". Check this properly before committing.
Find the driver in the repository, not in the marketing. Search the actual source tree for your model.
Read it. Coverage is uneven. A driver may implement voltage measurement and not the trigger configuration you need.
Check when it was last touched. Community drivers for older instruments can go years without maintenance.
Test it against your actual unit. Firmware revisions change behaviour, and drivers are usually written against one revision.
A partially implemented driver is worse than no driver, because you inherit its structure and then fight it. If the driver covers less than what you need, it is often faster to write your own class over PyVISA.
Whichever you choose, put your own interface in front of it. Test code should express the test, not the library.
from typing import Protocol
class Supply(Protocol):
def set_voltage(self, volts: float) -> None: ...
def output(self, on: bool) -> None: ...
def measure_current(self) -> float: ...Implement that over PyVISA today. If you later move to PyMeasure, or swap the physical instrument for a different model, only the implementation changes. Every test keeps working.
This is fifteen minutes of work that repeatedly saves days, and it is the single most useful habit in Python instrument code.
Every bench has at least one instrument with no driver anywhere: an old source, a niche load, something from a vendor with three products. This is not a problem, and it is worth showing how small the work is.
import pyvisa
class GenericLoad:
"""Minimal driver for an electronic load. Eight methods covers a bench."""
def __init__(self, resource: str):
self.inst = pyvisa.ResourceManager().open_resource(resource)
self.inst.timeout = 10000
self.inst.write("*RST"); self.inst.write("*CLS")
def constant_current(self, amps: float) -> None:
self.inst.write("FUNC CURR")
self.inst.write(f"CURR {amps}")
def input(self, on: bool) -> None:
self.inst.write(f"INP {'ON' if on else 'OFF'}")
def measure_voltage(self) -> float:
return float(self.inst.query("MEAS:VOLT?"))
def measure_current(self) -> float:
return float(self.inst.query("MEAS:CURR?"))
def check_errors(self) -> None:
response = self.inst.query("SYST:ERR?").strip()
if not response.startswith(("0,", "+0,")):
raise RuntimeError(response)
def close(self) -> None:
self.input(False)
self.inst.close()That is a complete, usable driver in forty lines, written from the instrument's programming guide in perhaps an hour. It exposes exactly what your tests need and nothing else, which makes it easier to read than a community driver covering the full command set.
The lesson is that driver coverage should not drive the library choice as heavily as people let it. A missing driver is an hour, not a blocker.
The strongest argument for the Python route, and the one that never appears in feature comparisons: you can test the test.
class FakeSupply:
def __init__(self): self.voltage = 0.0; self.on = False
def set_voltage(self, v): self.voltage = v
def output(self, on): self.on = on
def measure_current(self): return 0.1 if self.on else 0.0
def test_sequence_disables_output_on_failure():
psu = FakeSupply()
with pytest.raises(AssertionError):
run_regulation_test(psu, dmm=FakeDmm(bad=True))
assert psu.on is False, "output left enabled after a failed test"That test runs in CI, on a laptop, with no hardware, and it catches the single most expensive class of bug in lab automation: a failure path that leaves a device powered.
No graphical environment offers a practical equivalent, and it is available in every library on this page because they are all just Python underneath.
Library choice matters less than dependency discipline. A bench that works today and breaks after an unrelated install is the most common failure of a Python instrument stack, and it has nothing to do with which library you picked.
# requirements.txt, pinned
pyvisa==1.14.1
pyvisa-py==0.7.2 # pure-Python backend, no vendor VISA needed
numpy==1.26.4
pymeasure==0.14.0Three rules:
Pin exact versions and commit the file. Not >=. A bench is not a library.
Use a virtual environment per bench. Two stations with different instruments should not share a Python installation.
Record the VISA backend explicitly. pyvisa.ResourceManager() picks a backend by search order, which is fine until a machine has two installed. Name it: ResourceManager("@py") for pyvisa-py, or the path to the vendor library.
pyvisa-py is worth knowing about specifically. It is a pure-Python VISA implementation that speaks TCPIP and USB without NI-VISA or Keysight IO Libraries installed at all, which removes the largest install-time dependency from the stack. It does not cover GPIB as completely, so a GPIB bench still wants a vendor backend. See GPIB vs USB vs LAN for which transports you actually need.
The recommendation below is "pyVISA plus your own wrapper", which deserves a concrete shape rather than being left as advice.
import pyvisa
class Instrument:
"""Minimal base: open, query with error checking, close."""
def __init__(self, resource: str, timeout_ms: int = 10000, backend: str = ""):
self._rm = pyvisa.ResourceManager(backend)
self.io = self._rm.open_resource(resource)
self.io.timeout = timeout_ms
self.idn = self.io.query("*IDN?").strip()
def cmd(self, command: str):
self.io.write(command)
self._raise_on_error(command)
def ask(self, command: str) -> str:
value = self.io.query(command).strip()
self._raise_on_error(command)
return value
def _raise_on_error(self, context: str):
code, _, message = self.io.query("SYST:ERR?").strip().partition(",")
if code.strip() not in ("0", "+0"):
raise RuntimeError(f"{self.idn}: {context} -> {code}{message}")
def close(self):
self.io.close(); self._rm.close()
def __enter__(self): return self
def __exit__(self, *exc): self.close()About forty lines, and it gives you the three things every abstraction library is really selling: automatic error checking after every command, a context manager so sessions close on exception, and one place to change the timeout policy. Subclass it per instrument class and the model-specific SCPI stays contained.
The reason to write this rather than import it is that you now own the error-handling policy, which is the part that differs most between benches and the part a general-purpose library has to guess at.
| Situation | Library |
|---|---|
| Any SCPI instrument, full control, no magic | pyVISA directly |
| No vendor VISA installable on the machine | pyVISA with the @py backend |
| Tektronix bench, want typed model support | tm_devices |
| Research bench, many instrument types, want abstractions | PyMeasure |
| NI DAQ hardware channels | nidaqmx |
| One instrument, vendor ships a package | The vendor package, then wrap it |
The honest default is pyVISA directly with a thin wrapper of your own. Abstraction libraries save time when they cover your exact models and cost time when they do not, and the coverage question is model-specific rather than vendor-specific.
nidaqmx sideThese libraries solve transport and, in some cases, structure. None of them tell you which commands a specific instrument needs for a specific measurement.
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.
PyVISA for general use, because it is the transport layer everything else builds on and works with any SCPI instrument. PyMeasure if you want experiment structure and plotting included. QCoDeS if you are in a research lab with complex parameter sweeps.
PyVISA is a transport layer that sends and receives strings. PyMeasure builds on it and adds instrument classes with typed properties, an experiment runner, live plotting, and data management. PyMeasure is a framework, PyVISA is plumbing.
It is designed for research, particularly quantum and condensed matter labs, with strong support for complex parameter sweeps and a built-in database. It works elsewhere but its conventions assume a research workflow rather than a pass and fail production one.
PyMeasure uses PyVISA underneath as its default adapter, so it is installed either way. You do not interact with it directly unless you need an instrument PyMeasure does not have a class for.
PyMeasure and QCoDeS both ship substantial driver collections. Coverage is uneven across vendors, so check for your specific models before choosing on this basis. Any missing instrument is straightforward to add over PyVISA.
Yes, since they all sit on PyVISA. A common pattern is using PyMeasure classes for instruments it supports and raw PyVISA for anything it does not.
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.