Bitwise operations: AND, OR, XOR, NOT and shifts
Computers store integers as bits: a sequence of zeroes and ones. Most of the time we can ignore that representation and use ordinary arithmetic. Bitwise operations are useful when the individual bits are the data—for example, when working with permissions, network protocols, colour channels, hardware registers or compact encodings.
Python has six commonly used bitwise operators:
| Operation |
Operator |
Binary example |
Integer example |
| AND |
& |
1100 & 1010 == 1000 |
12 & 10 == 8 |
| OR |
| |
1100 | 1010 == 1110 |
12 | 10 == 14 |
| XOR |
^ |
1100 ^ 1010 == 0110 |
12 ^ 10 == 6 |
| NOT |
~ |
1100 becomes 0011 |
~12 == -13 |
| Left shift |
<< |
1100 << 1 == 11000 |
12 << 1 == 24 |
| Right shift |
>> |
1100 >> 1 == 0110 |
12 >> 1 == 6 |
The easiest way to understand them is to write the operands in binary. We will use four bits for the positive values in the examples:
Binary numbers in Python
In the diagrams and table above, bare sequences such as 1100 are binary bit patterns. Python source code needs the 0b prefix to distinguish a binary integer from an ordinary decimal integer:
binary = 0b1100
decimal = 12
assert binary == decimal
The prefix changes how we write the integer, not the value Python stores. Python's built-in bin() function converts an integer to a string using the same notation:
A binary integer literal is different from a bytes object. A bytes object is a sequence of whole byte values, where each byte contains eight bits:
data = bytes([0b00001100])
len(data) # 1 byte
data[0] # 12
AND: keep selected bits
AND produces 1 only when the bit in both operands is 1:
| A |
B |
A AND B |
| 0 |
0 |
0 |
| 0 |
1 |
0 |
| 1 |
0 |
0 |
| 1 |
1 |
1 |
Applied to our values:
1100 (12)
& 1010 (10)
------
1000 (8)
AND is often used with a mask: a value whose 1 bits mark the positions we want to keep. To retrieve the lowest four bits of any integer, use the mask 1111:
value = 0b10110110
mask = 0b00001111
lowest_four_bits = value & mask # 0b00000110, or 6
The zeroes in the mask clear the upper bits. The ones allow the lower bits through unchanged.
OR: set bits
OR produces 1 when either bit—or both bits—is 1:
| A |
B |
A OR B |
| 0 |
0 |
0 |
| 0 |
1 |
1 |
| 1 |
0 |
1 |
| 1 |
1 |
1 |
1100 (12)
| 1010 (10)
------
1110 (14)
This makes OR useful for turning selected bits on without disturbing the others. Imagine that each bit represents an application permission:
READ = 0b001
WRITE = 0b010
EXECUTE = 0b100
permissions = READ | WRITE # 0b011
permissions |= EXECUTE # 0b111
We can then use AND to test whether a particular flag is present:
if permissions & WRITE:
print("Writing is allowed")
XOR: toggle bits
Exclusive OR produces 1 when the bits are different, but 0 when they are the same:
| A |
B |
A XOR B |
| 0 |
0 |
0 |
| 0 |
1 |
1 |
| 1 |
0 |
1 |
| 1 |
1 |
0 |
1100 (12)
^ 1010 (10)
------
0110 (6)
XOR with 1 flips a bit, while XOR with 0 leaves it alone. That makes a mask useful for toggling flags:
settings = 0b1010
settings ^= 0b0010 # 0b1000: the second bit is now off
settings ^= 0b0010 # 0b1010: applying it again restores it
XOR is reversible: (value ^ key) ^ key == value. This property appears in checksums, error-correcting codes and cryptographic algorithms, although XOR with a repeated key is not secure encryption by itself.
NOT: invert bits
NOT flips every bit:
There is an important catch: an integer does not have a fixed width in Python. Python behaves as though signed integers have an unlimited number of sign bits, so ~x is equal to -(x + 1):
If we specifically want to invert a four-bit value, we can apply a four-bit mask afterwards:
(~0b1100) & 0b1111 # 0b0011, or 3
NOT is also handy for clearing flags. AND with an inverted mask preserves every bit except the ones selected by that mask:
Shifting bits left and right
A left shift moves every bit to the left and fills the newly opened positions on the right with zeroes:
0011 << 2 = 1100
^^
two new zeroes
For non-negative integers, shifting left by n positions is equivalent to multiplying by 2 ** n:
The written result may gain up to n significant bits, but it does not always do so. Leading zeroes do not contribute to an integer's value, and zero remains zero however far it is shifted:
1011 << 2 = 101100
0001 << 2 = 000100 (the value 4 needs only 3 significant bits: 100)
0000 << 2 = 000000 (still 0)
Python integers do not have a fixed width, so a left shift does not overflow merely because the result needs more bits. This differs from fixed-width integer types in languages where shifting may discard bits that no longer fit.
What happens during a right shift?
A right shift moves every bit to the right. Bits that move beyond the right-hand end are discarded. Those bits are lost from the result.
11[01] >> 2 = 0011
^^
the rightmost bits are discarded
For a non-negative integer, zeroes fill the newly opened positions on the left. Those leading zeroes are useful in a fixed-width diagram, but they are not part of the integer's minimal representation: bin(3) returns 0b11, not 0b0011, and printing the integer itself displays 3.
For non-negative integers, shifting right by n positions is equivalent to floor division by 2 ** n:
13 = 1101
13 >> 2 = 0011 = 11 # 3
13 // (2 ** 2) # 3
The discarded bits explain why this is floor division rather than exact division. 1101 is 13; its discarded 01 represents the remainder 1, leaving 0011, or 3.
Does >> n always remove n bits?
It moves the pattern right by exactly n positions, but the number of significant bits does not always decrease by exactly n:
1101 >> 2 = 0011 (13 becomes 3: 4 significant bits become 2)
1000 >> 2 = 0010 (8 becomes 2: 4 significant bits become 2)
0010 >> 2 = 0000 (2 becomes 0: it cannot become shorter than zero)
0000 >> 2 = 0000 (0 remains 0)
If n is at least the number of significant bits in a positive integer, all its 1 bits are discarded and the result is zero:
5 >> 3 # 0: 101 has been shifted completely away
5 >> 100 # 0
There is one further detail for negative Python integers. A right shift fills the left with 1 sign bits rather than zeroes, preserving the negative sign. This is called an arithmetic right shift:
-12 >> 2 # -3
-13 >> 2 # -4, the same as -13 // 4
Unless signed values are specifically involved, it is simplest to picture right shifts using non-negative integers: discard bits on the right and introduce zeroes on the left.
Shifts become especially useful when a larger integer contains several smaller values. A left shift makes empty space for a value; a right shift moves a value into a position where we can read it.
Building masks
Writing a mask in binary is clear when it is small, but we can generate a mask of any width. Start with 1, shift it left by the required width, then subtract one:
width = 4
mask = (1 << width) - 1
print(f"{mask:04b}") # 1111
Why does this work? 1 << 4 is 10000. Subtracting one turns the lower four positions into ones: 01111.
This pattern brings AND, OR and shifts together. It is also the key to the following bit-fiddling example.
Example: packing a string into bits
Suppose we want to encode a string containing only a small set of characters. A normal UTF-8 string uses one byte for each of the ASCII characters below. If there are no more than 16 distinct characters, however, each character can be represented by a number from 0 to 15—and that requires only four bits.
The following program builds a mapping for the characters it encounters, packs their numeric values into one integer and finally converts that integer to bytes. Decoding performs the operations in reverse:
import math
from typing import Any
def get_mapping_from_chars(characters: set[str]) -> dict[str, int]:
return {character: i for i, character in enumerate(sorted(characters))}
def get_reverse_mapping(mapping: dict[str, int]) -> dict[int, str]:
return {value: key for key, value in mapping.items()}
def get_bits_per_character(mapping: dict[Any, Any]) -> int:
"""Return the number of bits required to encode a single character."""
max_value = len(mapping) - 1
return math.ceil(math.log2(max_value + 1))
def get_bytecount_for_string(input_string: str, mapping: dict[str, int]) -> int:
"""Return the number of bytes required to encode the given string."""
bits_per_character = get_bits_per_character(mapping)
total_bits = len(input_string) * bits_per_character
return math.ceil(total_bits / 8)
def encode_string(input_string: str, mapping: dict[str, int]) -> bytes:
"""Encode a string into bytes using the provided mapping."""
value = 0
bits_per_character = get_bits_per_character(mapping)
for char in input_string:
value = value << bits_per_character
value = value | mapping[char]
byte_count = get_bytecount_for_string(input_string, mapping)
return value.to_bytes(byte_count)
def decode_bytes(
encoded: bytes,
character_count: int,
reverse_mapping: dict[int, str],
) -> str:
value = int.from_bytes(encoded)
bits_per_character = get_bits_per_character(reverse_mapping)
mask = (1 << bits_per_character) - 1
characters = []
for _ in range(character_count):
character_bits = value & mask
characters.append(reverse_mapping[character_bits])
value = value >> bits_per_character
return "".join(reversed(characters))
input_string = "ABCDABCEGEHSKEJWOIEWQOJQWOEJOVOI!"
mapping = get_mapping_from_chars(set(input_string))
reverse_mapping = get_reverse_mapping(mapping)
encoded = encode_string(input_string, mapping)
decoded = decode_bytes(encoded, len(input_string), reverse_mapping)
assert decoded == input_string
print(encoded)
print(decoded)
There are exactly 16 different characters in this input, so each mapped value occupies four bits. Encoding one character works like this:
value << 4 shifts the existing data left, making four empty bits at the end.
value | mapping[char] inserts the new character's value into those empty bits.
- Repeating the process joins all the four-bit values into a single integer.
Decoding starts at the other end:
(1 << 4) - 1 creates the mask 1111.
value & mask retrieves the final four-bit character value.
value >> 4 discards that character and brings the next one into position.
- Characters are extracted from right to left, so the final list is reversed.
The 33-character ASCII string normally occupies 33 bytes. Its packed character data occupies 132 bits, rounded up to 17 bytes. That comparison excludes the mapping and character count, which a self-contained file or network message would also need to store. This technique therefore pays off when the alphabet is known in advance or when enough data shares the same mapping.
That is bit fiddling in practice: shifts position the data, OR puts it in place, and a mask combined with AND takes it out again.