Industrial Networking

Python Modbus: Read and Write PLC Data with pymodbus

Simon Adamek 13 min read Updated

Modbus is one of the oldest and still most popular industrial communication protocols on the planet. Created by Modicon in 1979, it is today the lingua franca of sensors, drives, energy meters and PLCs in thousands of factories. Python Modbus is the fastest way to get that data onto your laptop - no vendor licenses, no gateway hardware, just a pip install and thirty lines of code.

This tutorial walks through the essentials: what Modbus is and why it matters, the difference between RTU and TCP, installing pymodbus, reading holding registers, writing setpoints, logging to CSV, and the serial-port variant for legacy RS-485 gear. Every snippet has been tested against real hardware.

What Modbus Is and Why You Should Know It

Modbus is a master-slave (client-server) protocol for exchanging data between industrial devices. Its biggest asset is simplicity: the spec defines only a handful of operations - read registers, write registers, read discrete inputs, read coils. There is no complex handshake, no certificates, no license fees.

In practice, Modbus is everywhere: temperature transmitters, VFDs, PLCs, energy meters, weather stations, inverters. If you work in automation, you will meet Modbus sooner rather than later. Python meets it through the pymodbus library and suddenly your laptop can talk to all of them.

Modbus RTU vs Modbus TCP

Modbus exists in two dominant variants that differ in the physical layer and framing.

Modbus RTU runs over RS-485 (or RS-232). Frames are binary, each containing a device address, function code, payload and CRC checksum. RTU is fast and deterministic but requires a physical serial cable and usually a USB-to-RS485 converter on the Python side.

Modbus TCP is the same protocol wrapped in TCP/IP. Instead of a serial cable you use standard Ethernet - RJ45 or Wi-Fi. Each device has an IP address and listens on port 502. Modbus TCP is easier to set up and is the natural choice when Python talks to a PLC over a LAN.

FeatureModbus RTUModbus TCP
Physical layerRS-485 / RS-232Ethernet (TCP/IP)
Frame formatBinary + CRCTCP/IP + MBAP header
Port / MediumCOM port (serial)Port 502 (Ethernet)
Speed9600-115200 baud10/100 Mbps
Typical Python useUSB-to-RS485 converterDirect via LAN

pymodbus - Modbus in Python

pymodbus is the de-facto Python library for Modbus. It supports both Modbus TCP and RTU, ships a client and a simulator server, handles all standard function codes, and runs on Windows, Linux and macOS. Installation takes one line:

pip install pymodbus

That's it - no compiled extensions, no system dependencies. After installing you have access to the TCP client, the serial (RTU) client, data-conversion helpers and a minimal Modbus server for local testing.

Modbus Function Codes - What You Can Read

Modbus exposes four primary data areas, each with its own function code. Knowing which is which saves you hours of head-scratching.

Data typeFunction codeAccesspymodbus method
Coils (digital outputs)01 / 05 / 15Read / Writeread_coils()
Discrete Inputs02Read onlyread_discrete_inputs()
Holding Registers03 / 06 / 16Read / Writeread_holding_registers()
Input Registers04Read onlyread_input_registers()

In practice you will spend 80% of your time on Holding Registers - this is where PLCs typically expose analog values: temperatures, pressures, speeds, counters, setpoints.

Modbus Protocol Addressing: Slave Address, Registers and PDU

Every Modbus request carries the same few pieces of information: the slave address (called Unit ID in Modbus TCP), the function code, the starting register address and the number of registers. The function code and data form the PDU (protocol data unit); Modbus RTU adds the slave address and CRC on the serial line, and Modbus TCP adds the MBAP header instead.

The most common source of confusion is register numbering. Device manuals often list holding registers as 40001, 40002 and so on, while pymodbus uses the zero-based address sent in the PDU. Holding register 40001 is address=0, 40010 is address=9. If all your values are shifted by one register, this is the reason.

