14  Backend kwargs and real hardware

import warnings

from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends import LiquidHandlerChatterboxBackend
from pylabrobot.liquid_handling.backends.hamilton.STAR_chatterbox import (
    STARChatterboxBackend,
)
from pylabrobot.liquid_handling.strictness import (
    Strictness, set_strictness, get_strictness,
)
from pylabrobot.resources import (
    STARLetDeck, PLT_CAR_L5AC_A00, cor_96_wellplate_360uL_Fb,
    TIP_CAR_288_C00, hamilton_96_tiprack_300uL_filter,
    opentrons_96_filtertiprack_200ul,
)

async def build(backend, tip_rack_fn):
    lh = LiquidHandler(backend=backend, deck=STARLetDeck())
    await lh.setup()
    carrier = PLT_CAR_L5AC_A00(name="carrier")
    lh.deck.assign_child_resource(carrier, rails=10)
    carrier[0] = plate = cor_96_wellplate_360uL_Fb(name="plate")
    tips = TIP_CAR_288_C00(name="tip_carrier")
    lh.deck.assign_child_resource(tips, rails=2)
    tips[0] = rack = tip_rack_fn(name="rack")
    return lh, plate, rack

star, star_plate, star_rack = await build(
    STARChatterboxBackend(), hamilton_96_tiprack_300uL_filter)
await star.pick_up_tips(star_rack["A1"])
1
Not exported from the backends package — this is the full import path. from pylabrobot.liquid_handling.backends import STARChatterboxBackend fails.
2
Both backends get the same deck, so the recipes below differ only in the backend.

14.1 Pass vendor-specific options

Use a Hamilton-only aspiration parameter from an otherwise ordinary lh.aspirate call.

await star.aspirate(
    star_plate["A1"], vols=[10],
    jet=[True],
    blow_out=[True],
)
1
jet and blow_out are not LiquidHandler parameters. They are collected by **backend_kwargs and forwarded to STARBackend.aspirate, which declares them by name.

STARBackend.aspirate has 42 named parameters beyond ops and use_channels:

import inspect
from pylabrobot.liquid_handling.backends.hamilton.STAR_backend import STARBackend

params = [p for p in inspect.signature(STARBackend.aspirate).parameters
          if p not in ("self", "ops", "use_channels")]
print(len(params), "vendor parameters")
print(params[:8])
42 vendor parameters
['jet', 'blow_out', 'lld_search_height', 'clot_detection_height', 'pull_out_distance_transport_air', 'second_section_height', 'second_section_ratio', 'minimum_height']

See also: the portable arguments this sits alongside are in chapter 4.


14.2 Why the chatterbox accepts anything

A misspelled kwarg is caught on one backend and ignored on another.

def accepts_kwargs(fn) -> bool:
    return any(p.kind == inspect.Parameter.VAR_KEYWORD
               for p in inspect.signature(fn).parameters.values())

print("chatterbox aspirate takes **kwargs:", accepts_kwargs(LiquidHandlerChatterboxBackend.aspirate))
print("STAR aspirate takes **kwargs:      ", accepts_kwargs(STARBackend.aspirate))
chatterbox aspirate takes **kwargs: True
STAR aspirate takes **kwargs:       False

The check runs in LiquidHandler._check_args and short-circuits:

if len(vars_keyword) > 0:
    return set()        # no extra arguments if the method accepts **kwargs

So on the STAR, a bogus argument is caught:

print("default strictness:", get_strictness())

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    await star.aspirate(star_plate["A2"], vols=[10], not_a_real_option=True)
    print("WARN  ->", str(caught[0].message))

set_strictness(Strictness.STRICT)
try:
    await star.aspirate(star_plate["A3"], vols=[10], not_a_real_option=True)
except TypeError as e:
    print("STRICT->", e)
set_strictness(Strictness.WARN)
1
Strictness.WARN is the default.
2
The argument is dropped, not passed on — the operation still runs.
3
Under STRICT the same call raises instead.
4
Strictness is a module-level global, so it stays changed until set back.
default strictness: Strictness.WARN
WARN  -> Extra arguments to backend.aspirate: {'not_a_real_option'}
STRICT-> Extra arguments to backend.aspirate: {'not_a_real_option'}
Strictness Extra argument on a backend without **kwargs
IGNORE logged at debug, dropped
WARN (default) UserWarning, dropped
STRICT TypeError
WarningGotcha: STRICT does nothing on the chatterbox

The generic chatterbox declares **backend_kwargs on every operation, so _check_args returns before the strictness branch is ever reached. Setting Strictness.STRICT and passing not_a_real_option=True to a chatterbox-backed handler raises nothing:

print("no error, no warning")
no error, no warning
cb, cb_plate, cb_rack = await build(
    LiquidHandlerChatterboxBackend(), opentrons_96_filtertiprack_200ul)
await cb.pick_up_tips(cb_rack["A1"])

set_strictness(Strictness.STRICT)
await cb.aspirate(cb_plate["A1"], vols=[10], not_a_real_option=True)
set_strictness(Strictness.WARN)
1
The same call that raises TypeError on the STAR.

A typo in a vendor kwarg therefore survives every simulated run and is discovered on the machine.

Missing arguments raise at any strictness.

