PyModbus
PyModbusDocs

Data Types

Convert between Modbus registers and Python data types with PyModbus. Integers, floats, strings, BCD, bit manipulation, and endianness handling.

Data Types in PyModbus

Modbus only knows 16-bit registers and single bits. You need BinaryPayloadDecoder and BinaryPayloadBuilder to work with real data types.

16-bit Integer (Single Register)

# Unsigned (0-65535)
result = client.read_holding_registers(100, 1)
value = result.registers[0]

# Signed (-32768 to 32767)
if value > 32767:
    signed_value = value - 65536
else:
    signed_value = value

# Or use struct
import struct
bytes_val = struct.pack('>H', value)
signed = struct.unpack('>h', bytes_val)[0]

Log register data automatically

TofuPilot records test results from your PyModbus scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

32-bit Integer (Two Registers)

from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

# Read
result = client.read_holding_registers(100, 2)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
value = decoder.decode_32bit_int()  # or decode_32bit_uint()

# Write
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_32bit_int(-123456)
client.write_registers(100, builder.to_registers())

Float (32-bit IEEE 754)

from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

# Read
result = client.read_holding_registers(100, 2)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
float_value = decoder.decode_32bit_float()

# Write
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_32bit_float(3.14159)
client.write_registers(100, builder.to_registers())

Double (64-bit Float)

# Read (4 registers)
result = client.read_holding_registers(100, 4)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
double_value = decoder.decode_64bit_float()

# Write
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_64bit_float(3.141592653589793)
client.write_registers(100, builder.to_registers())

Strings

# Read (8 registers = 16 characters)
result = client.read_holding_registers(100, 8)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG
)
string_value = decoder.decode_string(16).decode('ascii').strip('\x00')

# Write
builder = BinaryPayloadBuilder(byteorder=Endian.BIG)
text = "HELLO WORLD".ljust(16, '\x00')
builder.add_string(text)
client.write_registers(100, builder.to_registers())

Bits (From Registers)

result = client.read_holding_registers(100, 1)
value = result.registers[0]

# Extract individual bits
bit_0 = bool(value & 0x0001)
bit_1 = bool(value & 0x0002)
bit_7 = bool(value & 0x0080)
bit_15 = bool(value & 0x8000)

# Extract all 16 bits
bits = [(value >> i) & 1 for i in range(16)]
print(f"Bits: {bits}")

# Set specific bits
value = 0
value |= (1 << 0)   # Set bit 0
value |= (1 << 5)   # Set bit 5
value |= (1 << 15)  # Set bit 15
client.write_register(100, value)

Endianness (Byte Order)

Different devices use different byte orders. When values look wrong, try all four combinations.

from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

def try_all_endianness(registers):
    combinations = [
        (Endian.BIG, Endian.BIG, "Big-Big"),
        (Endian.BIG, Endian.LITTLE, "Big-Little"),
        (Endian.LITTLE, Endian.BIG, "Little-Big"),
        (Endian.LITTLE, Endian.LITTLE, "Little-Little"),
    ]

    for byte_order, word_order, name in combinations:
        decoder = BinaryPayloadDecoder.fromRegisters(
            registers,
            byteorder=byte_order,
            wordorder=word_order
        )
        value = decoder.decode_32bit_float()
        print(f"{name}: {value}")

result = client.read_holding_registers(100, 2)
try_all_endianness(result.registers)

Scaled Values

Many devices use scaled integers instead of floats. Check the device manual for the scaling factor.

# Temperature scaled by 10 (23.5C = 235)
result = client.read_holding_registers(100, 1)
temperature = result.registers[0] / 10.0

# Pressure scaled by 100 (1.23 bar = 123)
result = client.read_holding_registers(101, 1)
pressure = result.registers[0] / 100.0

# Write scaled value
temp_scaled = int(23.5 * 10)  # 235
client.write_register(100, temp_scaled)

BCD (Binary Coded Decimal)

Some devices (especially older meters and PLCs) use BCD encoding.

def bcd_to_int(bcd_value):
    result = 0
    multiplier = 1
    while bcd_value > 0:
        digit = bcd_value & 0x0F
        result += digit * multiplier
        multiplier *= 10
        bcd_value >>= 4
    return result

