Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Bits & Machine Numbers  ›  Lesson

AND, OR, XOR & Shifts

Bitwise Operators 25 minEvery integer is a row of bits, and five operators work on the whole row at once
You're building a piece ofQuarry — the record encoder
This piece — changed_flags(): Reports which status bits a write switched on and which it switched off.
Scenario A write lands on a Quarry record and its status word changes. Before it's logged, the change detector reports exactly which flags went on and which went off, so the flusher knows the record is dirty and the cache knows whether it was unpinned.
Your task
Build changed_flags(old, new). Each bit of a Quarry status word is a flag. Return {"set": [...], "cleared": [...]}: the bit positions, in increasing order, that the write switched on and switched off. Example: changed_flags(0b0101, 0b0110) → {"set": [1], "cleared": [0]}.

AND, OR, XOR & Shifts

In short: an integer is a row of bits, and four operators act on the whole row at once: & keeps a bit only where both sides have it, | keeps a bit where either side has it, ^ keeps a bit where the sides differ, and ~ flips every bit. << and >> slide the whole row left or right, which multiplies or floor-divides by a power of two. Python ints have no fixed width, so any trick that depends on a register's bits running out (wraparound, overflow) needs an explicit mask.

Four bit columns, x=12 and y=10 stacked above the AND, OR and XOR rows, with the AND row highlighted green because it keeps only the column where both are 1

Meet Quarry

For six sections you built Tunebox, a music app. From here the course builds something harder: Quarry, a search-and-storage engine. Think of the part of a database that nobody sees. It takes records in, stores them compactly, finds them again quickly and keeps them safe as they change. Over the coming sections Quarry gets a record encoder, a hash kernel, range analytics, a query planner, a text index and a diff engine. Each one is a real subsystem of a real engine, and each needs an algorithm you haven't met yet.

It starts at the bottom, with the record header. Every record Quarry stores carries four small facts: its id, the shard it lives on, its type, and the epoch (a version counter) it was written in. Held as a Python dict, that costs about two hundred bytes per record. Packed into a single 32-bit word, it costs four:

python

Across a hundred million records that is the difference between 20 GB and 400 MB. The line that builds word uses two operators this lesson is about: << and |. By the end of the section you'll have written the whole encoder yourself. You'll pack and unpack the fields, check them, and index them with bitsets.

A number is a row of switches

An integer is stored in binary, one switch per power of two. Python shows you the switches with bin or a format spec, and reads them back with int(text, 2):

python

Bit positions count from the right, starting at 0. Bit 0 is worth 1, bit 5 is worth 32, and 45 = 32 + 8 + 4 + 1. Everything below is about flipping, testing and moving those switches, many at once, in a single machine instruction.

Four operators that work on every bit at once

Operator Name A result bit is 1 when…
a & b AND both bits are 1
a | b OR at least one bit is 1
a ^ b XOR exactly one bit is 1 (the bits differ)
~a NOT the bit was 0

The first three line two numbers up by bit position and decide each column on its own. No column affects another, so there are no carries like there are in addition. Step through the columns:

watch it move · grid

Each column is decided on its own. AND wants both bits, OR wants either, XOR wants exactly one.

Bit 7: x has 1 and y has 0. AND gives 0, OR gives 1, and XOR gives 1 because they differ. 5 by 8 table. 19 of 40 cells filled.

1 / 9
What does this print?
print(12 & 10, 12 | 10, 12 ^ 10)

❓ Cross-question — "Aren't and/or the same as &/|?" No. and and or treat each whole value as one true or false and return one of the operands: 12 and 10 is 10. & and | work on each of the bits separately. Mixing them up is a classic bug. if flags & DIRTY tests one bit, but if flags and DIRTY is true whenever flags is non-zero.

XOR undoes itself

XOR has a property the others don't: applying the same value twice cancels out. a ^ b ^ b == a, because every bit of b flips the matching bit of a, then flips it back. XOR is also commutative and associative, so the order of a long chain of XORs doesn't matter. Put those facts together and pairs vanish:

python

Interview corner — the lost write

Quarry replicates every write, so its replication log should mention each record id exactly twice: once from the primary and once from the replica. One write was lost on one side, so its id appears only once. Find that id in one pass, O(n) time and O(1) extra memory. A set or a counter would also take O(n) time, but O(n) memory too.

XOR every id together. Each id that appears twice cancels itself out, and the one left standing is the lost write:

python

Look at the explanation, not the code. "Pairs cancel under XOR" is the fact that lets you solve the variants too, like the boss exercise below, where every id appears three times.

Shifts move the whole row

x << k slides every bit k places left and feeds zeros in on the right. That multiplies by 2**k. x >> k slides right and drops the bits that fall off, which is floor division by 2**k. Watch 13 shift left in an 8-bit register, the size of one byte on a real machine:

watch it move · array

A left shift doubles the number, until the top bit has nowhere left to go.

13 is 00001101. Bits 3, 2 and 0 are on, and 8 + 4 + 1 = 13. 8 cells. At index 4.

