It is not atomic because it takes multiple bytecode instructions to assign to multiple names and it doesn't take long to confirm it experimentally:
import signal
a = 1
b = 2
def handler(sig, trace):
print a, b
def main():
global a, b
signal.signal(signal.SIGINT, handler)
while True:
a, b = 3, 4
a, b = 1, 2
if __name__ == '__main__':
main()
$ python atom.py
^C3 4
^C3 4
^C1 2
^C1 2
^C3 4
^C1 2
^C1 2
^C1 2
^C1 2
^C1 4 <<<< inconsistent state
In this particular case, if you want two values to change atomically, you can get away with assignment to tuple and accessing its elements in the signal handler. Looking at disassembly:
>>> a = 1
>>> b = 2
>>> c = (1, 2)
>>> def foo():
... global a, b
... a, b = 1, 2
...
>>> def bar():
... global c
... c = (1, 2)
...
>>> dis.dis(foo)
3 0 LOAD_CONST 3 ((1, 2))
3 UNPACK_SEQUENCE 2
6 STORE_GLOBAL 0 (a)
9 STORE_GLOBAL 1 (b)
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
>>> dis.dis(bar)
3 0 LOAD_CONST 3 ((1, 2))
3 STORE_GLOBAL 0 (c)
6 LOAD_CONST 0 (None)
9 RETURN_VALUE
No matter how complex the value is, assignment to a variable (or dict
entry, or object field) is a single atomic store operation.