When the device cannot answer a query, it returns a Modbus exception instead of data. The most common codes are 01 (illegal function), 02 (illegal data address), 03 (illegal data value) and 04 (server device failure). In pymodbus you detect them with isError():

rr = client.read_holding_registers(address=500, count=2, slave=1)
if rr.isError():
    # e.g. exception code 2 = illegal data address
    print("Modbus exception:", rr)

First Script - Reading Data from a PLC via Modbus TCP

Time for practice. The snippet below connects to a Modbus TCP device and reads ten holding registers starting at address 0. This is the foundation you will build everything on.

read_modbus.py
from pymodbus.client import ModbusTcpClient

# Connection configuration
PLC_IP = "192.168.1.10"
PLC_PORT = 502

# Open connection
client = ModbusTcpClient(PLC_IP, port=PLC_PORT)
client.connect()

# Read 10 holding registers starting at address 0
result = client.read_holding_registers(
    address=0,
    count=10,
    slave=1
)

# Display the values
if not result.isError():
    for i, val in enumerate(result.registers):
        print(f"Register {i}: {val}")
else:
    print(f"Error: {result}")

# Close the connection
client.close()

This script does exactly what an engineer would do manually in Modbus Poll - only automatically. Schedule it on a cron job, pipe the output to CSV, send an email on alarm, or feed a Grafana dashboard.

What Each Line Does

  • Line 2: Import ModbusTcpClient from pymodbus - this is our Modbus TCP client class.
  • Lines 5-6: PLC IP address and port. Port 502 is the Modbus TCP default.
  • Line 9: Create the client object and open the TCP session.
  • Lines 12-16: Read ten holding registers starting at register 0. slave=1 is the Modbus Unit ID.
  • Lines 19-23: Check for errors and print each register value. Always handle the error case - real networks drop packets.

Continuous Monitoring - Polling in a Loop

A single read is only the start. In production you poll continuously - read every second, react on thresholds, log the history. The snippet below extends the example with error handling and a clean shutdown.

monitor_plc.py
import time
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("192.168.1.10")

try:
    client.connect()
    print("Connected to PLC.")

    while True:
        result = client.read_holding_registers(0, count=4, slave=1)

        if not result.isError():
            temp = result.registers[0] / 10
            pressure = result.registers[1] / 100
            print(f"Temp: {temp} °C | Pressure: {pressure} bar")

            if temp > 85.0:
                print("  ⚠ ALARM: Temperature too high")

        time.sleep(1)

except KeyboardInterrupt:
    print("Monitoring stopped.")
finally:
    client.close()

The PLC often stores analog values as scaled integers (real value × 10 or × 100), which is why we divide. Ctrl+C cleanly exits the loop and closes the socket - your PLC will thank you for not leaving stale connections open.

Modbus RTU - Serial Port Variant

If your device uses RS-485, you need a USB-to-RS485 converter (30-80 EUR) and a small change in code. Replace ModbusTcpClient with ModbusSerialClient:

modbus_rtu.py
from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(
    port="COM3",          # or /dev/ttyUSB0 on Linux
    baudrate=9600,
    parity="N",
    stopbits=1,
    bytesize=8,
    timeout=3
)

client.connect()
result = client.read_holding_registers(0, count=5, slave=1)
print(result.registers)
client.close()

Only the connection setup changes. Baud rate, parity and stop bits must match the device datasheet exactly - 9600-N-8-1 is the most common default for RTU devices.

Writing to Modbus Registers

Modbus is bidirectional. You can write setpoints, change VFD speeds, toggle outputs. Use write_register() for a single word or write_registers() for multiple:

write_modbus.py
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("192.168.1.10", port=502)
client.connect()

# Write a single register (address 100, value 1500)
client.write_register(100, 1500, slave=1)

# Write multiple registers at once
values = [100, 200, 300, 400]
client.write_registers(200, values, slave=1)

