Resolving Python 3 signal Module AttributeError on SIMATIC

David Krause10 min read
Other TopicSiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Resolving Python 3 signal Module AttributeError on SIMATIC IoT2000

Problem Summary

A Python 3 script executing on a SIMATIC IoT2000 (IOT2040 / IOT2050) gateway raises the following exception when importing and using the standard library signal module:

Traceback (most recent call last):
  File "signal.py", line 16, in <module>
    signal.signal(signal.SIGTERM, signal_handler)
AttributeError: module 'signal' has no attribute 'SIGTERM'

Identical scripts run without errors under the Python 2 interpreter bundled with the same IoT2000 firmware image. The interpreter-specific failure signature points to a module shadowing defect in the working directory rather than to a missing symbol in the runtime.

Symptom signature: The traceback cites the user file signal.py as the failing module while the actual stack frame is the stdlib signal module. This is the canonical fingerprint of a name-collision between a local file and a top-level standard library package.

Affected Environment

Component Specification
Hardware platform SIMATIC IOT2040 (6ES7647-0AA00-0YA2), SIMATIC IOT2050 (6ES7647-0BA00-0YA2)
Operating system Siemens IoT2000 Example Image (Debian-based)
Python 2 interpreter /usr/bin/python2 (CPython 2.7, preinstalled)
Python 3 interpreter /usr/bin/python3 (CPython 3.x, preinstalled)
Failing module signal — Set handlers for asynchronous events (Python standard library)
Failure class AttributeError raised from shadowed local file

The issue is independent of the SIMATIC application image revision and is reproducible on any image where the user stores a script named signal.py in the working directory from which python3 is invoked.

Root Cause Analysis

The CPython import system resolves a module by scanning each entry in sys.path in order. The first entry in sys.path is the directory containing the script that was launched (an empty string '' on POSIX, which the interpreter expands to the current working directory).

When the user writes:

# /home/user/myapp/signal.py
import os, sys
print("my local file is being executed")

and then runs python3 myapp.py from /home/user/myapp/, the import statement import signal inside myapp.py resolves to /home/user/myapp/signal.py rather than the standard library module at /usr/lib/python3/dist-packages/signal.py. Because the local file does not define SIGTERM, SIGINT, SIG_IGN, or any other attribute expected by the user code, the subsequent signal.signal(signal.SIGTERM, handler) raises AttributeError.

Why Python 2 Tolerates This but Python 3 Does Not

The interpreter differences observed in the field are subtle and stem from changes in the import machinery between Python 2 and Python 3:

Behavior Python 2.7 Python 3.x
Implicit relative imports Permitted (import signal resolves local first, then falls back to absolute) Removed in PEP 328; only absolute imports by default
Implicit package init Treats directories as packages without __init__.py Requires explicit package init for namespace packages
Fallback chain on collision May re-attempt the absolute path before binding Binds the first match strictly; no fallback
Cached bytecode inspection Looser timestamp comparison Strict PEP 552 hash-based pyc validation

The practical effect: in Python 2 the local signal.py may be executed once and then discarded in favor of the system module when the second-level statement from signal import * or attribute lookup occurs, masking the defect. In Python 3 the local module is bound to the name signal for the entire process lifetime.

Diagnostic Procedure

Confirm the shadowing hypothesis with the following steps executed on the IoT2000 over an SSH or serial console session.

Step 1: Reproduce the AttributeError

iot2000:~$ cd /home/user/myapp
iot2000:~$ python3 myapp.py
Traceback (most recent call last):
  File "signal.py", line 16, in <module>
    signal.signal(signal.SIGTERM, signal_handler)
AttributeError: module 'signal' has no attribute 'SIGTERM'

Step 2: Inspect sys.path

iot2000:~$ python3 -c "import sys; print('\n'.join(sys.path))"

/usr/lib/python3/dist-packages
/usr/lib/python3.7
/usr/lib/python3.7/lib-dynload

The first entry, the empty string, expands to /home/user/myapp — the directory that contains the offending signal.py.

Step 3: Identify the Resolved Module Path

iot2000:~$ python3 -c "import signal; print(signal.__file__)"
/home/user/myapp/signal.py

If the output is anything other than /usr/lib/python3/.../signal.py, the local file is shadowing the standard library module.

Step 4: Inspect sys.modules

iot2000:~$ python3 -c "import signal; print(signal.__dict__.keys())"
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__',
'__file__', '__cached__', '__builtins__', 'os', 'sys', 'print'])

Notice the absence of SIGTERM, SIGINT, signal, SIG_DFL, and SIG_IGN — the complete absence of these symbols is the diagnostic confirmation.

Step 5: Confirm with PYTHONDONTWRITEBYTECODE

If bytecode generation is suspected, disable it and inspect the source directories:

iot2000:~$ PYTHONDONTWRITEBYTECODE=1 python3 -c "import signal; print(signal.__file__)"

Solution