def int_to_bcd(value):
    result = 0
    shift = 0
    while value > 0:
        digit = value % 10
        result |= (digit << shift)
        shift += 4
        value //= 10
    return result

# Read BCD value
result = client.read_holding_registers(100, 1)
bcd = result.registers[0]
value = bcd_to_int(bcd)
print(f"BCD {bcd:04X} = {value}")

# Write BCD value
bcd = int_to_bcd(1234)
client.write_register(100, bcd)

Custom Data Structures

For devices with a fixed register layout, a dataclass maps cleanly to register blocks.

from dataclasses import dataclass
from typing import List
from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

@dataclass
class MotorData:
    speed: float       # RPM
    current: float     # Amps
    temperature: float # Celsius
    status: int        # Status bits

    @classmethod
    def from_registers(cls, registers: List[int]):
        decoder = BinaryPayloadDecoder.fromRegisters(
            registers,
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        return cls(
            speed=decoder.decode_32bit_float(),
            current=decoder.decode_32bit_float(),
            temperature=decoder.decode_32bit_float(),
            status=decoder.decode_16bit_uint()
        )

    def to_registers(self) -> List[int]:
        builder = BinaryPayloadBuilder(
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        builder.add_32bit_float(self.speed)
        builder.add_32bit_float(self.current)
        builder.add_32bit_float(self.temperature)
        builder.add_16bit_uint(self.status)
        return builder.to_registers()

# Read motor data (7 registers: 3 floats + 1 uint16)
result = client.read_holding_registers(100, 7)
motor = MotorData.from_registers(result.registers)
print(f"Speed={motor.speed} RPM, Current={motor.current} A")

# Write motor data
motor = MotorData(speed=1500.0, current=2.5, temperature=45.0, status=1)
client.write_registers(100, motor.to_registers())

Time and Date

from datetime import datetime

def read_datetime(client, address):
    """Read date/time from 3 registers in BCD format."""
    result = client.read_holding_registers(address, 3)

    year = bcd_to_int(result.registers[0]) + 2000
    month = bcd_to_int(result.registers[1] >> 8)
    day = bcd_to_int(result.registers[1] & 0xFF)
    hour = bcd_to_int(result.registers[2] >> 8)
    minute = bcd_to_int(result.registers[2] & 0xFF)

    return datetime(year, month, day, hour, minute)

def write_datetime(client, address, dt):
    """Write date/time to 3 registers in BCD format."""
    registers = [
        int_to_bcd(dt.year - 2000),
        (int_to_bcd(dt.month) << 8) | int_to_bcd(dt.day),
        (int_to_bcd(dt.hour) << 8) | int_to_bcd(dt.minute)
    ]
    client.write_registers(address, registers)

dt = read_datetime(client, 100)
print(f"Device time: {dt}")

write_datetime(client, 100, datetime.now())

Float Arrays

from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

def read_float_array(client, address, count):
    result = client.read_holding_registers(address, count * 2)
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    return [decoder.decode_32bit_float() for _ in range(count)]

def write_float_array(client, address, values):
    builder = BinaryPayloadBuilder(
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    for v in values:
        builder.add_32bit_float(v)
    client.write_registers(address, builder.to_registers())

temperatures = read_float_array(client, 100, 10)
write_float_array(client, 200, [20.0, 21.5, 22.0, 23.5, 25.0])

Quick Reference

Decoder methodBuilder methodRegistersPython type
decode_8bit_int()add_8bit_int(v)0.5int
decode_8bit_uint()add_8bit_uint(v)0.5int
decode_16bit_int()add_16bit_int(v)1int
decode_16bit_uint()add_16bit_uint(v)1int
decode_32bit_int()add_32bit_int(v)2int
decode_32bit_uint()add_32bit_uint(v)2int
decode_64bit_int()add_64bit_int(v)4int
decode_64bit_uint()add_64bit_uint(v)4int
decode_32bit_float()add_32bit_float(v)2float
decode_64bit_float()add_64bit_float(v)4float
decode_string(size)add_string(v)size/2bytes/str
decode_bits()add_bits(v)1list[bool]

Always check your device documentation for data type, scaling factor, and byte order. When in doubt, try all four endianness combinations.