print("Values written to PLC.")
client.close()

Warning: writing to PLC registers changes live machine parameters. Always test on a simulator first, and never enable writes to registers that influence safety logic - safety must live inside the PLC, not over a network from Python.

Logging Modbus Data to CSV

Reading without logging is half a job. The snippet below polls three registers every five seconds and appends them to a CSV with a timestamp. Open it later in Excel, pandas or any BI tool.

modbus_to_csv.py
import csv
import time
from datetime import datetime
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("192.168.1.10", port=502)
client.connect()

with open("modbus_data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Timestamp", "Temperature", "Pressure", "Flow"])

    for _ in range(100):
        result = client.read_holding_registers(0, count=3, slave=1)
        if not result.isError():
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            writer.writerow([now] + result.registers)
        time.sleep(5)

client.close()
print("Data saved to modbus_data.csv")

After a few hours of polling you have a time-series file ready for analysis, plotting or ML model training. Upgrade the CSV to InfluxDB or TimescaleDB when you outgrow the file.

Reading 32-bit Floats and Polling Many Devices

Many PLCs and energy meters publish REAL values as two consecutive 16-bit registers. The standard library struct module converts them without any extra dependency. This example assumes big-endian word order; if the result looks wrong, swap the two registers:

import struct
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("192.168.1.10", port=502)
client.connect()

# A REAL value stored in two holding registers (address 10 and 11)
rr = client.read_holding_registers(address=10, count=2, slave=1)
if not rr.isError():
    raw = struct.pack(">HH", rr.registers[0], rr.registers[1])
    value = struct.unpack(">f", raw)[0]
    print(f"Float value: {value:.2f}")

client.close()

When you need to read many PLCs at the same time, the asynchronous client built on asyncio sends the requests in parallel instead of waiting for each device in turn:

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def read_plc(ip):
    client = AsyncModbusTcpClient(ip, port=502)
    await client.connect()
    if not client.connected:
        return ip, None
    rr = await client.read_holding_registers(address=0, count=4, slave=1)
    client.close()
    return ip, None if rr.isError() else rr.registers

async def main():
    plcs = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]
    results = await asyncio.gather(*(read_plc(ip) for ip in plcs))
    for ip, registers in results:
        print(ip, registers)

asyncio.run(main())

Note on pymodbus versions: the examples use the pymodbus 3.x API with keyword arguments (address=, count=, slave=). Passing count by keyword works in every 3.x release, while newer releases no longer accept it as a positional argument. The newest versions also rename slave= to device_id=, so if you get an unexpected keyword error, check your version with pip show pymodbus and the project's release history.

Other Python Modbus Libraries

pymodbus is the most complete library written in Python, developed openly in a GitHub repository and published on the Python Package Index. It is not the only option:

  • minimalmodbus - small and simple, Modbus RTU and ASCII over a serial line only, built on pyserial.
  • umodbus and modbus-tk - lightweight implementations of the Modbus protocol for clients and servers.
  • pyserial - not a Modbus library, but the dependency every serial communication library needs. Install pymodbus with pip install "pymodbus[serial]" to pull it in for RTU.

For most projects using Python with PLCs, pymodbus is the safest choice because it covers TCP, RTU, sync and async clients and a simulator in one package. If your target is a Siemens S7-1500 with OPC UA instead of Modbus, see our guide to Python OPC UA with Siemens S7-1500, and the Modbus complete guide explains the protocol itself in more depth.

Testing Without a Physical PLC - Simulators

No PLC on your desk? No problem. A Modbus simulator fakes a slave device on your computer so you can develop against it. The common choices:

  • ModRSsim2 - free Windows simulator, great for beginners, supports TCP and RTU.
  • pymodbus server - spin up a slave in Python itself, useful for unit tests.
  • diagslave - lightweight command-line simulator for Linux and Windows.