The defect is resolved by ensuring the working directory contains no Python file whose top-level name collides with a standard library module. Several variants are listed in order of preference.

Option A — Remove the Local File (Recommended)

If the local signal.py was a placeholder or test artifact, delete it:

iot2000:~$ cd /home/user/myapp
iot2000:~$ ls -l signal.py
-rw-r--r-- 1 user user 47 Jun 12 10:14 signal.py
iot2000:~$ rm signal.py
iot2000:~$ python3 myapp.py
# script now runs cleanly under python3

Option B — Rename the Local File

If the file is a legitimate program module, rename it to a non-colliding identifier:

iot2000:~$ mv signal.py my_signals.py
iot2000:~$ sed -i 's/^import signal$/import my_signals as signal/' myapp.py

Option C — Move the Script into a Subpackage

Reorganizing the project tree under a package directory keeps the colliding name usable while removing it from sys.path[0]:

iot2000:~$ mkdir -p myapp/pkg
iot2000:~$ mv signal.py myapp/pkg/signal.py
iot2000:~$ touch myapp/pkg/__init__.py

Scripts inside myapp/ will no longer see myapp/pkg/signal.py via the implicit path entry because the package directory is not in sys.path by default.

Option D — Run from a Different Working Directory

Invoke the script from a directory that does not contain the colliding file:

iot2000:~$ cd /home/user
iot2000:~$ python3 myapp/myapp.py

Option E — Use the -I Isolated Flag (Not Recommended)

python3 -I prepends sys.path[0] with an isolated empty path; however this disables site and is generally too aggressive for application scripts on IoT2000.

Best practice: Never name an application module with the name of a Python standard library top-level package. The current Python 3.x standard library includes more than 200 top-level module names — see Python Library Index for the full list. Common collisions encountered on industrial projects include signal.py, email.py, logging.py, parser.py, select.py, ctypes.py, and platform.py.

Verification

After applying the solution, validate that the stdlib module is now bound correctly:

iot2000:~$ python3 -c "import signal; print(signal.__file__); print(hasattr(signal, 'SIGTERM'))"
/usr/lib/python3.7/signal.py
True

Run the original application end-to-end and confirm that the SIGTERM handler responds to kill <pid> from a second shell:

iot2000:~$ python3 myapp.py &
[1] 2841
iot2000:~$ kill 2841
# application prints: "SIGTERM received, shutting down"

Cross-check with the Python 2 interpreter to confirm parity:

iot2000:~$ python2 -c "import signal; print(signal.__file__)"
/usr/lib/python2.7/signal.py

Reference: CPython Module Resolution Order

The exact resolution algorithm implemented in CPython 3.x, per The Import System reference page, follows these rules:

  1. sys.modules — If the module name is already in sys.modules, the cached object is returned. This is the fastest path and is checked first.
  2. find_spec() — Each entry in sys.path is searched by calling the registered finders. The first finder that returns a non-None ModuleSpec wins.
  3. PathEntryFinder — For each path entry, CPython looks for:
    • a regular file with the module name plus .py;
    • a regular file with the module name plus .pyc;
    • a directory named after the module containing __init__.py;
    • an extension module .so with the module name.
  4. Namespace packages — If no __init__.py is found but a matching directory exists, Python 3.3+ treats the directory as a namespace package (PEP 420).
  5. ImportError — If none of the above produces a match, ModuleNotFoundError is raised.

Because sys.path[0] is the empty string expanded to the script's directory, that directory is checked before any system path. The collision is therefore deterministic on every launch.

Troubleshooting Matrix

Observed error Likely cause Diagnostic command Fix
AttributeError: module 'signal' has no attribute 'SIGTERM' Local signal.py shadowing stdlib python3 -c "import signal; print(signal.__file__)" Rename or remove local signal.py
AttributeError: module 'logging' has no attribute 'getLogger' Local logging.py in cwd Same, substitute logging Rename local file
ModuleNotFoundError: No module named 'requests' Third-party module not installed python3 -m pip list pip3 install requests
ImportError: cannot import name 'signal' from 'signal' Circular re-import of self Inspect __name__ == '__main__' Guard execution with if __name__ == '__main__':
SyntaxError on import of stdlib module Bytecode cache stale find . -name __pycache__ -exec rm -rf {} + Clear cache
AttributeError on Python 2 only sys.version_info-dependent code path Branch on sys.version_info[0] Guard with version check
Works on host PC, fails on IoT2000 Different sys.path env | grep PYTHONPATH Unset PYTHONPATH or align with target

Prevention and Project Hygiene

