PYTHON / OPERATORS
Bitwise operators
Use &, |, ^, ~, << and >> to read, set, clear and toggle individual bits of Python integers, and predict the result without guessing.
What you will learn
- Read & | ^ as column-by-column operations on the binary digits of two ints
- Build masks with 1 << position and (1 << n) - 1 to target specific bits
- Predict ~x as -x - 1, and mask with & 0xFF when you need a fixed width
- Know that + binds tighter than <<, so 1 << 2 + 3 shifts by 5
Understanding Bitwise operators
Bitwise operators ignore an integer's decimal appearance and work on its binary digits, one column at a time. For 10 & 6, line up 1010 and 0110 and apply the rule to each column: & keeps a 1 only where both operands have a 1, | keeps a 1 where either does, and ^ keeps a 1 only where the two differ. The result is a new integer built from those columns, which is why 10 & 6 is 2 and 10 ^ 6 is 12 even though nothing about those decimal numbers suggests it.
Python integers have no fixed width, so the mental model for negatives is an infinite two's complement pattern: a positive number has endless leading zeros, a negative number has endless leading ones. That single rule explains ~x == -x - 1 for every integer, because inverting all those leading zeros produces leading ones, and it explains why -1 >> 10 is still -1: the sign bits shifted in are ones, so you can never shift a negative number to zero. It also means << never overflows and never drops high bits; if you want byte-sized behaviour you must ask for it with & 0xFF.
Shifting by n is exact multiplication or division by 2 ** n, with >> rounding down rather than toward zero, so -7 >> 1 is -4, matching -7 // 2. The main precedence surprise is that + and - bind tighter than << and >>, while & binds tighter than == (the opposite of C), so 1 << 2 + 3 is 32 but 3 & 1 == 1 is True. Finally, & and | are not spellings of and and or: they never short-circuit and they combine bit patterns, so 2 & 4 is 0 while 2 and 4 is 4.
flags = 0b1010
mask = 0b0110
print(bin(flags & mask))
print(bin(flags | mask))
print(bin(flags ^ mask))
print(flags << 2, flags >> 1)
print(~flags, ~flags == -flags - 1)Bitwise operators treat an integer as an unbounded two's complement pattern of bits and combine those bits column by column.
Worked examples
Packing permissions into one integer
Shows the standard set, test and clear idioms used with bit flags.
READ, WRITE, EXEC = 1, 2, 4
perms = READ | EXEC
print(f"{perms:03b}")
print(bool(perms & WRITE), bool(perms & EXEC))
perms |= WRITE
perms &= ~EXEC
print(f"{perms:03b}", perms)Example explained
Line 1READ | EXEC turns on bit 0 and bit 2 at once, giving 0b101 (5).
Line 2perms & WRITE is 0 because bit 1 is off, and bool() turns that into the False answer to "is the flag set?".
Line 3perms |= WRITE sets a bit without disturbing the others, reaching 0b111.
Line 4perms &= ~EXEC clears one bit: ~4 is an all-ones pattern with a single zero at bit 2, so the & keeps everything else and leaves 0b011.
Negative integers and the infinite sign bits
Demonstrates that ~ and >> follow two's complement with no width limit.
for n in (5, -5):
print(n, ~n, n >> 1, n << 1)
print(-1 >> 10)
print(-7 >> 1, -7 // 2)
print(~0b1010, ~0b1010 & 0b1111)Example explained
Line 1~5 is -6 and ~-5 is 4 because inverting every bit always lands on -x - 1.
Line 2-1 is all ones, so -1 >> 10 shifts in more ones and stays -1.
Line 3-7 >> 1 is -4, not -3: right shift floors, exactly like -7 // 2.
Line 4~0b1010 is -11, so the four-bit complement 0b0101 only appears after masking with & 0b1111.
Precedence and the and/& confusion
Shows where bitwise operators sit relative to arithmetic, comparison and the logical keywords.
print(1 << 2 + 3)
print((1 << 2) + 3)
print(3 & 1 == 1)
print(2 and 3, 2 & 3)Example explained
Line 11 << 2 + 3 is 1 << 5 because + binds tighter than <<, so the shift count becomes 5.
Line 2Parentheses are the only way to get the shift first, giving 4 + 3.
Line 33 & 1 == 1 groups as (3 & 1) == 1 since & binds tighter than ==, which is the reverse of C.
Line 42 and 3 returns the operand 3, while 2 & 3 returns the bit pattern 0b10, so the two are not interchangeable.
Important notes
These operators require integers: 1.5 & 1 raises TypeError, and bool works only because bool is a subclass of int (True | False is 1).
The same symbols are overloaded elsewhere: & and | mean intersection and union on sets, and | merges dicts from Python 3.9 on, so seeing them does not always mean bit math.
Common mistakes
Expecting ~0b1010 to be 0b0101: it is -11, and you only get 5 after writing ~0b1010 & 0b1111, so unmasked ~ silently produces negative values.
Writing 1 << n + 1 when you meant (1 << n) + 1; + binds tighter, so the shift count grows and the value comes out twice as large as intended.
Reaching for & or | in a condition instead of and or or; they do not short-circuit and 2 & 4 is 0, so a truthiness test can flip from True to False with no error raised.
Try it yourself
Change, predict, then run
Write a function popcount(n) that counts the 1 bits in a non-negative integer using the n &= n - 1 trick in a while loop, and check that popcount(0b101101) is 4 and popcount(255) is 8.
Open the Python workspaceCheck your understanding
Why does ~7 evaluate to -8 in Python?
- Because ~ truncates the value to 32 bits and the highest bit is then read as a sign bit.
- Because Python integers behave as if they have endless leading sign bits, so inverting every bit of x yields -x - 1.
- Because Python stores small integers with the sign bit in position 0, and ~ flips that bit along with the rest.
- Because ~ inverts only the bits Python actually stores and then re-applies the original sign.
Show answer
A positive int is conceptually preceded by infinitely many zero bits; inverting them all gives infinitely many one bits, which is the two's complement encoding of -x - 1, so ~7 is -8 and ~255 is -256. The 32-bit truncation answer is tempting because that is what fixed-width C ints do, but Python integers have no width, so ~255 never becomes a large positive number like 4294967040.