Skip to content
Back to Blog
Tutorials

Bitwise Operations Explained: AND, OR, XOR, Shifts, and Masks

Master bitwise operations with hands-on examples: AND, OR, XOR, shifts, two's complement, bitmasks, and feature flags, with code in JS, Python, Go, and C.

17 min read

Bitwise Operations in Practice: AND, OR, XOR, Shifts, Masks

You open a legacy PostgreSQL migration and see permissions & 0b100. A colleague ships a feature flag system that packs 32 booleans into a single integer. A Kubernetes subnet calc spits out 192.168.1.0/24 and you need to extract the network address in code. Three situations, one underlying skill: bitwise operations.

Most application-layer developers never need to reach for & or ^ in a web app, until suddenly they do. This guide walks through the six bitwise operators, two’s complement, nine patterns worth memorizing, and the language-specific traps that will bite you (especially in JavaScript). Code is in JS, Python, Go, and C, and every example is runnable.

Open our Base Converter in another tab. Several sections invite you to type in a number and watch the bit pattern change.

Why bitwise operations still matter in 2026

High-level languages have not made bitwise operations obsolete. They have just hidden where the operations happen. A few places you are relying on them today, whether you realize it or not:

  • PostgreSQL row-level security uses a bitmap of ACL privileges (SELECT, INSERT, UPDATE, DELETE, …) packed into an integer.
  • Linux capabilities replace the old root-or-nothing model with 40+ permission bits you combine with |.
  • JWT algorithm headers encode the hash algorithm in a small field where bit-level comparison is common at the library layer.
  • Snowflake, ULID, and UUIDv7 pack timestamp, machine ID, and sequence number into a single 64-bit or 128-bit integer using left shifts.
  • Redis BITCOUNT and BITOP expose bitwise primitives directly to application code for cardinality estimation and A/B bucketing.
  • Image processing reads 32-bit RGBA pixels and extracts channels with & and >>.

Bitwise operations remain O(1) at the CPU instruction level. When you pack 32 booleans into one integer, you save 31 bytes of memory, and (more importantly) you can check “any of these 32 flags set” in a single != 0 test.

Binary foundations you need first

This guide assumes you already know how binary works. If you need a refresher, read our Number Base Conversion Guide first and come back.

A quick vocabulary check before we start:

  • A bit is a 0 or a 1.
  • A nibble is 4 bits (one hex digit).
  • A byte is 8 bits.
  • A word is typically 32 or 64 bits, depending on your CPU.

Integers in most languages come in fixed widths: 8, 16, 32, 64. The width matters a lot for bitwise operations because shifts can push bits off the end, and the sign bit sits at the leftmost position of signed integers.

Try this now. Open the Base Converter, enter 170 as decimal, and look at the binary output. You should see 10101010, an alternating pattern we will come back to several times below.

The six bitwise operators

Every mainstream language gives you the same six operators, sometimes with slight syntax differences. The symbols &, |, ^, ~, <<, >> work in JavaScript, Python, Go, Rust, C, C++, Java, and C# unchanged. JavaScript adds one extra: >>>, the unsigned right shift.

AND (&): bit filter

The output bit is 1 only if both input bits are 1.

ABA & B
000
010
100
111

Think of AND as a gate: only bits that are set in both operands survive. The most common use is masking, keeping some bits and zeroing others.

// Extract the low 4 bits (the rightmost nibble)
const value = 0b11010110;   // 214
const low4  = value & 0x0F; // 0b00000110 = 6

// Check if a number is odd
const isOdd = (n) => (n & 1) === 1;
isOdd(7);  // true
isOdd(42); // false
# Same in Python
value = 0b11010110
low4 = value & 0x0F  # 6

def is_odd(n):
    return (n & 1) == 1

OR (|): bit setter

The output bit is 1 if either input bit is 1.

ABA | B
000
011
101
111

OR combines flags. If you have READ = 1, WRITE = 2, EXECUTE = 4, then READ | WRITE is 3, with both permissions enabled.

const READ  = 0b001;
const WRITE = 0b010;
const EXEC  = 0b100;