Establish the following conventions in every Python project deployed to SIMATIC IoT2000 gateways:

  1. Adopt a src/ layout. Place all application modules under src/myapp/ with a package __init__.py; never at the repository root.
  2. Reserve top-level names for the standard library by adding a CI lint such as:
    python3 -c "import sys, os; \
    import pkgutil; \
    stdlib = {m.name for m in pkgutil.iter_modules(None) if m.module_finder.path == sys.prefix}; \
    collisions = [f for f in os.listdir('.') if f[:-3] in stdlib and f.endswith('.py')]; \
    assert not collisions, collisions"
  3. Use absolute imports only per PEP 328. Avoid from . import signal patterns that re-introduce ambiguity.
  4. Pin the Python version on the IoT2000 by invoking /usr/bin/python3.7 or the specific minor version shipped with the image, not the unversioned python3 symlink, in production scripts.
  5. Document the signal handler semantics with a reference to the official signal module documentation, which states that handlers always execute in the main thread of the main interpreter.

Signal Handler Caveats Specific to IoT2000

Beyond the shadowing defect, several operational caveats apply when using signal.signal() in long-running services on the IoT2000 platform:

Signal Default action on Linux Recommended handler use
SIGTERM Process termination Graceful shutdown, flush logs, close sockets
SIGINT Process termination Ctrl+C handling for interactive services
SIGHUP Process termination Reload configuration without restart
SIGUSR1 / SIGUSR2 Process termination Custom IPC between systemd and service
SIGPIPE Process termination Suppress with SIG_IGN when writing to closed sockets
SIGCHLD Ignore Use with sigaction for child reaping
Threading constraint: Per the official Python signal documentation, signal handlers are always executed in the main thread of the main interpreter. Code that runs in worker threads cannot receive signals directly; communicate via threading.Event or a queue from the main-thread handler.

Sample Reproducer Script

The following reproducer creates the shadowing file and demonstrates the failure on Python 3 while succeeding on Python 2. Use it to validate any deployment pipeline.

#!/usr/bin/env python3
# save as /tmp/sigtest/repro.py
import os, signal, sys

def handler(signum, frame):
    sys.stdout.write("received signal %d\n" % signum)
    sys.stdout.flush()

def main():
    if len(sys.argv) > 1 and sys.argv[1] == "shadow":
        # write a local file that shadows the stdlib module
        with open(os.path.join(os.path.dirname(__file__), "signal.py"), "w") as f:
            f.write("print('local signal shadow loaded')")
    signal.signal(signal.SIGTERM, handler)
    print("PID:", os.getpid(), "waiting for SIGTERM")
    signal.pause()

if __name__ == "__main__":
    main()

Execute:

iot2000:~$ mkdir /tmp/sigtest && cd /tmp/sigtest
iot2000:~$ cp repro.py .
iot2000:~$ python3 repro.py shadow
local signal shadow loaded
Traceback (most recent call last):
  File "repro.py", line 14, in main
    signal.signal(signal.SIGTERM, handler)
AttributeError: module 'signal' has no attribute 'SIGTERM'

iot2000:~$ rm signal.py
iot2000:~$ python3 repro.py
PID: 3104 waiting for SIGTERM
# in another shell: kill 3104
# received signal 15

Related Defect Classes

The shadowing root cause generalizes to many other stdlib top-level names. The following table lists the most common offenders encountered in IoT2000 deployments:

Conflicting local file Stdlib module shadowed Symptom
email.py email package Missing MIMEText
logging.py logging Missing getLogger
select.py select Missing select
parser.py argparse internals Argparse failure
xml.py xml package Missing etree
copy.py copy Missing deepcopy
os.py os Most calls fail
json.py json Missing dumps
time.py time Missing sleep
types.py types Missing SimpleNamespace

FAQ

Why does my Python 2 script work while Python 3 fails with the same signal.py in the directory?

Python 2's legacy import system used implicit relative imports and a looser fallback chain that could re-resolve to the absolute stdlib module on the second attribute lookup. Python 3 binds the first match found in sys.path strictly and never falls back, so a local signal.py is the authoritative module for the process lifetime.

How do I find the file that is shadowing a standard library module?

Run python3 -c "import signal; print(signal.__file__)" from the suspect directory. If the output is a path inside /usr/lib/python3/..., no collision exists; any other path indicates the shadowing file. Combine with python3 -c "import signal; print(signal.__spec__.origin)" for the authoritative module origin.

Can I keep my custom signal.py file and still use the stdlib signal module?

Yes. Rename the file (e.g., to my_signals.py), import it explicitly as import my_signals as signal, and access the stdlib module via import importlib; real_signal = importlib.import_module('signal'). The two are now distinguishable by name.

Does this issue affect the SIMATIC IOT2050 the same way as the IOT2040?

Yes. Both run the same Siemens IoT2000 Example Image family with identical Python 2 and Python 3 interpreters. The defect is purely a function of the local filesystem and the import system, not the specific board revision.

What does the official Python documentation say about signal handlers and threads on IoT2000?

Per the Python 3 signal module reference, signal handlers always run in the main thread of the main interpreter. Use threading.Event, queue.Queue, or multiprocessing primitives to communicate the signal into worker threads, since signals are not delivered to threads other than the main thread.

Back to blog