Start with ModRSsim2: launch it, set register values by hand, point your Python client at 127.0.0.1:502 and iterate. You can develop the entire data pipeline without touching the production PLC - a habit worth forming early.

Troubleshooting the Common Failures

ConnectionException: Failed to connect - network path blocked. Verify the PLC is on the same subnet, ping it, check firewall rules, and confirm port 502 is actually open (some PLCs ship with Modbus TCP disabled by default).

All registers read as 0 or nonsense - wrong Unit ID (slave= argument). Many devices use Unit ID 1, but gateways and RTU-over-TCP bridges often use 247 or the device's physical address.

Values look off by factor of 10 or 100 - the PLC is publishing scaled integers. Check the documentation or the PLC program; divide on the Python side accordingly.

Reads succeed but writes silently fail - the register is read-only (Input Register, function code 04) and you should be writing to a Holding Register instead, or the PLC has write protection enabled for this area.

FAQ

Which is faster, Modbus TCP or Modbus RTU?

Modbus TCP over 100 Mbps Ethernet is roughly 10-20× faster than Modbus RTU at 115200 baud. For new installations, TCP is almost always the better choice. RTU still wins on cable cost for long linear runs and in EMC-hostile environments where RS-485's differential signaling is more robust than Ethernet.

Can I have multiple Python clients polling the same PLC?

Yes - Modbus TCP supports multiple parallel connections. Each client opens its own TCP session. Mind the PLC's connection limit (typically 8-16 concurrent Modbus TCP sessions) and avoid hammering with sub-100 ms polls from ten clients at once.

Is pymodbus production-ready?

Yes. Version 3.x is stable, well-tested, and used in commercial integrations. For enterprise deployments, pair it with proper logging, reconnect-on-failure logic, and a circuit-breaker pattern so a down PLC does not crash your whole service.

Should I use async pymodbus?

If you poll dozens of devices simultaneously - yes, use AsyncModbusTcpClient. For a single PLC the sync API is simpler and plenty fast.

How do I read 32-bit floats from a Modbus device?

Modbus registers are 16-bit. Floats and 32-bit integers are split across two consecutive registers, with byte order (big-endian / little-endian / word-swapped) varying by vendor. Combine the two registers with Python's struct module (see the example above) or, in recent pymodbus versions, with client.convert_from_registers() - the older BinaryPayloadDecoder is deprecated. Always test against a known value first because vendors disagree on register order.

Can Modbus do security / authentication?

Classic Modbus has none. There is Modbus TCP Secure (port 802) with TLS, but adoption is limited. In practice, isolate Modbus devices on a dedicated OT network behind a firewall and rely on network-layer security. Do not expose Modbus TCP on public networks.

Which Python Modbus library is best?

For most automation projects pymodbus is the best choice: it supports Modbus TCP and RTU, sync and asyncio clients, and includes a simulator server. minimalmodbus is a good alternative for simple RTU-only scripts.

What's Next

You now have a working Modbus foundation in Python - TCP, RTU, read, write, log. This is enough to unlock most retrofit and data-acquisition projects across the industry.

To go deeper, our Python for Automation Engineers course walks through the full industrial Python stack: Modbus, OPC UA, Snap7, SQL, reporting, visualization, and a complete Factory Data Analyzer project built from scratch. For the communication layer specifically - Modbus, Profinet, OPC UA side by side - the Communication Protocols course covers what you need to integrate across vendor ecosystems.

Both courses are built around real industrial projects, not textbook exercises - same philosophy you see in this article.

Online courses

Want to program PLCs in practice?

Siemens, CODESYS, communication and safety courses with a certificate and instructor support. Start with a free mini-course.

See courses
Select the fields to be shown. Others will be hidden. Drag and drop to rearrange the order.
  • Image
  • SKU
  • Rating
  • Price
  • Stock
  • Availability
  • Add to cart
  • Description
  • Content
  • Weight
  • Dimensions
  • Additional information
Click outside to hide the comparison bar
Compare