const rw = READ | WRITE;  // 0b011 = 3
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE  # 3

XOR (^): bit toggle

The output bit is 1 if the input bits differ.

ABA ^ B
000
011
101
110

XOR has three algebraic properties that power some of the cleverest tricks in computer science:

  • a ^ a = 0: anything XOR’d with itself cancels.
  • a ^ 0 = a: XOR with zero is the identity.
  • a ^ b ^ a = b: XOR is its own inverse.

The last property is why XOR shows up in parity checks, stream ciphers, and the notorious “find the single non-duplicated number in an array” interview question.

// Find the one unique number in an array where every other number appears twice
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]);  // 4
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2])  # 4

NOT (~): bit inverter

Unary ~ flips every bit: 0 becomes 1, 1 becomes 0.

~0b00001111  // -16  (JavaScript coerces to 32-bit signed)
~5           // -6
~5  # -6
// Go uses ^ as unary bitwise NOT, watch out
var x int8 = 5
fmt.Println(^x)  // -6

The result of ~5 is -6 in every mainstream language, and this surprises beginners. The reason is two’s complement, which we cover in the next section. For now, just know that ~x equals -(x + 1) in any language that uses two’s complement for negatives (which is all of them).

Left shift (<<): power-of-two multiplier

x << n shifts every bit of x to the left by n positions, filling zeros on the right. Mathematically, this multiplies by 2ⁿ.

1 << 0   // 1   (2^0)
1 << 1   // 2   (2^1)
1 << 3   // 8   (2^3)
1 << 10  // 1024 (2^10 = 1 KiB)

// Building bit flags
const FLAG_ADMIN    = 1 << 0;
const FLAG_EDITOR   = 1 << 1;
const FLAG_REVIEWER = 1 << 2;

The handy thing about 1 << n is that it creates a number with a single bit set at position n. That bit becomes a flag.

Watch out for overflow. In JavaScript, 1 << 31 is -2147483648 (not 2147483648) because JavaScript bitwise operators work on 32-bit signed integers.

Right shift (>> vs >>>): divide or padded?

Right shift moves bits to the right. The question is what fills the vacated leftmost positions.

  • >> (arithmetic right shift) preserves the sign bit. Negative numbers stay negative.
  • >>> (logical or unsigned right shift) fills with zeros. Only JavaScript has this as a dedicated operator.
-8 >> 1   // -4   (sign bit preserved)
-8 >>> 1  // 2147483644  (sign bit treated as a data bit)

8 >> 1    // 4
8 >> 2    // 2

In C, whether >> is arithmetic or logical for signed types is implementation-defined. Most compilers do arithmetic, but do not rely on this without checking. Go requires shift amounts to be unsigned integers and treats signed and unsigned types explicitly. Python has no >>> because it has no fixed-width integers.

Two’s complement: how computers represent negatives

If bits are just 0 and 1, how do you encode -5? The answer the world settled on in the 1960s is two’s complement, and every modern CPU uses it.

The naive approach (reserve one bit for the sign) has two problems. First, you end up with both +0 and -0, which is awkward. Second, addition and subtraction circuits have to check the sign bit, making the hardware more complex. Two’s complement solves both.

The rule is short:

  1. Take the positive binary representation.
  2. Flip every bit (that is the “one’s complement”).
  3. Add 1.

