Skip to main content

Modbus transport

ModbusTCPTransport and ModbusRTUTransport are the Modbus transports that register-mapped instrument drivers sit on top of. They are a public part of the library so customers can build their own drivers for Modbus-attached instruments (temperature and process controllers, meters, PLCs) without wrapping pymodbus themselves. They are intentionally narrow: a transport opens, closes, and locks a Modbus TCP or RTU connection and exposes raw function-code I/O plus typed register encode and decode. The caller owns the register map (which address holds what).
Modbus is a register-and-coil protocol. A driver reads and writes numbered 16-bit registers and single-bit coils by address; there is no self-describing command set. The Modbus transports use pymodbus under the hood and support both Modbus TCP and Modbus RTU (serial).

When to reach for it

The Modbus transports address Modbus TCP and RTU (serial) devices, exposing raw function-code ops plus a typed codec. Reach for one when the register map is fixed in code, or when you want a standalone client addressing registers by number. For a comparison against the other transports, see Transports. For a config-driven device where the register map lives in a JSON file rather than driver code, use ModbusDevice instead. ModbusDevice composes a Modbus transport and adds semantic access by register alias, scaling, validation, and background polling.

Quickstart

A Modbus transport holds no unit address of its own: it serves any number of unit addresses on the line. Every I/O method takes the target’s unit_id as a keyword argument:
Use ModbusRTUTransport for serial devices. It supports both Modbus serial-line transmission modes: RTU framing (the default) and ASCII framing, via framer:

Register and data types

Modbus defines four address spaces. The Modbus transports name them with the RegisterType vocabulary, and the typed access path dispatches on it: Values wider than 16 bits span consecutive registers. DataType names the encoding, and register_count() reports the span:

Typed access

read_typed and write_typed handle the multi-register encode and decode, so callers work in native Python types rather than assembling 16-bit words:
Modbus itself does not specify how a multi-register value is ordered, so vendors differ. Three keyword flags cover the common permutations, all defaulting to False (big-endian, high word first):
Four rules the typed path enforces:
  • "input" and "discrete" are read-only. write_typed raises ValueError rather than issuing a doomed request.
  • Single-bit spaces require "bool". Passing any other data_type for "coil" or "discrete" raises ValueError.
  • Coil writes require an actual bool. There is no numeric coercion, so write_typed("coil", addr, 1, "bool") raises rather than silently treating 1 as True.
  • Register count follows from the data type. read_typed reads exactly the span register_count() reports, so callers never pass a count.
register_count, decode_registers, and encode_value are also available as module-level functions for callers that hold raw registers already and only need the codec.

Atomic multi-step sequences

Hold the transport lock across several ops to keep them atomic, for example selecting a page or bank register and then reading from it:
Note that a transport error inside the block closes the dead socket before re-raising, so the next op after the with reconnects rather than reusing it.

Sharing one connection across unit addresses

An RS-485 multi-drop line, or a TCP-to-serial gateway fronting one, often serves several devices at different unit addresses over what is physically one connection. Build a single transport and address each device by unit_id per call instead of opening a connection per device. Callers that share the transport pass a holder identity to open/close, so the connection opens once and tears down only when the last owner leaves:
The valid unit_id range is 0 to 255 on ModbusTCPTransport and 0 to 247 on ModbusRTUTransport (Modbus over Serial Line reserves 248-255, so a real RTU/ASCII slave will never have one of those addresses); check_unit_id() validates an address against the transport’s range up front. This is the same shared-ownership mechanism described in Transports: shared ownership, applied to one line instead of one device with two categories. For config-driven devices, ModbusDevice wraps this pattern: pass the same transport to several devices, each with its own unit_id.

Configuration

ModbusTCPTransport

ModbusRTUTransport

Neither transport carries a unit address: every I/O method takes unit_id as a keyword argument. The valid range is 0 to 255 on ModbusTCPTransport and 0 to 247 on ModbusRTUTransport (248-255 are reserved on a real serial line, never assigned to a slave).

Method reference

Error handling

Device-side failures carry the standard Modbus exception-code name, so the message identifies the protocol-level cause rather than just reporting a failure:
The pymodbus synchronous client does not reconnect on its own between operations. The Modbus transport closes the dead socket when an op fails with a transport error and re-raises, so the next call establishes a fresh connection. Application code still has to decide whether to retry; the transport does not retry for you.