If you have searched for a Python test framework for hardware, you have probably noticed the problem: almost everything you find is built for unit-testing Python code, not for system-testing a physical device on a bench or a production line.
The distinction matters. A unit test asserts that a function returns the right value. A production test measures a voltage on a real board, compares it to a limit, records the number, and tells an operator whether to put the unit in the pass bin or the fail bin. Those are different jobs, and most frameworks only do the first one.
Here are the options that actually exist, and where each one stops working.
What hardware test needs that unit testing does not
Before comparing, it helps to name the requirements. A framework for production test needs:
| Requirement | Why unit-test frameworks lack it |
|---|---|
| Measurements as data | An assert throws away the value. You need the number, its units and its limits, stored |
| Serial number identity | Unit tests have no concept of which physical object is under test |
| Operator interaction | Someone has to scan a barcode, press a button, confirm a fixture is closed |
| Instrument lifecycle | A DMM must be opened once, shared across steps, and closed even when a test fails |
| Ordered phases with continue-on-fail | A failing unit test aborts. A failing production test often continues to collect more diagnostic data |
| Result destination | Unit tests print to a terminal. Production results need to outlive the shift |
The last one is the one people underestimate. A test that prints "PASS" to a console has told you nothing in six months when a customer returns a unit and you want to know what its rail voltage measured on the day it shipped.
Option 1: pytest
pytest is the default Python testing framework and the first thing most engineers reach for. It works for hardware, with caveats.
import pytest@pytest.fixture(scope="session")def dmm(): import pyvisa rm = pyvisa.ResourceManager() inst = rm.open_resource("USB0::0x2A8D::0x1601::MY60012345::INSTR") yield inst inst.close()def test_rail_3v3(dmm): voltage = float(dmm.query("MEAS:VOLT:DC?")) assert 3.2 <= voltage <= 3.4, f"3.3V rail out of range: {voltage}V"What works. Fixtures handle instrument lifecycle well, which is genuinely the right abstraction for a DMM or a power supply. The plugin ecosystem is enormous. Every Python developer already knows it, so there is no training cost. It runs in CI without modification.
Where it stops. The measured value lives inside an assert and then disappears. You know the test failed; you do not have a record that the rail measured 3.19V and had been drifting for three weeks. There is no serial number concept, no operator prompt, and no interface for a technician. You end up writing all of that yourself, which is exactly the custom code you were trying to avoid.
Use it when you are doing R&D validation or firmware CI, and the results feed a developer rather than a production line.
Option 2: OpenHTF
OpenHTF is a Python framework Google built specifically for hardware test. It is the closest thing to a purpose-built answer, and it is free.
import openhtf as htffrom openhtf.util import unitsfrom openhtf.plugs import user_input@htf.measures( htf.Measurement("rail_3v3") .in_range(3.2, 3.4) .with_units(units.VOLT),)def power_rail_test(test): test.measurements.rail_3v3 = 3.31def main(): test = htf.Test(power_rail_test) test.execute(test_start=user_input.prompt_for_test_start())if __name__ == "__main__": main()What works. Measurements are first-class objects with names, values, units and limits. Pass or fail is evaluated from the limits automatically, so you are not writing comparison logic. Serial number prompting is built in. Plugs manage instrument setup and teardown. There is a basic web interface for operators.
Where it stops. Parallel testing of multiple units is limited. The project is not actively developed by Google, and the community is small. Building a real operator interface beyond the basic one means frontend work.
Use it when you are doing production functional test in Python and want structured measurements without designing a data model.
Option 3: The TofuPilot Framework
We built this because OpenHTF got the data model right and stopped short on everything around it. It is open source under MIT.
A procedure is a .yaml file that declares the sequence, the measurements and their limits. Phases are plain Python functions with no decorator, and plugs connect instruments as Python classes.
name: PCBA Functional Testunit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-200"plugs: - name: Multimeter python: plugs.dmm:Multimetermain: - name: Power Rail Test python: phases.power_rail measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4def power_rail(measurements, multimeter): measurements.rail_3v3 = multimeter.read_voltage()class Multimeter: def read_voltage(self) -> float: return 3.31The limits live in the YAML, so the phase stays a plain function that assigns a value. TofuPilot generates the measurement key from the name in snake_case (Rail 3V3 becomes rail_3v3), and injects plugs into the phase by the same rule.
What it adds over OpenHTF. Operator interfaces are built from the procedure definition, with text inputs, checklists, images, sliders and progress bars, so there is no frontend work. Phases and multiple fixture slots run in parallel on a Rust engine while your code stays plain Python. Procedures deploy to stations from a Git push, with immutable build artifacts and instant rollback. Runs queue offline when the network drops and sync when it returns.
Where it stops. It is newer than OpenHTF, so the community is smaller. If your team does not write Python, none of the Python options are the right answer.
Use it when you are running production test on real stations and the operator interface and deployment story matter as much as the test logic.
Option 4: Keep your own framework
Plenty of teams have a home-grown Python framework that works. If yours does, the honest advice is to be careful about replacing it.
The reason is that your framework was shaped around your products, your serial number scheme and your sub-assembly structure. Commercial and open-source alternatives ask you to bend those to their model, and the mismatch is where migrations go wrong.
Keep it when it fits, and the only complaint is maintenance burden rather than missing capability. Reducing the maintenance is often cheaper than migrating.
Replace it when the maintenance is really the analytics layer rather than the test execution. Yield, capability analysis, control charts and traceability are each a small project, and together they are the thing nobody has time to keep up.
Comparison
| pytest | OpenHTF | TofuPilot Framework | |
|---|---|---|---|
| Built for | Software testing | Hardware test | Hardware test |
| Measurements | Implicit in asserts | Structured with limits | Structured with limits |
| Limits declared in | Assert expressions | Python decorators | Procedure YAML |
| Serial number prompt | Manual | Built in | Built in |
| Operator interface | None | Basic web UI | Built from the procedure |
| Parallel phases | Via xdist | Limited | Native |
| Multi-slot fixtures | Manual | Limited | Native |
| Deployment to stations | Your problem | Your problem | Git push |
| Offline handling | Your problem | Your problem | Queues and syncs |
| License | MIT | Apache 2.0 | MIT |
Choosing
Doing R&D validation or firmware CI? Use pytest. The ecosystem and familiarity outweigh the missing hardware features, and your results feed a developer rather than a line.
Running production test and want structured data without building a schema? Use OpenHTF or the TofuPilot Framework. Both give you measurements with limits and units as first-class objects.
Running production test across multiple stations where operators need an interface and scripts need to reach the stations reliably? That is what the TofuPilot Framework was built for, and it is the gap that made us build it rather than extend OpenHTF.
Already have something that works? Fix the maintenance problem before you replace the framework. The framework is rarely the expensive part.
Where the results go
Whichever framework you choose, decide early where results are stored. A framework executes tests; it does not answer "what was our yield last week" or "which test step is costing us the most units".
Store the measured value, not just pass or fail. Version the procedure so results recorded before a limit change stay interpretable. Model sub-assemblies from day one even if you do not use them yet. Those three decisions are much harder to retrofit than to make at the start.