For a long time, I avoided bitwise operators in Python entirely. They felt like a low-level C concept that had no business in a high-level language. That changed once I started working on flag systems, performance-sensitive code, and a few embedded-adjacent projects where manipulating individual bits was the cleanest solution available. This guide covers everything I’ve picked up about bitwise operators in Python — how they work, how they’re implemented, and where they genuinely earn their place in real code.
What Bitwise Operators Do
Bitwise operators work directly on the binary representation of integers, manipulating individual bits rather than the number as a whole. Python provides six of them:
| Operator | Name | Description |
|---|---|---|
& | AND | 1 if both bits are 1 |
| | OR | 1 if either bit is 1 |
^ | XOR | 1 if bits differ |
~ | NOT | Inverts all bits |
<< | Left shift | Shifts bits left, filling with 0 |
>> | Right shift | Shifts bits right |
a = 12 # binary: 1100
b = 10 # binary: 1010
print(a & b) # 8 -> 1000
print(a | b) # 14 -> 1110
print(a ^ b) # 6 -> 0110
print(~a) # -13 -> two's complement inversion
print(a << 2) # 48 -> 110000
print(a >> 2) # 3 -> 0011
Viewing Binary Representations
To actually see what’s happening, I use Python’s built-in bin() function:
print(bin(12)) # '0b1100'
print(bin(10)) # '0b1010'
print(bin(12 & 10)) # '0b1000'
I can also format numbers with padded binary using f-strings:
a = 12
print(f"{a:08b}") # '00001100'
How Each Operator Works, Bit by Bit
AND (&) — Only bits that are 1 in both numbers stay 1:
1100
& 1010
------
1000
OR (|) — Any bit that’s 1 in either number becomes 1:
1100
| 1010
------
1110
XOR (^) — Bits that differ become 1, matching bits become 0:
1100
^ 1010
------
0110
NOT (~) — Flips every bit. In Python, integers are conceptually infinite in precision and signed, so ~x is equivalent to -x - 1, not a simple bit flip of a fixed-width number like in C.
print(~5) # -6
print(~0) # -1
print(~-1) # 0
Left shift (<<) — Shifts bits left, effectively multiplying by 2^n:
print(5 << 1) # 10 (5 * 2)
print(5 << 3) # 40 (5 * 8)
Right shift (>>) — Shifts bits right, effectively performing floor division by 2^n:
print(20 >> 1) # 10 (20 // 2)
print(20 >> 2) # 5 (20 // 4)
Python’s Integer Representation and Why ~ Feels Unusual
Unlike C or Java, Python integers aren’t stored in a fixed number of bits (like 32 or 64). CPython implements arbitrary-precision integers, and negative numbers are handled conceptually using an infinite two’s-complement representation. That’s why ~5 gives -6 rather than some large unsigned number — Python is modeling an infinite sign-extended bit pattern, not a fixed-width register.
This has real implications for anyone porting bit-manipulation code from C: masking is often necessary to constrain results to a specific bit width.
def to_8bit(n):
return n & 0xFF
print(to_8bit(~5)) # 250, simulating 8-bit unsigned wraparound
Internal Implementation Notes
CPython’s integer objects (PyLongObject) store digits in a variable-length array, meaning bitwise operations on very large integers involve iterating over multiple “digit” chunks internally rather than a single CPU register operation. For typical small integers, Python also uses interned/cached objects for values -5 to 256, but bitwise operations still produce new integer objects since integers are immutable.
Performance-wise, bitwise operations are extremely fast — implemented in C at the interpreter level — but they’re still going through Python’s general dynamic-typing overhead compared to raw C bit manipulation. For truly performance-critical numeric bit work at scale, I reach for NumPy, which supports vectorized bitwise operations on fixed-width integer arrays.
import numpy as np
arr = np.array([12, 10, 7], dtype=np.uint8)
print(arr & 5) # vectorized bitwise AND across the whole array
Common Use Cases: Bit Flags
The most practical use case I’ve run into repeatedly is representing sets of boolean flags compactly using individual bits, instead of multiple separate boolean variables.
READ = 1 # 0001
WRITE = 2 # 0010
EXECUTE = 4 # 0100
DELETE = 8 # 1000
permissions = READ | WRITE # 0011
print(permissions & READ) # 1 -> True, READ is set
print(permissions & EXECUTE) # 0 -> False, EXECUTE not set
# Adding a permission
permissions |= EXECUTE
print(bin(permissions)) # 0b111
# Removing a permission
permissions &= ~WRITE
print(bin(permissions)) # 0b101
# Toggling a permission
permissions ^= READ
print(bin(permissions)) # 0b100
This pattern shows up in operating system permission systems, game engine state flags, and network protocol headers.
Using Python’s Enum with Flags
Python’s standard library actually formalizes this bit-flag pattern with enum.Flag and enum.IntFlag, which I now prefer over raw integer constants for readability:
from enum import IntFlag, auto
class Permission(IntFlag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
p = Permission.READ | Permission.WRITE
print(p) # Permission.READ|WRITE
print(Permission.READ in p) # True
print(Permission.EXECUTE in p) # False
Bit Masking for Data Extraction
Bitwise operators are essential when parsing binary protocols or file formats, where individual fields are packed into specific bit ranges of a byte or word.
# Suppose a byte encodes: 2 bits version, 3 bits type, 3 bits flags
byte_value = 0b10101110
version = (byte_value >> 6) & 0b11 # top 2 bits
msg_type = (byte_value >> 3) & 0b111 # middle 3 bits
flags = byte_value & 0b111 # bottom 3 bits
print(version, msg_type, flags) # 2 5 6
This kind of manual field extraction shows up constantly in networking code, image format parsing, and hardware interfacing.
Real-World Applications
- Networking: parsing IP headers, subnet masks, and checksums.
- Graphics: manipulating RGBA color channels packed into a single integer.
- Cryptography: XOR-based simple ciphers and hash mixing functions.
- Hashing algorithms: many hash functions rely heavily on shifts and XOR for bit diffusion.
- Compression: bit-packing algorithms store data more compactly using explicit bit manipulation.
# Packing RGB into a single integer
def pack_rgb(r, g, b):
return (r << 16) | (g << 8) | b
def unpack_rgb(value):
r = (value >> 16) & 0xFF
g = (value >> 8) & 0xFF
b = value & 0xFF
return r, g, b
packed = pack_rgb(255, 128, 64)
print(hex(packed)) # 0xff8040
print(unpack_rgb(packed)) # (255, 128, 64)
Best Practices
- Use named constants or
IntFlagenums instead of magic numbers for flag bits. - Always mask (
& 0xFF,& 0xFFFF, etc.) when simulating fixed-width behavior, since Python integers don’t wrap automatically. - Use
bin()and f-string binary formatting liberally when debugging — visualizing the bits saves guesswork. - Prefer
<</>>over* 2/// 2only when the intent is genuinely about bit manipulation; for plain arithmetic, multiplication and division are clearer.
Common Mistakes
# Mistake: assuming ~x flips bits like unsigned fixed-width integers
print(~5) # -6, not some large positive number as in C's unsigned view
# Mistake: forgetting operator precedence with bitwise operators
print(5 & 3 == 3) # This is 5 & (3 == 3) -> 5 & True -> 1, likely unintended
print((5 & 3) == 3) # Correct grouping
Debugging Tips
- Print binary representations at every step:
print(f"{value:08b}"). - Break multi-step bit manipulations into named intermediate variables.
- Use small, known test values first (like
0b1010) before applying logic to real data.
FAQs
Why does ~5 return -6 instead of a large positive number? Python integers are arbitrary-precision and conceptually signed with infinite two’s-complement representation, so ~x always equals -x - 1.
Can I use bitwise operators on floats? No — bitwise operators only work on integers (and booleans, which are a subclass of int). Attempting them on floats raises a TypeError.
What’s the difference between and/or and &/|? and/or are logical operators using short-circuit evaluation on truthy/falsy values; &/| are bitwise operators working on the binary representation of integers (and are also overloaded for set operations).
Are bitwise operations faster than arithmetic equivalents like multiplication? At the CPU instruction level, yes historically, but in Python’s interpreted context the difference is usually negligible for typical code; clarity should guide the choice unless profiling shows otherwise.
Summary
Bitwise operators give me direct control over the binary representation of integers — useful for flags, protocol parsing, and low-level data packing. Python’s arbitrary-precision integer model makes ~ behave differently than in fixed-width languages, which is worth internalizing early. For most everyday Python code these operators are a niche tool, but when the right problem shows up — flags, masks, packed binary data — nothing else is as clean or efficient.