Strictness governs extra arguments only. An argument the backend requires and did not receive raises before strictness is consulted:

TypeError: Missing arguments to backend.aspirate: {...}

See also: _check_args returns the set of extras to remove, which is why a WARN-level extra never reaches the backend.


14.3 Run the protocol against a STAR without a STAR

Validate a Hamilton protocol — signatures, tip types, vendor kwargs — with no machine attached.

print("backend:", type(star.backend).__name__)
print("base:   ", type(star.backend).__bases__[0].__name__)
print("channels:", len(star.head))
1
STARChatterboxBackend subclasses STARBackend, so it inherits the real signatures and the real argument checking, and overrides only the parts that would talk to a machine.
backend: STARChatterboxBackend
base:    STARBackend
channels: 8

The STAR also enforces hardware constraints the generic chatterbox does not. Hamilton channels take Hamilton tips:

try:
    bad_rack = opentrons_96_filtertiprack_200ul(name="ot_rack")
    star.deck.get_resource("tip_carrier")[1] = bad_rack
    await star.pick_up_tips(bad_rack["A1"])
except RuntimeError as e:
    print(f"RuntimeError: {e}")
1
The same rack the chatterbox recipes throughout this book pick up from without complaint.
2026-08-27 22:14:10,987 - pylabrobot - WARNING - Resource 'ot_rack' is very high on the deck: 279.19 mm. Be careful when traversing the deck.
RuntimeError: Cannot pick up tips on channels [0].

Import path.

STARChatterboxBackend is not re-exported from pylabrobot.liquid_handling.backends. Import it from pylabrobot.liquid_handling.backends.hamilton.STAR_chatterbox. Its constructor takes the machine’s configuration — num_channels, core96_head_installed, iswap_installed, and the MachineConfiguration / ExtendedConfiguration dataclasses — which is how you model a specific instrument rather than a generic one.

See also: opentrons_chatterbox.py provides the same thing for the OT-2.


14.4 Jog a channel by hand

Move one channel to a coordinate, outside any pipetting operation.

await star.prepare_for_manual_channel_operation(channel=0)
await star.move_channel_x(channel=0, x=100.0)
await star.move_channel_y(channel=0, y=200.0)
await star.move_channel_z(channel=0, z=150.0)
1
Puts the machine into a state where a single channel can be driven directly. Required first.
2
One axis per call, in millimetres, absolute.
2026-08-27 22:14:11,030 - pylabrobot - INFO - moving channel 0 to y: 200.0
WarningGotcha: not implemented on the generic chatterbox

All four raise NotImplementedError with an empty message on LiquidHandlerChatterboxBackend:

try:
    await cb.prepare_for_manual_channel_operation(channel=0)
except NotImplementedError as e:
    print("repr:", repr(e), "| message:", repr(str(e)))
1
The message is empty, so the traceback’s last line is the only clue about which call failed.
repr: NotImplementedError() | message: ''

14.5 Read a backend error family

Read which module failed from a Hamilton firmware error.

from pylabrobot.liquid_handling.backends.hamilton.STAR_backend import (
    STARFirmwareError, STARModuleError, ClotDetectedError,
)

module_error = ClotDetectedError(
    message="Clot detected",
    trace_information=61,
    raw_response="C0RTid0001er99",
    raw_module="C0",
)

firmware_error = STARFirmwareError(
    errors={"Pipetting channel 1": module_error},
    raw_response="C0RTid0001er99/61",
)

for module, err in firmware_error.errors.items():
    print(f"{module}: {type(err).__name__}")
    print(f"  message:   {err.message}")
    print(f"  trace:     {err.trace_information}")
    print(f"  raw module:{err.raw_module}")

print("is a STARModuleError:", isinstance(module_error, STARModuleError))
1
Constructed here; on hardware star_firmware_string_to_error builds these by parsing the firmware response string.
2
STARFirmwareError wraps a dict keyed by module name, one entry per module that reported a fault.
3
Same consumption pattern as ChannelizedError (chapter 10): iterate the dict, read each cause.
Pipetting channel 1: ClotDetectedError
  message:   Clot detected
  trace:     61
  raw module:C0
is a STARModuleError: True

The shape repeats across vendors.

TecanError, VantageFirmwareError, the Cytomat and Liconic families, and the Molecular Devices errors follow the same layout: one base class per vendor, leaf classes per documented fault. Catch the base; read the leaf.

See also: the portable errors that raise identically on every backend are in chapter 10.


14.6 What to remember

  • Unrecognised keyword arguments to lh.aspirate and friends are forwarded to the backend. STARBackend.aspirate names 42 of them.
  • _check_args returns immediately if the backend method declares **kwargs. The generic chatterbox does, so no strictness level validates anything against it.
  • Strictness is a module-level global: IGNORE drops, WARN warns and drops, STRICT raises. Missing required arguments raise regardless.
  • STARChatterboxBackend subclasses STARBackend, so it validates kwargs and tip types like a STAR with no machine attached. Import it from its module; it is not re-exported.
  • prepare_for_manual_channel_operation then move_channel_x/y/z jogs one channel; NotImplementedError on the generic chatterbox.
  • STARFirmwareError.errors is keyed by module name; each value is a STARModuleError subclass with message, trace_information, raw_response, and raw_module.