Python Part I · Module 3 · Operators and Expressions
Bitwise Operators: & | ^ ~ << >>
Every operator so far has treated a number as a single quantity. These six treat it as a row of bits and work on each position independently, which is how you test whether a flag is set, extract a field from a packed value, or move a value into position. For a semiconductor engineer this is not an exotic corner of the language — it is the operator family that maps most directly onto the register and bus work in Part II, so it gets more care here than a general Python course would give it. The chapter builds each operation from an aligned binary example before naming the symbol, and it spends real time on one distinction that catches almost every hardware engineer exactly once: a Python integer does not have a width, and that changes what one of these operators does.
Foundation15 min readPythonPart IFundamentalsBitwiseMasks
Part I · Module 3 · Chapter 3.7 · Bitwise Operators: & | ^ ~ << >>
1. Writing a Number in Binary
Before the operators, two pieces of notation.
0b in front of a number means the digits are binary.
print(0b1010)
print(0b1111)10
15Both lines printed an ordinary decimal number, because that is what they are. 0b1010 is not a special kind of value — it is the integer ten, written in a way that makes its bit pattern visible in the source.
bin() goes the other way. It is a name Python provides, used like print and type: give it an integer and it hands back a piece of text showing the binary form.
print(bin(10))
print(bin(255))0b1010
0b11111111Notice the result is text — the 0b prefix is part of it. That is exactly what you want for looking at a result.
2. Bitwise AND — &
Line up two numbers in binary and compare each position. A result bit is 1 only when both input bits are 1.
1100 12
& 1010 10
--------
1000 8Position by position: 1 and 1 gives 1; 1 and 0 gives 0; 0 and 1 gives 0; 0 and 0 gives 0.
a = 0b1100
b = 0b1010
print(a & b)
print(bin(a & b))8
0b1000What it is for: testing and extracting
Because & gives 1 only where both have 1, an all-ones pattern lets selected positions through and blocks the rest. Such a pattern is called a mask.
value = 0b10110110
mask = 0b00001111
print(bin(value & mask))0b110The mask has 1s in the low four positions and 0s above. Everywhere the mask is 0 the result is forced to 0; everywhere it is 1 the original bit survives. So the answer is the bottom four bits of value — which were 0110, printed as 0b110 because leading zeros are not shown.
The same idea tests a single bit:
flags = 0b1010
print(flags & 0b0010)
print(flags & 0b0001)2
0A non-zero answer means that bit was set; zero means it was not.
3. Bitwise OR — |
A result bit is 1 when either input bit is 1.
1100 12
| 1010 10
--------
1110 14a = 0b1100
b = 0b1010
print(a | b)
print(bin(a | b))14
0b1110What it is for: setting bits
| turns positions on without disturbing the others:
flags = 0b1010
print(bin(flags | 0b0001))
print(bin(flags | 0b0100))0b1011
0b1110The first set the lowest bit; the second set the third. In both cases every other position kept its original value.
4. Bitwise XOR — ^
A result bit is 1 when the two input bits are different.
1100 12
^ 1010 10
--------
0110 6a = 0b1100
b = 0b1010
print(a ^ b)
print(bin(a ^ b))6
0b110What it is for: flipping, and detecting difference
XOR against a 1 flips that position; against a 0 it leaves it alone:
flags = 0b1010
print(bin(flags ^ 0b1111))0b101Every one of the low four bits was flipped.
It also answers are these different? — XOR of a value with itself is always zero:
print(0b1010 ^ 0b1010)
print(0b1010 ^ 0b1011)0
1Zero means identical; anything non-zero marks exactly which positions differ.
5. Shifting — << and >>
<< moves every bit left by the given number of positions, filling with zeros on the right.
0011 3
<< 2
1100 12print(3 << 2)
print(bin(3 << 2))12
0b1100>> moves bits right, discarding what falls off the end.
1100 12
>> 2
0011 3print(12 >> 2)
print(bin(12 >> 2))3
0b11For non-negative numbers, once the bit movement is clear, the arithmetic effect is easy to see: shifting left by n multiplies by 2 to the power n, and shifting right divides and rounds down.
print(3 << 4)
print(3 * (2 ** 4))48
48What it is for: building a mask for one position
The most common use you will meet:
print(bin(1 << 0))
print(bin(1 << 3))
print(bin(1 << 7))0b1
0b1000
0b100000001 << n produces a value with exactly bit n set, which is how a single-bit mask is built without writing the zeros out by hand. Combined with & from §2:
status = 0b10110110
print(status & (1 << 4))
print(status & (1 << 3))16
0Bit 4 is set; bit 3 is not. The brackets around 1 << 4 are not required, but they make the intent obvious — Chapter 3.8 argues that case properly.
6. A Python Integer Has No Width
This section is the reason the chapter needs care, and it is where a hardware background actively misleads.
value = 0b1111
print(value)150b1111 does not create a four-bit value. It creates the integer fifteen, written in binary. Python does not remember that you typed four digits, and there is no width attached to the result.
That is a genuine difference from RTL. In SystemVerilog, 4'b1111 is a four-bit vector and the width is part of the type — it governs what happens on overflow and inversion. Python has nothing equivalent. Chapter 2.6 §4 already noted that Python integers have no size limit; this is the same fact showing up in bit operations.
7. Bitwise NOT — ~
Here is where the missing width shows.
print(~3)-4Not 12, which is what "flip the four bits I typed" would give. Python had no four bits to flip.
The behaviour is consistent and easy to state:
print(~0)
print(~1)
print(~3)
print(~255)-1
-2
-4
-256Every answer follows the same rule: ~x produces -(x + 1).
x = 3
print(~x)
print(-(x + 1))-4
-4The reason is that Python's integers behave as though they have an unlimited run of leading sign bits, so inverting produces a negative number rather than a bounded pattern. You do not need the representation to use the rule.
8. Inverting Within a Fixed Width
To invert and keep only a chosen number of bits, invert and then mask.
value = 0b1111
mask = 0b1111
print(~value)
print(~value & mask)
print(bin(~value & mask))-16
0
0b0Two steps. ~value produced -16, which is not what you wanted on its own. Then & mask kept only the low four positions — and since inverting all-ones within four bits gives all-zeros, the answer is 0.
A case where the result is more visible:
value = 0b1010
mask = 0b1111
print(bin(~value & mask))0b1011010 inverted within four bits is 0101, printed as 0b101.
The mask does exactly what §2 described: 0b1111 has ones in the four positions you care about, so only those four survive. Written in hex it is the same value and slightly easier to read at larger widths:
print(bin(~0b1010 & 0xF))
print(bin(~0b10101010 & 0xFF))0b101
0b10101010xF is four bits of ones; 0xFF is eight.
9. These Are Not and, or and not
Chapter 3.5 §6 made this point from the other side; here it is from this one.
print(6 & 3)
print(6 and 3)2
3& compared the bits of six and three: 0110 & 0011 is 0010, which is 2. and asked a logical question and handed back one of the values.
They are different operators doing different jobs. Words for logic, symbols for bits.
10. Summary of the Six
| Operator | Each result bit | Common use |
|---|---|---|
& | 1 when both are 1 | test a bit, extract a field with a mask |
| | 1 when either is 1 | set bits |
^ | 1 when they differ | flip bits, detect difference |
~ | inverts — but no width, so ~x is -(x + 1) | rarely alone; pair with a mask |
<< | shifts left, zero-filled | build 1 << n, scale up |
>> | shifts right, discarding | extract a field after masking, scale down |
11. Predict, Run, Observe
Predict each — in binary where the output is shown that way.
a = 0b1100
b = 0b1010
print(a & b)
print(a | b)
print(a ^ b)
print(bin(a & b))
print(bin(a | b))
print(bin(a ^ b))
print(a << 1)
print(a >> 2)
print(~a)
print(bin(~a & 0b1111))8
14
6
0b1000
0b1110
0b110
24
3
-13
0b11The last two are the check. ~a gave -13 — following -(x + 1) with a being 12 — and masking to four bits gave 0b11, because 1100 inverted within four bits is 0011.
12. Exercise
Exercise 1 — reason it out first. Given x = 0b1101 and y = 0b0111, write out the aligned column working for x & y, x | y and x ^ y the way §2 does. Then check all three with bin().
Exercise 2 — extract a field. Given status = 0b11010110, use a mask to print just the low four bits in binary. Then use a different mask to print just the low two.
Exercise 3 — test a bit. Using 1 << n and &, print whether bit 2 and bit 5 of 0b00100100 are set. A non-zero answer means set.
Exercise 4 — the width trap. Predict both, then run:
print(bin(~0b1010))
print(bin(~0b1010 & 0xF))Explain in one sentence why the first is not a four-bit answer.
Exercise 5 — the silent typo. Predict both, then run:
print(2 ** 5)
print(2 ^ 5)Neither fails. Say in one sentence which one you almost certainly meant and how you would spot the mistake in a review.
Exercise 6 — set without disturbing. Given flags = 0b1001, print the binary result of setting bit 1 without changing any other position. Confirm bits 0 and 3 are still set.
13. Summary
0bwrites a number in binary,0xin hexadecimal, andbin()shows an integer's binary form as text. All are spellings of ordinary integers.&gives 1 only where both are 1 — used with a mask to test a bit or extract a field.|gives 1 where either is 1 — used to set bits without disturbing others.^gives 1 where they differ — used to flip bits and to detect difference. It is not exponentiation.<<and>>move bits, and1 << nis the standard way to build a single-bit mask.- A Python integer has no width.
0b1111is the number 15, not a 4-bit vector — no overflow, no wraparound. ~xis-(x + 1), not "flip the bits you typed". For fixed-width inversion, mask afterwards:~value & 0xF.&and|are notandandor. On integers they give different answers.
One chapter remains, and it settles a question that has been deferred since 3.1: when several operators appear in one expression, which happens first?
Where this fits
Part of the Python curriculum.
