As journeyman python developer, I was surprised to find that Python allows bitwise operators to be used on sets. I was even more surprised to find that they work on dicts as well. So, here’s a quick post on how to use them.
Bitwise Operations on sets
I’m assuming most folks that find this article have seen bitwise operators before and included a primer for those that haven’t at the bottom of this article. In Python, The bitwise operators OR |
, AND &
, as well as XOR ^
work on sets in the same way they work on integers. I also included difference -
, subset, and superset here because they are common set operation with a similar shorthand, although it is not a bitwise operation. The same is true regardless of the data type of the elements in the sets. Additionally, sets support updates using the bitwise assignment operations |=
, &=
, as well as ^=
. For example, the following code:
a = {1, 2, 3}
b = {2, 3, 4}
# Union
print(a | b) # {1, 2, 3, 4} Equivalent to a.union(b)
# Intersection
print(a & b) # {2, 3} Equivalent to a.intersection(b)
# Symetric Difference
print(a ^ b) # {1, 4} Equivalent to a.symmetric_difference(b)
print(b ^ a) # {1, 4}
# Difference
print(a - b) # {1} Equivalent to a.difference(b)
print(b - a) # {4}
# Subset
print(a <= b) # False Equivalent to a.issubset(b)
print(a < b) # False Equivalent to a.issubset(b) and a != b
# Superset
print(a >= b) # False Equivalent to a.issuperset(b)
print(a > b) # False
# NOTE: |, &, ^ are commutative, while -, >, < is not
One pontential gotcha is that the methods accept iterables, so if you pass a list or tuple, you will get a set back. There isn’t an equivalent approach for itererables using the bitwise assignment operators. For more details see the Python API docs for Set.
Bitwise Operations on dicts
Bitwise operations on dicts are a little more interesting. Python 3.9 added support for the |
and |=
operations on dicts. These operations nearly equivalent to dict.update()
, they update the first dict with the key-value pairs from the second dict. The |
operation is not commutative, so the order of the dicts matters. For example, the following code:
a = {1: 'a', 2: 'b'}
b = {2: 'c', 3: 'd'}
print(a | b) # {1: 'a', 2: 'c', 3: 'd'}
print(b | a) # {2: 'b', 3: 'd', 1: 'a'}
For the curious here is the PEP proposal to add union operators to dict and the Github MR for the change. I got a kick out of reading the conversation with the submitter and Guido von Rossum, as well as how little code was needed for the change.
Appendix A - Bitwise Operators
For folks that haven’t seen them before. Python has the following bitwise operators:
| # OR
1 | 0 = 1
1 | 1 = 1
0 | 1 = 1
0 | 0 = 0
& # AND
1 & 0 = 0
1 & 1 = 1
0 & 1 = 0
0 & 0 = 0
^ # XOR
1 ^ 0 = 1
1 ^ 1 = 0
0 ^ 1 = 1
0 ^ 0 = 0
~ # NOT
~1 = 0
~0 = 1
<< # Left Shift
1 << 1 = 10
1 << 2 = 100
>> # Right Shift
1 >> 1 = 0
10 >> 2 = 1
100 >> 2 = 10
Appendix B - Differences in Bitwise Opcodes
Out of curiosity I ran the following code to see how the bitwise operators are implemented in CPython (version 3.10.8). No shock that differnt syntax resutls in different opcodes. I may try to update this post with more details later, I’m still curious about how these opcodes translate into the difference in behavior in CPython. Something for another day.
# Given sets
a = {1, 2, 3}
b = {2, 3, 4}
print(dis.dis("a | b"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 BINARY_OR
# 6 RETURN_VALUE
print(dis.dis("a.union(b)"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 LOAD_METHOD 2 (union)
# 6 CALL_METHOD 1
# 8 RETURN_VALUE
print(dis.dis("a & b"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 BINARY_AND
# 6 RETURN_VALUE
print(dis.dis("a.intersection(b)"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 LOAD_METHOD 2 (intersection)
# 6 CALL_METHOD 1
# 8 RETURN_VALUE
print(dis.dis("a ^ b"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 BINARY_XOR
# 6 RETURN_VALUE
print(dis.dis("a.symmetric_difference(b)"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 LOAD_METHOD 2 (symmetric_difference)
# 6 CALL_METHOD 1
# 8 RETURN_VALUE
print(dis.dis("a - b"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 BINARY_SUBTRACT
# 6 RETURN_VALUE
print(dis.dis("a.difference(b)"))
# 1 0 LOAD_NAME 0 (a)
# 2 LOAD_NAME 1 (b)
# 4 LOAD_METHOD 2 (difference)
# 6 CALL_METHOD 1
# 8 RETURN_VALUE
Additional Resources
- Detailed post on all things bitwise in Python: Bitwise Operators in Python
- A post comparing different ways of merge Python dictionaries New Union Operators to Merge Dictionaries in Python 3.9
- CPython Source code for the set object in v3.10.8: Objects/setobject.c