Worked example, encoding -5 in 8-bit two’s complement:

 5 in binary:        0000 0101
 flip all bits:      1111 1010   (this is -6 in two's complement!)
 add 1:              1111 1011   ← this is -5

Verify with our base converter: input 251 (decimal) into the Base Converter with base 10, and the binary output is 11111011. In an 8-bit signed context, 11111011 is -5. In an 8-bit unsigned context, the same bit pattern is 251. The bits are identical; the interpretation differs.

This explains the earlier ~5 = -6 surprise. Bitwise NOT inverts bits, which gives you one’s complement. Two’s complement is one’s complement plus 1. So:

~x   = -(x + 1)        // identity in any two's complement language
~5   = -6
~(-3) = 2

For n-bit signed integers, the representable range is -2ⁿ⁻¹ to 2ⁿ⁻¹ − 1. An 8-bit signed integer covers -128 to 127. A 32-bit signed integer covers roughly -2.1 billion to +2.1 billion.

Essential bit manipulation patterns

These nine patterns cover maybe 95% of the bit manipulation you will ever write. Memorize them and you will recognize them everywhere in systems code.

Set a bit: x | (1 << n)

Turn bit n on, leave other bits unchanged.

let flags = 0b0100;
flags = flags | (1 << 0);  // 0b0101

Clear a bit: x & ~(1 << n)

Turn bit n off, leave other bits unchanged. ~(1 << n) is a mask with every bit set except bit n.

let flags = 0b0111;
flags = flags & ~(1 << 1);  // 0b0101

Toggle a bit: x ^ (1 << n)

Flip bit n regardless of its current state.

let flags = 0b0100;
flags = flags ^ (1 << 2);  // 0b0000
flags = flags ^ (1 << 2);  // 0b0100 again

Check a bit: (x >> n) & 1

Returns 1 if bit n is set, 0 otherwise. Equivalent form: (x & (1 << n)) !== 0.

const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1;  // 1

Isolate lowest set bit: x & -x

Produces a value with only the rightmost 1 bit of x kept. The trick works because -x in two’s complement is ~x + 1, which flips every bit up to and including the lowest set bit.

const x = 0b10110100;
const lowest = x & -x;  // 0b00000100 = 4

This is the core trick inside Fenwick trees (Binary Indexed Trees) for O(log n) prefix sums.

Count set bits (popcount)

Counting the number of 1 bits in an integer. Most languages now have a native function:

// JavaScript (BigInt or manual)
const popcount = (n) => {
  let count = 0;
  while (n) { count += n & 1; n >>>= 1; }
  return count;
};
popcount(0b10110100);  // 4
# Python 3.10+
(0b10110100).bit_count()  # 4
// Go
import "math/bits"

bits.OnesCount(0b10110100) // 4

XOR swap without a temp variable

A classic party trick: swap two integers without a third variable. Never use this in production (it is slower than a temp variable and breaks if a and b alias the same memory location), but it is worth understanding.

let a = 5, b = 9;
a = a ^ b;  // a = 5 ^ 9
b = a ^ b;  // b = (5 ^ 9) ^ 9 = 5
a = a ^ b;  // a = (5 ^ 9) ^ 5 = 9
// a = 9, b = 5

Detect power of two: (x & (x - 1)) === 0

A power of two has exactly one bit set. Subtracting 1 flips that bit off and sets every lower bit. ANDing gives zero only for powers of two (and 0 itself, so guard with x > 0).

const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16);  // true
isPow2(17);  // false

Fast oddness check: x & 1

Faster than x % 2 in some languages, identical in others after compiler optimization. Worth it in hot loops or when readability does not matter.

const isOdd = (x) => (x & 1) === 1;

Bitmask flags in real code

The patterns above show up in production code every day. Here are four places you will meet them.

Feature flags in 32 booleans

Instead of a 32-field struct of booleans, pack them into one integer:

const FLAGS = {
  DARK_MODE:      1 << 0,
  NEW_NAV:        1 << 1,
  AI_SUGGESTIONS: 1 << 2,
  BETA_EDITOR:    1 << 3,
  // ... up to 1 << 31
};

let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS;  // opt in

if (userFlags & FLAGS.AI_SUGGESTIONS) {
  showSuggestions();
}

userFlags &= ~FLAGS.DARK_MODE;  // opt out

This stores 32 booleans in 4 bytes and lets you query any subset with a single AND. Databases love this pattern because it is one column instead of 32.

Unix file permissions

chmod 755 is bitwise. The three octal digits map to three triples of bits:

7 = 111  (owner:   rwx)
5 = 101  (group:   r-x)
5 = 101  (others:  r-x)

Try it: open the Base Converter, set source to octal, enter 755, and look at the binary output 111101101. That is literally how the filesystem stores the permission field.

Setting only “group write”:

