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.
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.
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:
-
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. -
find_spec() — Each entry in
sys.pathis searched by calling the registered finders. The first finder that returns a non-NoneModuleSpecwins. -
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
.sowith the module name.
- a regular file with the module name plus
-
Namespace packages — If no
__init__.pyis found but a matching directory exists, Python 3.3+ treats the directory as a namespace package (PEP 420). -
ImportError — If none of the above produces a match,
ModuleNotFoundErroris 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:
-
Adopt a src/ layout. Place all application modules under
src/myapp/with a package__init__.py; never at the repository root. -
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" -
Use absolute imports only per PEP 328. Avoid
from . import signalpatterns that re-introduce ambiguity. -
Pin the Python version on the IoT2000 by invoking
/usr/bin/python3.7or the specific minor version shipped with the image, not the unversionedpython3symlink, in production scripts. - 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.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.