Let's Conect with Us Connect!

Siemens S7 Snap7 Python Integration A to Z

Siemens S7 Snap7 Python Integration A to Z

 

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:

  1. Please report them on the issue tracker with a clear description and your version (python -c "import snap7; print(snap7.__version__)").

  2. 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.Client.

S7-400

~1996

Yes

No

No

Full

Works out of the box with s7.Client.

S7-1200 (FW ≤3)

2009

Yes

No

No

Full

Use s7.Client with PUT/GET enabled in TIA Portal.

S7-1200 (FW 4+)

~2014

Yes

Yes

No

Full

Use s7commplus.Client (S7CommPlus V1) or s7.Client (PUT/GET).

S7-1500 (FW 1.x)

2012

PUT/GET only

Yes

No

Full

Use s7commplus.Client (S7CommPlus V1) or s7.Client (PUT/GET).

S7-1500 (FW 2.x)

~2016

PUT/GET only

No

V2

Full

Use s7commplus.Client with TLS, or s7.Client (PUT/GET).

S7-1500 (FW 3.x+)

~2022

PUT/GET only

No

V3

Full

Use s7commplus.Client with TLS, or s7.Client (PUT/GET).

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 Logo class.

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 set_connection_params with TSAP addressing (s7 package)

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 ClientHello advertises the X25519MLKEM768 hybrid group whose ~1.2 KB key share the PLC drops.

  • Modern signature algorithms — OpenSSL advertises Ed25519, Ed448, and RSA-PSS variants in the signature_algorithms extension. 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.

  1. 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
    
  2. Point OPENSSL_CONF at it before the Python process starts. OpenSSL reads this configuration once, when it initialises, so setting the variable from inside Python (e.g. via os.environ) after ssl is 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

db_read(1, 0, 1)

1 byte at offset 0 of DB1

DB1.DBW10

db_read(1, 10, 2)

2 bytes (WORD) at offset 10

DB1.DBD20

db_read(1, 20, 4)

4 bytes (DWORD) at offset 20

DB1.DBX0.3

db_read(1, 0, 1) then get_bool(data, 0, 3)

Bit 3 of byte 0

M0.0

mb_read(0, 1) then get_bool(data, 0, 0)

Bit 0 of merker byte 0

MW10

mb_read(10, 2)

2 bytes (WORD) from merker byte 10

IW0 / EW0

read_area(Area.PE, 0, 0, 2)

Analog input word at address 0

QW0 / AW0

read_area(Area.PA, 0, 0, 2)

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.

Post a Comment

Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
Site is Blocked
Sorry! This site is not available in your country.