const perms = 0o755;
const withGroupWrite = perms | 0o020;  // 0o775

IP subnet masking

Given 192.168.1.10/24, extract the network address by ANDing with the mask:

const ip      = 0xC0A8010A;  // 192.168.1.10
const mask    = 0xFFFFFF00;  // 255.255.255.0 (/24)
const network = ip & mask;   // 0xC0A80100 = 192.168.1.0

Packed IDs: Snowflake

Twitter’s Snowflake packs timestamp, machine ID, and sequence into a 64-bit integer:

┌─ 1 bit ─┬─── 41 bits ───┬─ 10 bits ─┬─ 12 bits ─┐
│  sign   │   timestamp   │ machine   │   seq     │
└─────────┴───────────────┴───────────┴───────────┘

Encoding an ID is two shifts and two ORs:

const id = (BigInt(timestamp) << 22n) |
           (BigInt(machineId) << 12n) |
            BigInt(sequence);

Decoding is the reverse: right shift and mask. For a full walkthrough of when to pick Snowflake vs ULID vs UUIDv7, see our distributed ID comparison.

Cross-language gotchas

JavaScript: the 32-bit coercion trap

JavaScript converts operands to 32-bit signed integers before every bitwise operation, then converts the result back to a Number. Any value above 2³¹ − 1 = 2147483647 overflows:

2147483647 | 0   // 2147483647   (still fine)
2147483648 | 0   // -2147483648  (overflowed!)
4294967295 | 0   // -1           (all bits set, interpreted signed)

For 64-bit work, use BigInt. It has independent bitwise operators with no width limit:

(2n ** 40n) | 1n  // 1099511627777n

Operator precedence bugs

This is one of the most common real-world bitwise bugs:

// Buggy: reads as (x & (1 == 0)) because == binds tighter than &
if (x & 1 == 0) { /* ... */ }

// Correct: parenthesize
if ((x & 1) == 0) { /* ... */ }

Comparison operators bind tighter than bitwise AND/OR/XOR in C, JavaScript, Python, Go, and most descendants. Parenthesize when in doubt.

Language comparison table

LanguageWidth coercionNegative >>BigInt support
JavaScriptForces 32-bit signed; >>> is unsignedarithmeticBigInt has separate operators
PythonArbitrary precision; no fixed widtharithmeticNative
GoStrict; shift amount must be unsignedarithmetic for signed typesmath/big
C/C++Follows type; int, unsigned, etc.implementation-defined for signedNone built in
RustStrict; panics on overflow in debugarithmetic for signed typesu128 / external crates

Python’s infinite-width twist

Python integers have no fixed width, so two’s complement logic extends “infinitely” to the left. That is why ~5 is -6 (not 250 or 65530): Python treats the result as a negative integer, not a fixed-width bit pattern. If you need wrap-around semantics, mask explicitly:

# Simulate 8-bit NOT
(~5) & 0xFF  # 250

Performance reality check in 2026

The common lore is that bitwise operations are “always faster.” In 2026, that is half true.

Compilers already do the obvious rewrites. Modern optimizers turn x * 2 into x << 1 automatically. Writing x << 1 in application code for speed is cargo-cult performance tuning. It does not help, and it hurts readability.

Where bitwise code genuinely wins:

  • Hot loops in numeric code: popcount, leading and trailing zero counts, bitboard chess engines.
  • Compact data structures: Bloom filters, roaring bitmaps, Fenwick trees.
  • Hardware registers and memory-mapped I/O: embedded code, kernels, firmware.
  • Cryptography primitives: AES, ChaCha20, and SHA are all built from XOR, rotates, and shifts.
  • Compression and decompression: Huffman coding, run-length, packed integers.
  • Database engines: bitmap indexes, packed column formats like Parquet dictionary encoding.

Where it does not help: replacing x % 2 with x & 1 in a business-logic function that runs twice per request. The speedup is unmeasurable; the readability cost is real.

The one case where bit manipulation always wins is memory footprint. Packing 32 flags into an int saves 31 bytes compared to 32 booleans. At scale (millions of user records, billions of events) that is the difference between a cache-friendly layout and a workload that thrashes L2.

