Python to PLC communication looks simple right up until you have to choose *how* Python should query the controller. REST, JSON-RPC or gRPC? Three styles, the same result (reading one variable from the controller), but three completely different calls on the wire.
In this post we break them down on a live example: we read the current shift OEE from a Siemens S7-1200 G2 and a Phoenix PLCnext controller. Along the way we answer the question that comes up in every training session: is the Web API in the S7-1200 G2 the same as a JSON API?
The same read, three ways
Let's say the controller program has a data block OEE_Data with a variable LastShift.rOEE. In Python we want to get a single number:
"OEE_Data".LastShift.rOEE = 84.72 %We can do this same read in three ways. They differ in philosophy, not in the result:
- REST - "give me a RESOURCE": the target lives in the URL, the action is the HTTP verb (GET, PUT).
- JSON-RPC - "CALL a function": one address, and you pass the action in the body (the
methodfield). - gRPC - also "CALL a function", but binary and over HTTP/2.
Style 1: REST, resource-oriented
In REST every thing is a resource with its own address (e.g. /api/variables), and what you do with it is set by the HTTP verb: GET (read), PUT (write). Data travels as JSON, readable at a glance. This style is used by the Web API in Phoenix PLCnext.
# PLCnext _pxc_api (REST) - "give me a RESOURCE"
r = requests.get(".../api/variables",
params={"paths": "...rOEE"})
print(r.json()["variables"][0]["value"])
# -> 84.72
# on the wire:
GET /api/variables?paths=...rOEE (action = HTTP verb + resource address)Style 2: JSON-RPC, the Siemens S7-1200 G2 Web API
In the S7-1200 G2 Siemens went with JSON-RPC 2.0. There is no long list of addresses here - you always POST to a single endpoint /api/jsonrpc, and you pick the action with the method field (e.g. PlcProgram.Read), put the arguments in params, and the answer comes back in result or error.
First you have to log in with the Api.Login method - the controller returns a token that you add to every following request in the X-Auth-Token header:
# 1) login -> token
login = {"id": 1, "jsonrpc": "2.0", "method": "Api.Login",
"params": {"user": "HMI_access", "password": "********"}}
token = requests.post(URL, json=login, verify=False).json()["result"]["token"]With the token, reading a variable is a single POST:
# Siemens S7-1200 G2 Web API (JSON-RPC 2.0 over HTTPS)
msg = {
"id": 1,
"jsonrpc": "2.0",
"method": "PlcProgram.Read", # action = function name
"params": {"var": '"OEE_Data".LastShift.rOEE',
"mode": "simple"},
}
r = requests.post("https://192.168.0.3/api/jsonrpc",
json=msg,
headers={"X-Auth-Token": token},
verify=False) # PLC has a self-signed cert
print(r.json()["result"]) # -> 84.72
# on the wire:
POST /api/jsonrpc (one address, action in the body)On the controller side you have to enable the web server and the Web API in TIA Portal, and grant the user read and write rights for variables. You will find the full method list in Siemens Industry Online Support.
S7-1200 G2 Web API: is it the same as a JSON API?
Short answer: both yes and no. "JSON API" is a casual name for any interface that accepts and returns JSON over HTTP. In that sense the S7-1200 G2 Web API *is* a JSON API - you talk to it with ordinary HTTPS requests and the payload is JSON.
But technically it is a specific protocol: JSON-RPC 2.0, not a RESTful "JSON API". The difference is practical and easy to trip over:
- REST (JSON API): different URLs (nouns), the action is the HTTP verb. You read a resource:
GET /variables/.... - Siemens Web API (JSON-RPC): one address
/api/jsonrpc, the action sits in themethodfield in the body. You read by calling a function:PlcProgram.Read. - In JSON-RPC it is always POST - even when you only read. You will not read anything here with a GET.
- Every message has a fixed skeleton:
jsonrpc: 2.0,method,params,id.
Takeaway for practitioners: if you search the web for "Siemens REST API", you will be disappointed. Search for "S7-1200 Web API JSON-RPC" and treat it as calling functions (login, read, write, logout), not as fetching URLs.
Style 3: gRPC, contract and binary code
gRPC also "calls a function", but differently than JSON-RPC. First you define a contract in a .proto file (types and services), generate code from it (stubs), and the data travels as Protocol Buffers, a binary format over HTTP/2. Not readable at a glance, but fast and with a ready type contract.
// .proto contract (types + services)
service PlcData {
rpc ReadTag(Req) returns (Reply);
}
message Reply { double value = 1; }
# Python (after generating the stubs)
reply = stub.ReadTag(Req(var="...rOEE"))
print(reply.value) # -> 84.72
# on the wire: HTTP/2 + binary protobufREST vs JSON-RPC vs gRPC: the differences
If you remember one sentence: REST says "give me a RESOURCE", and RPC (JSON-RPC and gRPC) says "CALL A FUNCTION". The difference between JSON-RPC and gRPC is in turn text over HTTP versus a binary format over HTTP/2.
- REST - many addresses, HTTP verbs, text JSON. Easy to inspect and debug.
- JSON-RPC - one address, action in the body, text JSON. This is how the Siemens Web API works.
- gRPC - a
.protocontract, binary protobuf, HTTP/2. The fastest, but it requires code generation.
In our project it looks like this: the Siemens S7-1200 G2 uses JSON-RPC, the Phoenix PLCnext uses REST. When to pick what? REST and JSON-RPC for IT, web and cloud integration (they pass through the firewall on port 443). gRPC for fast, binary exchange between services.
What if I don't want a Web API? snap7 and the native S7 protocol
The Web API is not the only way to a Siemens controller. The python-snap7 library talks to the controller with the native S7 protocol (port 102) - no token login, but with raw bytes that you decode yourself by offsets from the data block:
import snap7
from snap7.util import get_real
plc = snap7.client.Client()
plc.connect("192.168.0.3", 0, 1) # IP, rack, slot
data = plc.db_read(1, 0, 4) # DB1, offset 0, 4 bytes
print(get_real(data, 0)) # -> 84.72This requires PUT/GET communication enabled in the CPU and a data block with standard (not optimized) access - otherwise the byte offsets will not match. snap7 is great for fast block reads, and the Web API for web integrations and getting through a firewall.
Common problems (and how to solve them)
- SSLError / certificate - the S7-1200 G2 has a self-signed cert. In testing use
verify=False, and for production add the certificate to the trusted ones. - HTTP 403 / no permissions - the Web API user does not have "Read tags" / "Write tags" rights. Check the user configuration in TIA Portal.
- Token expired - after a period of inactivity the controller invalidates the token. Log in again with the
Api.Loginmethod. - Variable name - the data block name in quotes, members joined with a dot. A typo ends up as an
errorfield in the response. - snap7: wrong values - the data block is optimized or the byte offset is wrong. Switch the DB to standard access and check the offsets in TIA.
Python and other PLCs: Modbus TCP, OPC UA and vendor protocols
REST, JSON-RPC and gRPC are the modern web-style options, but most Python PLC communication in industrial automation still runs over classic industrial Ethernet protocols. The good news: for almost every protocol there is an open-source Python library, so the protocol implementation is already done for you. Here is the short map:
| Protocol | Python library | Typical PLCs | What you get |
|---|---|---|---|
| S7 protocol (TCP port 102) | python-snap7 | Siemens S7-300, S7-400, S7-1200, S7-1500 | Fast raw reads of data blocks, inputs, outputs and markers |
| Siemens Web API (JSON-RPC over HTTPS) | requests | S7-1200 G2 and recent S7-1500 firmware | PLC tags by symbolic name, user rights, works through firewalls |
| Modbus TCP (port 502) | pymodbus | Almost any PLC with a Modbus TCP server, also Siemens via MB_SERVER | Coils and 16-bit registers, simplest common denominator |
| OPC UA | asyncua | S7-1500, CODESYS, PLCnext and many others | Typed, browsable variables, subscriptions, security |
| EtherNet/IP (CIP) | pycomm3 | Rockwell Allen-Bradley ControlLogix and CompactLogix | Read and write tags by name |
| MC protocol (SLMP) | pymcprotocol | Mitsubishi MELSEC Q and iQ-R series | Device memory such as D registers and M bits |
PROFINET is not on the list on purpose. It is a real-time fieldbus for I/O between a controller and its devices, not a protocol a Python script on a PC uses to read PLC data. If you need data from a PROFINET network, read it from the PLC with one of the protocols above.
Which one should you use? If the PLC has an OPC UA server, start there: you get data types and names instead of byte offsets (see Python OPC UA: read data from Siemens S7-1500). If you need something that works with PLCs from different vendors, Modbus TCP is the lowest common denominator. For Siemens PLCs without OPC UA, choose between snap7 and the Web API as described above.
Python script example: Modbus TCP with pymodbus
The same OEE value can be read over Modbus TCP. On a Siemens PLC you would map it to the holding registers of an MB_SERVER instance; on other PLCs to whatever register area the vendor uses. A REAL takes two 16-bit registers, so the script reads two and joins them:
import struct
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("192.168.0.10", port=502)
client.connect()
# two holding registers = one 32-bit REAL (big-endian word order)
rr = client.read_holding_registers(address=0, count=2)
if rr.isError():
print("Modbus error:", rr)
else:
oee = struct.unpack(">f", struct.pack(">HH", *rr.registers))[0]
print(round(oee, 2)) # -> 84.72
client.close()
The word order is the classic Modbus trap: some devices send the low word first, and then the value comes out as nonsense. Swap the two registers if you get a huge or tiny number. A longer walkthrough with coils, writes and error handling is in Python Modbus PLC communication.
Mapping PLC data types to Python
Whatever the protocol, data from the PLC ends up as Python objects. With the Web API and OPC UA the conversion is done for you. With snap7 and Modbus you decode bytes yourself, so you need to know the data types:
| PLC type (IEC 61131-3) | Size | Python type | snap7 helper |
|---|---|---|---|
| BOOL | 1 bit | bool | get_bool(data, byte, bit) |
| INT | 16 bit, signed | int | get_int(data, offset) |
| DINT | 32 bit, signed | int | get_dint(data, offset) |
| REAL | 32 bit, floating point | float | get_real(data, offset) |
| CHAR | 8 bit | str (one character) | read the byte and decode it |
| STRING (Siemens S7) | 2-byte header + characters | str | get_string(data, offset) |
Siemens S7 data is big-endian, which is why the snap7 helpers and the Modbus example above use big-endian conversion. Python ints have no fixed size, so an INT of -1 only comes out right if you decode it as signed. That is exactly what the helpers do.
Real time, latency and scan time: where Python fits
Python to PLC communication is great for data, and a bad idea for control. A PLC executes its program in a deterministic cycle with a scan time of a few milliseconds. A Python script on a Windows or Linux PC is not real-time: garbage collection, the network stack and the operating system add latency you cannot guarantee. So the rule is simple:
- Real-time control stays in the PLC. Interlocks, PID loops and sequences belong in PLC programming, not in a Python loop.
- Python reads and writes data. OEE and shift reports, logging to a database or Excel, quality data, recipes, dashboards and integration with MES or cloud systems.
- Poll at a sensible rate. A loop that reads every 100 ms to 1 s is plenty for reporting and does not load the CPU communication. If you need every change, use OPC UA subscriptions instead of polling faster.
- Read in blocks. One read of 50 bytes from a data block is much faster than 50 single reads, which is a simple optimization with any protocol.
In larger plants this role is often taken by a SCADA system or a gateway (for example Ignition, which has Python-based scripting), but for one machine or one report a lightweight Python script is often all you need. To learn the basics step by step, check our Python basics for automation engineers course.
Frequently asked questions (FAQ)
No. It is JSON-RPC 2.0 - a single /api/jsonrpc endpoint with the action in the method field. JSON sent over HTTP is easy to mistake for REST, but it is a different style.
Yes. First Api.Login returns a token that you add in the X-Auth-Token header to every request, and at the end you call Api.Logout.
Web API - when you need web integration, HTTPS and getting through a firewall. snap7 - when you want fast native data block reads on the local network.
gRPC (binary protobuf over HTTP/2). REST and JSON-RPC are slower, but simpler to debug and more universal.
Yes. For Rockwell Allen-Bradley ControlLogix and CompactLogix use pycomm3, which reads tags by name over EtherNet/IP. For Mitsubishi MELSEC PLCs use the MC protocol, for example with pymcprotocol. Modbus TCP works with both if the PLC is configured as a Modbus server.
Allen-Bradley hardware used in our materials (Micro800 controllers, HMI panels and accessories) is supplied by our technology partner Elmark Automatyka, an authorized Rockwell Automation distributor.
Yes. The S7-300 and S7-400 speak the S7 protocol natively and do not need the PUT/GET setting. The rack and slot differ: for an S7-300 the CPU is usually in rack 0, slot 2, while the S7-1200 and S7-1500 use rack 0, slot 1.
No, and it does not need to be. Use Python for data acquisition, reporting and integration, and keep real-time control, safety and interlocks in the PLC program.
Summary
Python to PLC communication comes down to one choice: resource or function. REST fetches a resource, JSON-RPC and gRPC call a function. The Siemens S7-1200 G2 goes with JSON-RPC (Web API), the Phoenix PLCnext with REST, and gRPC is left for fast, binary exchange between services. Once you know these three styles, plus Modbus TCP, OPC UA and snap7, you can connect Python to almost any controller, from Siemens PLCs to Allen-Bradley and Mitsubishi.
Want to try it on a live controller and write your own OEE report in Python? Start with our Python basics for automation engineers course at ControlByte Academy.


