Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bit Operations

You can check, set, clear, and flip any single bit — the foundation of cryptography, compression, and graphics.

This page is a practical tour of all six bitwise operators, the most useful tricks built from them, and the real systems that depend on them every day.


Reading Binary in Python

Before anything else, get comfortable reading and writing numbers in binary.

# bin() shows the binary string, format strings let you control width
n = 42

print(f"Decimal:     {n}")
print(f"Binary:      {bin(n)}")
print(f"Fixed-width: {n:08b}")   # 8-bit zero-padded
print(f"Hex:         {hex(n)}")
print()

# You can write literals directly in binary
x = 0b00101010   # Same as 42
print(f"0b00101010 = {x}")
print(f"Bit length: {x.bit_length()} bits")

AND ( & ) — Masking and Checking Bits

AND keeps a bit only when both operands have it set. Its primary use is masking: isolating specific bits by ANDing with a pattern (the mask) that has 1s only in the positions you care about.

# --- Check if a number is even or odd ---
# The lowest bit is 1 for odd numbers, 0 for even numbers
for n in range(8):
    parity = "odd" if n & 1 else "even"
    print(f"{n} = {n:04b}  ->  {parity}")

print()

# --- Extract specific bits with a mask ---
byte = 0b11010110   # 214

lower_nibble = byte & 0b00001111   # Mask: keep only the lower 4 bits
upper_nibble = (byte >> 4) & 0b00001111   # Shift down, then mask

print(f"byte         = {byte:08b}  ({byte})")
print(f"lower nibble = {lower_nibble:08b}  ({lower_nibble})")
print(f"upper nibble = {upper_nibble:08b}  ({upper_nibble})")

OR ( | ) — Setting Bits

OR sets a bit to 1 wherever either operand has a 1. Use it to switch specific bits on without disturbing the rest.

flags = 0b00000000   # All flags off

READ    = 0b00000100  # bit 2
WRITE   = 0b00000010  # bit 1
EXECUTE = 0b00000001  # bit 0

# Grant read and write permission
flags = flags | READ | WRITE

print(f"flags = {flags:08b}  ({flags})")

# Check individual permissions
print(f"Can read?    {'yes' if flags & READ    else 'no'}")
print(f"Can write?   {'yes' if flags & WRITE   else 'no'}")
print(f"Can execute? {'yes' if flags & EXECUTE else 'no'}")

print()
# This is exactly how Unix file permissions (rwxrwxrwx) work
# chmod 644 sets: owner=110 (rw-), group=100 (r--), other=100 (r--)
chmod_644 = 0b110100100
print(f"chmod 644 = {chmod_644:09b}  (owner: rw-, group: r--, other: r--)")

XOR ( ^ ) — Flipping Bits and Cancellation

XOR outputs 1 only when the two input bits differ. It has two elegant properties:

  • a ^ a == 0 — a value XOR’d with itself cancels out
  • a ^ 0 == a — a value XOR’d with zero is unchanged

These properties make XOR the go-to operator for toggling bits and finding the unique element in a list.

# --- Flip (toggle) specific bits ---
byte = 0b10110100
mask = 0b00001111   # Flip the lower 4 bits

flipped = byte ^ mask
print(f"Original: {byte:08b}")
print(f"Mask:     {mask:08b}")
print(f"Flipped:  {flipped:08b}")
print()

# --- Find the unique number in a list ---
# Every number appears twice EXCEPT one.
# XOR of a number with itself is 0, so all pairs cancel out.
nums = [4, 1, 2, 1, 2, 4, 7, 3, 3]
result = 0
for n in nums:
    result ^= n
print(f"Numbers:       {nums}")
print(f"Unique number: {result}")
print()

# --- Swap two variables without a temporary variable ---
a, b = 13, 27
print(f"Before: a={a}, b={b}")
a ^= b
b ^= a
a ^= b
print(f"After:  a={a}, b={b}")

NOT ( ~ ) — Complement

NOT flips every bit. In Python, integers have arbitrary precision and use two’s complement representation, so ~n always equals -(n+1).

for n in [0, 1, 5, 42, 127]:
    print(f"~{n} = {~n}")

print()
# In fixed-width contexts (like 8-bit hardware), NOT is simpler.
# For an 8-bit value, you AND with 0xFF to mask to 8 bits:
n = 0b10110100   # 180
complement_8bit = (~n) & 0xFF
print(f"n              = {n:08b}  ({n})")
print(f"~n (8-bit)     = {complement_8bit:08b}  ({complement_8bit})")
print(f"n + ~n (8-bit) = {n + complement_8bit}  (always 255 for 8-bit)")

Left Shift ( << ) — Multiply by Powers of 2

Shifting bits left by k positions is equivalent to multiplying by 2^k, and it executes in a single CPU instruction.

n = 1

print("Left shifts — powers of 2:")
for k in range(9):
    shifted = n << k
    print(f"  1 << {k} = {shifted:>4}  ({shifted:08b})")

print()
# Practical: building bitmasks for specific positions
print("Building position masks:")
for position in [0, 1, 3, 7]:
    mask = 1 << position
    print(f"  bit {position}: {mask:08b}  (decimal {mask})")

Right Shift ( >> ) — Divide by Powers of 2

Shifting bits right by k positions is equivalent to integer division by 2^k.

n = 256

print("Right shifts — halving repeatedly:")
for k in range(9):
    shifted = n >> k
    print(f"  256 >> {k} = {shifted:>4}")