1 / 6
What does this print?
print(-9 >> 1, -9 // 2)

Python has no fixed width, so you add it

A C uint8_t holds 8 bits, a Java int holds 32. A Python int holds as many as it needs. That's why the last step above says 416 and the register says 160. It's also why ~ looks strange in Python:

What does this print?
print(~5, ~5 & 0xFF)

The fix is always the same: decide the width, and mask to it. A mask of w ones is (1 << w) - 1. That's 0xFF for 8 bits and 0xFFFFFFFF for 32. x & mask keeps the low w bits and throws the rest away. Quarry's headers are 32-bit words, so any arithmetic on one that could spill past bit 31 gets masked back, just as the hardware would. This helper was meant to model an 8-bit register, but it forgot:

broken — fix it

It prints 104 416 4080. Python's << never loses a bit, so bits pushed past b7 are still there. Make shl8 behave like the register.

How wide does a field have to be? A w-bit field holds 2**w different values, 0 to 2**w - 1. Store anything bigger and it wraps. Only the low bits survive, so the stored value is x % 2**w:

python

A Quarry segment holds up to 3,000 record ids. With 11 bits, id 2048 is stored as 0 and collides with the first record. Twelve bits hold 0 to 4,095 and cover them all. That's why the header gives the id 12 bits, and why a segment is capped at 4,096 records.

Counting the bits two words disagree on

XOR finds the differences between two words, and a loop that tests & 1 and shifts right counts them. Step through it and watch diff lose one bit per pass:

step through it
1old = 0b1011
2new = 0b0110
3diff = old ^ new
4count = 0
5while diff:
6 count += diff & 1
7 diff >>= 1
8print(count)

Now read bits off the bottom yourself. This to_bits fills an 8-cell row from the right. Each pass takes x & 1 (the lowest bit) and then shifts it away. Change 45 to another number, or change the width, and watch your own loop fill the row:

your code, drawn · array

Your loop reads the lowest bit with & 1 and shifts it away, so the row fills from b0 upward.

Coming from Java/JS: Java's int is a 32-bit signed word, so 1 << 31 is negative there. It also has >>>, an unsigned right shift. JavaScript turns numbers into 32-bit integers for bitwise operators, so 2**32 | 0 is 0. Python has neither problem and no >>>. Its ints never overflow, so you pick the width yourself with a mask.

Quick practice

What does this print?
print(6 ^ 6, 6 ^ 0)
broken — fix it

It wrongly prints "compressed". Bit 3 was never set. Fix the test.


🎯 Your turn

Every Quarry record has a status word where each bit is a flag: bit 0 means dirty (changed since the last flush), bit 1 means pinned in cache, bit 2 means deleted, and so on. When a write lands, the change log wants to know which flags it switched on and which it switched off.

Write changed_flags(old, new) returning {"set": [...], "cleared": [...]}. Each list holds bit positions in increasing order.

  • changed_flags(0b0101, 0b0110) → {"set": [1], "cleared": [0]}
  • changed_flags(9, 9) → {"set": [], "cleared": []}

Hint — old ^ new marks every bit that changed. Of those, the ones that are on in new were set, and the ones on in old were cleared.

Then press ▶ Run, try the Live App chips, and hit ✓ Check. Green = the first piece of Quarry is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upA word as a string of bits

Write bits_of(x, width) returning the low width bits of x as a string of '0' and '1', most significant first, always exactly width characters long. Bits above width are ignored. A negative x shows its two's-complement bits, so bits_of(-1, 4) is '1111'.

bitwisebinary
DrillHow many bits two headers disagree on

Write hamming(a, b) returning how many bit positions differ between two non-negative integers. Quarry uses it to spot headers that were corrupted by one or two flipped bits.

xorbitwise
BuildRotate a fixed-width word

Write rotate_left(x, k, width) that rotates the width-bit word x left by k places. Bits pushed off the top come back in at the bottom, so nothing is lost. x always fits in width bits, and k may be larger than width. Hash functions use rotation to mix bits.

bit-shiftbitmask
BossThe lost write, with triple replication

Quarry's archive tier keeps three copies of every write, so each record id appears exactly three times in its log, except one id whose write reached only one copy. Write lost_write_three(ids) returning that id. All ids are non-negative and below 2**20. Use O(1) extra memory. A dict of counts doesn't count.

xorbit-manipulation
CapstoneAn adder built from gates

Hardware adds two numbers with nothing but AND, XOR and shifts. Write add_words(a, b, width) that adds two width-bit unsigned words without using + or -, and returns [total, overflow]. total is the sum wrapped to width bits, and overflow is True when the true sum didn't fit.

bit-manipulationbitmask
ExplainPorting a checksum that flips bits

A teammate ports a 32-bit checksum from C to Python. The C code ends with return ~crc;. In Python, every checksum comes out negative and never matches the values stored in old Quarry segments. Explain why this happens, what ~ actually does in Python, and how to fix the port so it gives the C results.

changed_flags.py