Introduction
python-snap7 is a pure Python S7 communication library for interfacing natively with Siemens S7 PLCs. The library implements the complete S7 protocol stack including TPKT (RFC 1006), COTP (ISO 8073), and S7 protocol layers, as well as the S7CommPlus protocol for newer PLCs.
The name “python-snap7” is historical: the library originally started as a Python wrapper around the Snap7 C library. As of version 3.0, the C library is no longer used, but the name is kept for backwards compatibility.
python-snap7 requires Python 3.10+ and runs on Windows, macOS and Linux without any native dependencies.
The s7commplus package
For S7-1200 and S7-1500 PLCs, the s7commplus package provides a native S7CommPlus protocol client. It supports V1, V2 (TLS), and V3 connections:
from s7commplus import Client
client = Client()
client.connect("192.168.1.10")
data = client.db_read(1, 0, 4)
client.disconnect()
The s7 package (legacy S7)
The s7 package implements the classic S7 protocol. It supports S7-300, S7-400, S7-1200 and S7-1500 PLCs via the PUT/GET interface:
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 1)
data = client.db_read(1, 0, 4)
client.disconnect()
Use s7.Client for S7-300/400 PLCs or when PUT/GET access is enabled on S7-1200/1500. Use s7commplus.Client for native S7CommPlus communication with S7-1200/1500 PLCs.
Note
The snap7 package name continues to work as an alias for s7 and is not deprecated. Existing code using from snap7 import Client does not need to change.
Note
Version 3.0 is a complete rewrite. Previous versions of python-snap7 were a wrapper around the C snap7 shared library. Starting with version 3.0, the entire protocol stack is implemented in pure Python. This eliminates the need for platform-specific shared libraries and makes the library portable to any platform that runs Python.
If you experience issues, please report them on the issue tracker with a clear description and the version you are using. As a workaround, you can install the last pre-3.0 release with pip install "python-snap7<3".
Installation
python-snap7 is a pure Python package with no native dependencies. Install it using pip:
$ pip install python-snap7
If you want to use the CLI interface for running an emulator, install it with:
$ pip install "python-snap7[cli]"
That’s it! No native libraries or platform-specific setup is required. This works on any platform that supports Python 3.10+, including ARM, Alpine Linux, and other environments where the old C library was hard to install.
Upgrading from 2.x
Version 3.0 is a complete rewrite. Previous versions wrapped the C snap7 shared library; version 3.0 implements the entire protocol stack in pure Python. While the public API is largely the same, this is a fundamental change under the hood.
If you experience issues after upgrading:
Please report them on the issue tracker with a clear description and your version (
python -c "import snap7; print(snap7.__version__)").As a workaround, pin to the last pre-3.0 release:
$ pip install "python-snap7<3"
PLC Support Matrix
This page documents which Siemens PLC families are supported by python-snap7, the communication protocols they use, and any configuration requirements.
Supported PLCs
PLC Family | Introduced | S7 (classic) | S7CommPlus V1 | S7CommPlus V2/V3 | python-snap7 support | Notes |
|---|---|---|---|---|---|---|
S7-300 | ~1994 | Yes | No | No | Full | Works out of the box with |
S7-400 | ~1996 | Yes | No | No | Full | Works out of the box with |
S7-1200 (FW ≤3) | 2009 | Yes | No | No | Full | Use |
S7-1200 (FW 4+) | ~2014 | Yes | Yes | No | Full | Use |
S7-1500 (FW 1.x) | 2012 | PUT/GET only | Yes | No | Full | Use |
S7-1500 (FW 2.x) | ~2016 | PUT/GET only | No | V2 | Full | Use |
S7-1500 (FW 3.x+) | ~2022 | PUT/GET only | No | V3 | Full | Use |
S7-1500R/H | ~2019 | No | No | V2/V3 | Not supported | Redundant CPUs; no classic S7 fallback available. |
ET 200SP CPU | ~2014 | PUT/GET only | Yes | Yes | PUT/GET only | Same behavior as S7-1500 with matching firmware. |
S7-200 SMART | ~2012 | Subset | No | No | Partial | Basic read/write works. Some advanced functions may not be available. |
LOGO! 8 | ~2014 | Subset | No | No | Full | Use the |
Enabling PUT/GET Access
For S7-1200 and S7-1500 PLCs, classic S7 protocol access requires the PUT/GET option to be enabled. See TIA Portal Configuration for step-by-step instructions.
When using s7commplus.Client, the S7CommPlus protocol does not require PUT/GET to be enabled. PUT/GET is only needed when using s7.Client (legacy protocol).
Warning
PUT/GET access provides unauthenticated read/write access to PLC memory. Only enable this on networks that are properly segmented and secured.
Protocol Overview
Siemens has evolved their PLC communication protocols over time:
Protocol | Encryption | Authentication | Used by |
|---|---|---|---|
S7 (classic) | None | None | S7-300, S7-400, S7-1200, S7-1500 (PUT/GET mode) |
S7CommPlus V1 | None | Challenge-response | S7-1200 FW 4+, S7-1500 FW 1.x |
S7CommPlus V2 | TLS 1.3 | Challenge-response + TLS | S7-1500 FW 2.x |
S7CommPlus V3 | TLS | Certificate-based | S7-1500 FW 3.x+ |
python-snap7 implements the classic S7 protocol (via s7) and S7CommPlus V1, V2, and V3 (via s7commplus). The classic protocol remains available on most PLC families via the PUT/GET mechanism. S7CommPlus V4 is not yet supported; for PLCs that require it, consider using OPC UA as an alternative.
Alternatives for Unsupported PLCs
If your PLC is not supported by python-snap7, consider these alternatives:
OPC UA: S7-1500 PLCs (FW 2.0+) include a built-in OPC UA server. Use a Python OPC UA client such as opcua-asyncio.
TIA Portal: Siemens’ official engineering tool supports all protocols and PLC families.
PROFINET: For real-time communication needs, PROFINET may be more appropriate than S7
Connecting to PLCs
This page shows how to connect to different Siemens PLC models using python-snap7.
Rack/Slot Reference
PLC Model | Rack | Slot | Notes |
|---|---|---|---|
S7-300 | 0 | 2 | |
S7-400 | 0 | 3 | May vary with multi-rack configurations |
S7-1200 | 0 | 1 | PUT/GET access must be enabled in TIA Portal (or use S7CommPlus) |
S7-1500 | 0 | 1 | PUT/GET access must be enabled in TIA Portal (or use S7CommPlus) |
S7-200 / Logo | – | – | Use |
Warning
S7-1200 and S7-1500 PLCs ship with PUT/GET communication disabled by default. Use s7commplus.Client to communicate via S7CommPlus, which does not require PUT/GET to be enabled. If you need to use s7.Client (legacy protocol), enable PUT/GET in TIA Portal under the CPU properties. See TIA Portal Configuration for step-by-step instructions.
S7-300
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 2)
S7-400
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 3)
S7-1200 / S7-1500 (S7CommPlus)
from s7commplus import Client
client = Client()
client.connect("192.168.1.10")
S7-1200 / S7-1500 (Legacy PUT/GET)
If PUT/GET access is enabled in TIA Portal, you can also use the legacy protocol:
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 1)
See Client for details on TLS and password authentication.
S7CommPlus over TLS (V2/V3, TIA Portal V17+)
S7-1500 firmware ≥ V2.9 and S7-1200 firmware ≥ V4.5 negotiate S7CommPlus V2 or V3, which transports the protocol inside a TLS 1.3 session. Pass use_tls=True to connect to activate it:
from s7commplus import Client
client = Client()
client.connect(
"192.168.1.10",
use_tls=True,
)
data = client.db_read(1, 0, 4)
client.disconnect()
The client wraps the ISO-on-TCP socket with TLS 1.3 between the InitSSL exchange and the CreateObject request. By default the PLC’s certificate is not verified — fine for development, not fine in production. To verify the PLC against a CA bundle, pass tls_ca:
client.connect(
"192.168.1.10",
use_tls=True,
tls_ca="/path/to/plc-ca.pem",
)
If the PLC requires mutual TLS (client-side certificate), supply tls_cert and tls_key as well.
The cryptography package is required for TLS support. Install with the s7commplus extra:
pip install 'python-snap7[s7commplus]'
Note
Older S7-1200 firmware (FW < 4.5) negotiates V1 of the S7CommPlus protocol, which predates TLS and uses a different proprietary handshake. Client(...) falls back transparently to legacy PUT/GET on those PLCs (db_read / db_write work); browse() and other CommPlus-only operations are not yet supported on those firmwares — see issue #710.
TLS handshake rejected by the PLC (connection reset)
S7 PLCs have a minimal TLS stack that rejects ClientHello messages containing features it does not recognise. Two common causes:
Post-quantum key share (OpenSSL ≥ 3.5) — the default
ClientHelloadvertises theX25519MLKEM768hybrid group whose ~1.2 KB key share the PLC drops.Modern signature algorithms — OpenSSL advertises Ed25519, Ed448, and RSA-PSS variants in the
signature_algorithmsextension. Instead of ignoring unknown algorithms (as TLS 1.2 requires), the PLC treats them as a fatal error and sends a TCP RST.
CPython’s ssl module exposes no API for either the supported_groups or signature_algorithms lists. The fix is to restrict both through OpenSSL’s own configuration via the OPENSSL_CONF environment variable.
Create an OpenSSL configuration file, e.g.
s7-openssl.cnf:# Restrict TLS parameters for S7 PLC compatibility. openssl_conf = openssl_init [openssl_init] ssl_conf = ssl_configuration [ssl_configuration] system_default = system_default_sect [system_default_sect] # Classic ECDHE curves only — no post-quantum groups. Groups = x25519:secp256r1:secp384r1 # Classic signature algorithms only — no Ed25519, Ed448, or RSA-PSS. SignatureAlgorithms = RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA256:ECDSA+SHA384
Point
OPENSSL_CONFat it before the Python process starts. OpenSSL reads this configuration once, when it initialises, so setting the variable from inside Python (e.g. viaos.environ) aftersslis imported is too late — it must be set in the environment:# for a single run OPENSSL_CONF=/path/to/s7-openssl.cnf python your_script.py # or for the whole shell session export OPENSSL_CONF=/path/to/s7-openssl.cnf
With both settings applied, the ClientHello contains only algorithms that S7 PLCs understand and the handshake completes normally.
PLC Password Authentication
If the PLC has a password configured (Full access (no protection) disabled in TIA Portal), call authenticate after connect:
from s7commplus import Client
client = Client()
client.connect(
"192.168.1.10",
use_tls=True,
)
client.authenticate(password="hunter2")
data = client.db_read(1, 0, 4)
Authentication requires TLS to be active (use_tls=True). The client auto-detects whether the PLC firmware uses the legacy SHA-1 challenge or the newer AES-256-CBC challenge. For accounts with a username (TIA Portal V17+ user-based access control), pass it explicitly:
client.authenticate(password="hunter2", username="operator")
S7-200 / Logo (TSAP Connection)
S7-200 and Logo PLCs require TSAP addressing via TSAP addressing:
from s7 import Client
client = Client()
client.set_connection_params("192.168.1.10", 0x1000, 0x2000)
client.connect("192.168.1.10", 0, 0)
Using a Non-Standard Port
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 1, tcp_port=1102)
Routing (Multi-Subnet Access)
Warning
Routing support is experimental and may change in future versions.
When the target PLC sits on a different subnet behind a gateway PLC, use connect_routed to let the gateway forward the connection:
from s7 import Client
client = Client()
client.connect_routed(
host="192.168.1.1", # gateway PLC address
router_rack=0, # gateway rack
router_slot=2, # gateway slot
subnet=0x0001, # target subnet ID
dest_rack=0, # target PLC rack
dest_slot=3, # target PLC slot
)
data = client.db_read(1, 0, 4)
client.disconnect()
Legacy snap7 Package
If you have existing code using snap7.Client, it continues to work unchanged — snap7 is an alias for s7:
import snap7
client = snap7.Client()
client.connect("192.168.1.10", 0, 1)Reading & Writing Data
This page covers address mapping, data type conversions, memory area access, and analog I/O – everything you need for reading from and writing to a PLC.
All examples assume you have a connected client:
from s7 import Client
client = Client()
client.connect("192.168.1.10", 0, 1)
Address Mapping
PLC addresses in Siemens TIA Portal / STEP 7 map to python-snap7 API calls as follows.
PLC Address | API Call | Explanation |
|---|---|---|
DB1.DBB0 |
| 1 byte at offset 0 of DB1 |
DB1.DBW10 |
| 2 bytes (WORD) at offset 10 |
DB1.DBD20 |
| 4 bytes (DWORD) at offset 20 |
DB1.DBX0.3 |
| Bit 3 of byte 0 |
M0.0 |
| Bit 0 of merker byte 0 |
MW10 |
| 2 bytes (WORD) from merker byte 10 |
IW0 / EW0 |
| Analog input word at address 0 |
QW0 / AW0 |
| Analog output word at address 0 |
Important
The byte_index parameter in all s7.util getter/setter functions is relative to the returned bytearray, not the absolute PLC address.
For example, to read DB1.DBX10.3:
data = client.db_read(1, 10, 1) # Read 1 byte starting at offset 10
from s7.util import get_bool
value = get_bool(data, 0, 3) # byte_index=0, NOT 10
You read from PLC offset 10, but data[0] is byte 10 from the PLC.
Data Types
Each example below shows a complete read and write cycle. Data conversion helpers live in s7.util and work with any client.
BOOL
Booleans require a read-modify-write pattern. You cannot write a single bit to the PLC; you must read the enclosing byte, change the bit, then write the whole byte back.
from s7 import util
# Read DB1.DBX0.3 (bit 3 of byte 0)
data = client.db_read(1, 0, 1)
value = util.get_bool(data, 0, 3)
print(f"DB1.DBX0.3 = {value}")
# Write DB1.DBX0.3 -- read first, then modify, then write
data = client.db_read(1, 0, 1)
util.set_bool(data, 0, 3, True)
client.db_write(1, 0, data)
Warning
Never write a freshly created bytearray for booleans. Always read the current byte first to avoid overwriting neighboring bits.
BYTE (1 byte, unsigned 0–255)
from s7 import util
# Read DB1.DBB0 (1 byte at offset 0)
data = client.db_read(1, 0, 1)
value = util.get_byte(data, 0)
print(f"DB1.DBB0 = {value}")
# Write
data = bytearray(1)
util.set_byte(data, 0, 200)
client.db_write(1, 0, data)
INT (2 bytes, signed -32768 to 32767)
from s7 import util
# Read DB1.DBW10
data = client.db_read(1, 10, 2)
value = util.get_int(data, 0)
print(f"DB1.DBW10 = {value}")
# Write
data = bytearray(2)
util.set_int(data, 0, -1234)
client.db_write(1, 10, data)
WORD (2 bytes, unsigned 0–65535)
from s7 import util
# Read DB1.DBW20
data = client.db_read(1, 20, 2)
value = util.get_word(data, 0)
print(f"DB1.DBW20 = {value}")
# Write
data = bytearray(2)
util.set_word(data, 0, 50000)
client.db_write(1, 20, data)
DINT (4 bytes, signed -2147483648 to 2147483647)
from s7 import util
# Read DB1.DBD30
data = client.db_read(1, 30, 4)
value = util.get_dint(data, 0)
print(f"DB1.DBD30 = {value}")
# Write
data = bytearray(4)
util.set_dint(data, 0, 100000)
client.db_write(1, 30, data)
DWORD (4 bytes, unsigned 0–4294967295)
from s7 import util
# Read DB1.DBD40
data = client.db_read(1, 40, 4)
value = util.get_dword(data, 0)
print(f"DB1.DBD40 = {value}")
# Write
data = bytearray(4)
util.set_dword(data, 0, 3000000000)
client.db_write(1, 40, data)
LINT (8 bytes, signed -9223372036854775808 to 9223372036854775807)
from s7 import util
# Read 8 bytes from DB1 at offset 60
data = client.db_read(1, 60, 8)
value = util.get_lint(data, 0)
print(f"LINT = {value}")
# Write (no set_lint helper -- use struct directly)
import struct
data = bytearray(struct.pack(">q", 123456789012345))
client.db_write(1, 60, data)
ULINT (8 bytes, unsigned 0–18446744073709551615)
from s7 import util
# Read 8 bytes from DB1 at offset 68
data = client.db_read(1, 68, 8)
value = util.get_ulint(data, 0)
print(f"ULINT = {value}")
# Write (no set_ulint helper -- use struct directly)
import struct
data = bytearray(struct.pack(">Q", 9876543210))
client.db_write(1, 68, data)
REAL (4 bytes, IEEE 754 float)
from s7 import util
# Read DB1.DBD50
data = client.db_read(1, 50, 4)
value = util.get_real(data, 0)
print(f"DB1.DBD50 = {value}")
# Write
data = bytearray(4)
util.set_real(data, 0, 3.14)
client.db_write(1, 50, data)
LREAL (8 bytes, IEEE 754 double)
from s7 import util
# Read DB1, offset 60, 8 bytes
data = client.db_read(1, 60, 8)
value = util.get_lreal(data, 0)
print(f"LREAL = {value}")
# Write
data = bytearray(8)
util.set_lreal(data, 0, 3.141592653589793)
client.db_write(1, 60, data)
STRING (2 header bytes + characters)
S7 strings have a specific format:
Byte 0: Maximum length (set when the variable is declared in the PLC)
Byte 1: Actual (current) length of the string content
Bytes 2+: ASCII characters
When reading, always request max_length + 2 bytes to include the header.
from s7 import util
# Read a string at DB1, offset 10, declared as STRING[20] in the PLC
max_length = 20
data = client.db_read(1, 10, max_length + 2) # 20 + 2 header bytes = 22
text = util.get_string(data, 0)
print(f"String = '{text}'")
# Write a string
data = client.db_read(1, 10, max_length + 2)
util.set_string(data, 0, "Hello", max_length)
client.db_write(1, 10, data)
Note
Always read the existing data before writing a string. The set_string function preserves the max-length header byte and pads unused characters with spaces.
DATE_AND_TIME (8 bytes, BCD encoded)
from s7 import util
from datetime import datetime
# Read DATE_AND_TIME at DB1, offset 70 (returns ISO 8601 string)
data = client.db_read(1, 70, 8)
dt_string = util.get_dt(data, 0)
print(f"DATE_AND_TIME = {dt_string}") # e.g. '2024-06-15T14:30:00.000000'
# Parse to Python datetime if needed
dt_obj = datetime.fromisoformat(dt_string)
# Write DATE_AND_TIME
data = client.db_read(1, 70, 8)
util.set_dt(data, 0, datetime(2024, 6, 15, 14, 30, 0))
client.db_write(1, 70, data)
Memory Areas
python-snap7 provides convenience methods for data blocks and merkers, and the generic read_area / write_area for all other areas.
Data Blocks (DB)
# Read 10 bytes from DB1 starting at offset 0
data = client.db_read(1, 0, 10)
# Write 4 bytes to DB1 starting at offset 0
client.db_write(1, 0, bytearray([0x01, 0x02, 0x03, 0x04]))
Merkers / Flags (M)
# Read 4 merker bytes starting at MB0
data = client.mb_read(0, 4)
# Write 2 bytes starting at MB10
client.mb_write(10, 2, bytearray([0xFF, 0x00]))
Inputs (I / E)
from s7.type import Area
# Read 2 input bytes starting at IB0
data = client.read_area(Area.PE, 0, 0, 2)
Outputs (Q / A)
from s7.type import Area
# Read 2 output bytes starting at QB0
data = client.read_area(Area.PA, 0, 0, 2)
# Write to QB0
client.write_area(Area.PA, 0, 0, bytearray([0x00, 0xFF]))
Timers (T)
from s7.type import Area
# Read timer T0 (1 timer = 2 bytes)
data = client.read_area(Area.TM, 0, 0, 1)
Counters (C)
from s7.type import Area
# Read counter C0 (1 counter = 2 bytes)
data = client.read_area(Area.CT, 0, 0, 1)
Analog I/O
Analog inputs are typically 16-bit integers in the peripheral input area (Area.PE). The raw value from the PLC needs to be scaled to engineering units.
Reading Analog Inputs
from s7 import util
from s7 import Client
from s7.type import Area
client = Client()
client.connect("192.168.1.10", 0, 1)
# Read AIW0 (analog input word at address 0)
data = client.read_area(Area.PE, 0, 0, 2)
raw_value = util.get_int(data, 0)
print(f"Raw value: {raw_value}")
# Scale to engineering units
# S7 analog modules typically use 0-27648 for 0-100% range
min_range = 0.0 # e.g., 0 bar
max_range = 10.0 # e.g., 10 bar
scaled = raw_value * (max_range - min_range) / 27648.0 + min_range
print(f"Pressure: {scaled:.2f} bar")
# Read AIW2 (second analog input)
data = client.read_area(Area.PE, 0, 2, 2)
raw_value = util.get_int(data, 0)
Writing Analog Outputs
from s7 import util
from s7.type import Area
# Write to AQW0 (analog output word at address 0)
data = bytearray(2)
util.set_int(data, 0, 13824) # ~50% of 27648
client.write_area(Area.PA, 0, 0, data)
Note
The standard scaling factor 27648 applies to most Siemens analog I/O modules. Check your module documentation for the actual range.
Multi-Variable Read
The read_multi_vars method reads multiple variables in a single PDU request, which is significantly faster than individual reads.
from s7 import Client
from s7.type import Area, WordLen, S7DataItem
from ctypes import c_uint8, cast, POINTER
client = Client()
client.connect("192.168.1.10", 0, 1)
# Prepare items to read
items = []
# Item 1: 4 bytes from DB1, offset 0
item1 = S7DataItem()
item1.Area = Area.DB
item1.WordLen = WordLen.Byte
item1.DBNumber = 1
item1.Start = 0
item1.Amount = 4
buffer1 = (c_uint8 * 4)()
item1.pData = cast(buffer1, POINTER(c_uint8))
items.append(item1)
# Item 2: 2 bytes from DB2, offset 10
item2 = S7DataItem()
item2.Area = Area.DB
item2.WordLen = WordLen.Byte
item2.DBNumber = 2
item2.Start = 10
item2.Amount = 2
buffer2 = (c_uint8 * 2)()
item2.pData = cast(buffer2, POINTER(c_uint8))
items.append(item2)
# Execute the multi-read
result, data_items = client.read_multi_vars(items)
# Access the returned data
value1 = bytearray(buffer1)
value2 = bytearray(buffer2)
Warning
The S7 protocol limits multi-variable reads to 20 items per request. If you need more, split them across multiple calls.