Quick reference cheat sheet

OperationOperatorExampleResultTypical Use
AND&0b1100 & 0b10100b1000Mask/extract bits
OR|0b1100 | 0b10100b1110Combine flags
XOR^0b1100 ^ 0b10100b0110Toggle / detect diff
NOT~~0b1100...11110011Invert for mask
Left shift<<1 << 38Multiply by 2ⁿ
Right shift>>16 >> 24Divide by 2ⁿ (signed)
Unsigned right shift (JS)>>>-1 >>> 04294967295Treat as unsigned
Set bit n|x | (1 << n)Turn bit on
Clear bit n& ~x & ~(1 << n)Turn bit off
Toggle bit n^x ^ (1 << n)Flip bit
Check bit n&(x >> n) & 10 or 1Test bit
Lowest set bit& -x & -xIsolate bit
Is power of 2&x > 0 && (x & (x-1)) == 0boolTest power

FAQ

What’s the difference between logical (&&) and bitwise (&) AND?

Logical AND works on whole boolean values and short-circuits, so false && expr never evaluates expr. Bitwise AND works on individual bits of integers and always evaluates both sides. Use && for conditions, & for bit manipulation.

Why does ~1 equal -2 in most languages?

Bitwise NOT on 1 flips every bit to produce the one’s complement. In two’s complement integer representation, flipping all bits of x gives -(x + 1), so ~1 equals -2, ~0 equals -1, and ~(-1) equals 0. This identity holds in JavaScript, Python, Go, C, Rust, and every other language that stores signed integers in two’s complement.

Is x << 1 really faster than x * 2?

Not in practice. Every modern compiler recognizes x * 2 and emits the same shift instruction at the machine level, so benchmarks show no measurable difference on x86 or ARM. Use x * 2 for readability; reserve << for cases where you are intentionally thinking in bits, such as building a bitmask or packing structured IDs.

Does JavaScript support 64-bit bitwise operations?

JavaScript does not support 64-bit bitwise operations with the standard &, |, ^, <<, >> operators, because those force operands to 32-bit signed integers before the operation runs. For 64-bit or larger values, use BigInt literals such as 1n << 40n, which give arbitrary-precision bitwise operations with their own matching operators.

How do I count the number of set bits efficiently?

Use your language’s built-in: bits.OnesCount in Go, Integer.bitCount in Java, .bit_count() in Python 3.10+, popcount intrinsics in C/C++. These map to a single POPCNT CPU instruction on modern x86 and ARM.

When should I use bitmask flags instead of a struct of booleans?

Use bitmask flags when you need to store many booleans compactly (databases, network protocols, file formats) or test combinations quickly with a single AND such as flags & REQUIRED_MASK. Prefer a struct of booleans when fields have different types, when you need descriptive debug output, or when readability matters more than a few bytes of memory.

What happens when I shift by more than the bit width?

Undefined in C/C++. In JavaScript, the shift count is taken mod 32, so 1 << 32 is 1, not 0. In Python, there is no width, so 1 << 100 is just a larger integer. Never rely on overshift behavior; mask the shift count yourself if needed.

Why does Python’s ~5 give -6 instead of 2?

Python integers have no fixed width, so two’s complement extends conceptually to infinity. ~5 equals -(5 + 1) = -6, same as every other two’s complement language. If you want the 8-bit “inverted” value 250, mask: (~5) & 0xFF.

Is XOR encryption secure?

A one-time pad with a truly random key as long as the message is information-theoretically unbreakable. Reusing the same key across messages is catastrophically insecure, and standard XOR “encryption” with a short repeating key is trivially breakable. Real ciphers like AES and ChaCha20 use XOR internally, but as one step among many.

How do I represent a negative number using two’s complement by hand?

Write the positive value in binary at the target width, flip every bit, then add 1. Example: -5 in 8 bits = 00000101 → flip to 11111010 → add 1 → 11111011. Verify with our Base Converter by converting 251 (the unsigned interpretation of 11111011) and confirming you get 11111011.

Related Articles

View all articles