print()
# Practical: extract which 'chunk' a value falls in
# (Used in hash tables, image processing, memory addressing)
value = 0b11010110   # 214
print(f"value = {value:08b}  ({value})")
print(f"upper 4 bits = {value >> 4:04b}  ({value >> 4})")
print(f"lower 4 bits = {value & 0xF:04b}  ({value & 0xF})")

Common Bit Tricks

These patterns appear so frequently they are worth memorising.

# --- n & 1: Check parity (even/odd) ---
print("=== n & 1: parity check ===")
for n in [0, 1, 6, 7, 100, 101]:
    print(f"  {n} is {'odd' if n & 1 else 'even'}")

print()

# --- n & (n-1): Clear the lowest set bit ---
# Counts bits, detects powers of 2
print("=== n & (n-1): clear lowest set bit ===")
for n in [12, 8, 7, 6]:
    result = n & (n - 1)
    print(f"  {n:04b} & {n-1:04b} = {result:04b}  ({n} & {n-1} = {result})")

print()

# --- Power of 2 check: a power of 2 has exactly one 1-bit ---
print("=== Power of 2 detection ===")
def is_power_of_two(n):
    return n > 0 and (n & (n - 1)) == 0

for n in [1, 2, 3, 4, 5, 8, 16, 12, 1024, 1000]:
    print(f"  {n:>5}: {is_power_of_two(n)}")

print()

# --- Count set bits (Brian Kernighan's algorithm) ---
print("=== Count set bits ===")
def count_bits(n):
    count = 0
    while n:
        n &= n - 1   # Clear the lowest set bit each iteration
        count += 1
    return count

for n in [0, 1, 7, 8, 255, 42]:
    print(f"  {n:>3} = {n:08b}  ->  {count_bits(n)} bit(s) set")

Real-World Applications

# === 1. RGB Colour Channels (Image Processing) ===
# A 24-bit colour packs red, green, blue into one integer
colour = 0xFF8C00   # Dark orange

red   = (colour >> 16) & 0xFF
green = (colour >>  8) & 0xFF
blue  =  colour        & 0xFF

print(f"Colour: #{colour:06X}")
print(f"  R = {red}   ({red:08b})")
print(f"  G = {green}   ({green:08b})")
print(f"  B = {blue}    ({blue:08b})")
print()

# === 2. Network Subnet Masks ===
# A subnet mask isolates the network portion of an IP address
ip      = (192 << 24) | (168 << 16) | (1 << 8) | 42   # 192.168.1.42
mask    = (255 << 24) | (255 << 16) | (255 << 8) | 0   # 255.255.255.0

network = ip & mask
host    = ip & (~mask & 0xFFFFFFFF)

def ip_str(n):
    return f"{(n>>24)&0xFF}.{(n>>16)&0xFF}.{(n>>8)&0xFF}.{n&0xFF}"

print(f"IP address:  {ip_str(ip)}")
print(f"Subnet mask: {ip_str(mask)}")
print(f"Network:     {ip_str(network)}")
print(f"Host:        {host}")
print()

# === 3. XOR in Cryptography ===
# XOR is the core of stream ciphers: ciphertext = plaintext XOR key
message = [72, 101, 108, 108, 111]   # "Hello" as ASCII
key     = [0xAB, 0xCD, 0xEF, 0x12, 0x34]

encrypted = [m ^ k for m, k in zip(message, key)]
decrypted = [e ^ k for e, k in zip(encrypted, key)]   # XOR again to reverse

print(f"Message:   {[chr(b) for b in message]}")
print(f"Encrypted: {[hex(b) for b in encrypted]}")
print(f"Decrypted: {[chr(b) for b in decrypted]}")
print()

# === 4. Chess Bitboards ===
# A standard chessboard has 64 squares — fits in a 64-bit integer.
# Each piece type gets its own 64-bit mask.
# White pawns on starting rank (rank 2 = bits 8-15):
white_pawns = 0b0000000000000000000000000000000000000000000000001111111100000000

print("White pawns starting position:")
for rank in range(7, -1, -1):
    row = (white_pawns >> (rank * 8)) & 0xFF
    print(f"  Rank {rank+1}: {row:08b}")
print()

# === 5. Unix File Permissions ===
# rwxrwxrwx is stored as a 9-bit integer
OWNER_R = 0o400
OWNER_W = 0o200
OWNER_X = 0o100
GROUP_R = 0o040
GROUP_W = 0o020
GROUP_X = 0o010
OTHER_R = 0o004
OTHER_W = 0o002
OTHER_X = 0o001

# chmod 755 (rwxr-xr-x)
perms = OWNER_R | OWNER_W | OWNER_X | GROUP_R | GROUP_X | OTHER_R | OTHER_X
print(f"chmod 755:")
print(f"  Octal:  {oct(perms)}")
print(f"  Binary: {perms:09b}")
print(f"  owner write? {'yes' if perms & OWNER_W else 'no'}")
print(f"  group write? {'yes' if perms & GROUP_W else 'no'}")
print(f"  other write? {'yes' if perms & OTHER_W else 'no'}")

Summary Table

TrickCodeUse Case
Check odd/evenn & 1Parity, loop branching
Clear lowest set bitn & (n-1)Counting bits, power-of-2 check
Check power of 2n > 0 and n & (n-1) == 0Alignment checks
Set bit kn | (1 << k)Grant permissions, set flags
Clear bit kn & ~(1 << k)Revoke permissions, clear flags
Toggle bit kn ^ (1 << k)Flip a flag
Check bit k(n >> k) & 1Read a flag
Multiply by 2^kn << kFast scaling
Divide by 2^kn >> kFast downscaling, extracting channels
XOR swapa^=b; b^=a; a^=bNo temp variable needed