Bitwise Operators for Programmers
AND, OR, XOR, NOT and shifts with truth tables and real uses: masks, flags, checking odd/even and powers of two, plus how C, Python and JS differ.
Bitwise Operators for Programmers
Bitwise operators are the lowest-level tool a programmer can use to manipulate data, working directly on the individual bits that make up integers. Unlike arithmetic operators that treat a number as a single value, bitwise operators treat it as a fixed-length string of binary digits, letting you test, set, or shift those digits with surgical precision. Bitwise operations work as explained here: truth tables for the core operations, the difference between logical and arithmetic shifts, and the common patterns and tricks that make them so valuable in systems programming, graphics, and embedded development. Whether you are working in C, Python, or JavaScript, understanding bitwise operators correctly is the difference between elegant, efficient code and subtle, hard-to-find bugs.
Truth Tables for AND, OR, XOR, and NOT
Every bitwise operation reduces to a fixed rule for each pair of bits, and those rules are captured in truth tables. The bitwise AND operator (&) yields 1 only when both input bits are 1. The bitwise OR operator (|) yields 1 when at least one input bit is 1. The bitwise XOR operator (^), exclusive OR, yields 1 when exactly one input bit is 1, which is what makes it so useful for toggling. The bitwise NOT operator (~), also called bitwise complement, is unary and inverts every bit: 0 becomes 1 and 1 becomes 0.
These tables are identical in every language that supports the operators, because they are defined by Boolean algebra, not by any particular implementation. For example, given two 4-bit values, 1010 and 1100, AND gives 1000, OR gives 1110, XOR gives 0110, and NOT of 1010 gives 0101 in a 4-bit context. The trick is remembering that NOT is not logical negation: in two's complement, ~0 equals -1, not 0, so using it in a conditional like if (~x) tests for x being -1, not for x being zero. That single distinction trips up more programmers than any other operator here.
When you apply these to multi-bit integers, the operation is performed bitwise, meaning the nth bit of the result depends only on the nth bits of the operands. There is no carry between positions, unlike addition, so the result is always exactly as wide as the operands (or wider, if the language promotes them). This independence is why bitwise operators are ideal for constructing bit masks, which are patterns of bits used to select or clear specific fields within a larger value.
Logical vs Arithmetic Shifts
A left shift (<<) always fills the low-order bits with 0, regardless of the operand's sign, because there is no sign to preserve on the right side. In most languages, a left shift by one position multiplies the value by 2, barring overflow. A right shift (>>) is where languages differ. In C, the C17 standard (section 6.5.7) states that right-shifting a negative value is implementation-defined, meaning the result depends on the compiler. JavaScript has both: the signed right shift (>>) preserves the sign bit, while the unsigned right shift (>>>) always fills with 0. Python's right shift (>>) is always arithmetic, because its integers are arbitrarily large and it preserves the sign for negative numbers.
The practical consequence is that you must know whether your value is signed or unsigned before shifting right. A logical shift on a negative number in C will produce a large positive number, while an arithmetic shift gives you a negative result that approximates division. This is why the choice between logical and arithmetic shifts is not a stylistic one, it changes the result, and getting it wrong produces bugs that are invisible until you test with negative inputs.
Bit Manipulation: Set, Clear, Toggle, and Test a Bit
Bit manipulation is the art of using bitwise operators to read or modify a single bit within a larger integer, and it is a core skill for systems programming. To set a bit, meaning force it to 1 without touching the others, use the bitwise OR: x | (1 << n). To clear a bit, force it to 0, use AND with the complement: x & ~(1 << n). To toggle a bit, flip it, use XOR: x ^ (1 << n). To test whether a bit is set, shift the value right by n and AND with 1: (x >> n) & 1, which yields 0 or 1.
These patterns are so common that they appear in nearly every low-level library. A typical use case is a set of boolean flags packed into a single 8-bit or 32-bit integer, where each bit represents one option, and you set or clear them independently. One subtlety: the expression ~0 gives an all-ones value, but its width depends on the integer type, so always mask the complement to the relevant width if you are working with a fixed-size type in C, or you may inadvertently affect higher bits.
A common failure is using bitwise NOT (~) where you meant logical NOT (!). For example, if (~flags) is true when flags is 0, because ~0 is -1 (all bits set), which is non-zero, so the condition is true, exactly the opposite of what a programmer expecting logical negation would intend. Always test with a known value and a debugger when you are first learning these, because the difference between a set bit and a cleared bit is easy to invert.
Bit Masks: Practical Patterns for Selecting and Isolating Bits
A bit mask is any value used to select, clear, or isolate a specific set of bits within a larger integer, and it is the single most reusable concept in bit manipulation. To isolate a field, AND it with a mask that has 1s only in the positions you care about. To clear a field, AND with the complement of the mask. These two operations are the basis of many algorithms, including counting set bits (popcount) and detecting whether a number is a power of two, since a power of two has exactly one set bit, so x & (x - 1) equals 0.
Masks also appear in network programming, where an IP address is paired with a subnet mask to extract the network prefix, and in graphics, where color channels are packed into a 32-bit integer and masks separate red, green, blue, and alpha components. A mask that is off by one bit, such as using 0x7F instead of 0xFF, will silently produce wrong results in a way that is hard to trace without unit tests that cover boundary values like all-ones and all-zeros. A left shift by n bits multiplies a non-negative integer by 2^n, and a right shift by n bits divides by 2^n, with the caveat that right shift on signed values may be arithmetic or logical depending on the language. The right shift of a non-negative value is floor division by 2^n, but for negative values it is implementation-defined, so on one compiler -8 >> 1 might give -4, while on another it could give a large positive number.
Common Bit Tricks and Their Use Cases
Programmers reach for bitwise operators even when a straightforward arithmetic approach exists, because they replace multiple branches with a single expression. Testing whether a number is odd or even is the simplest: x & 1 is 1 for odd and 0 for even, since the least significant bit is the only one that affects the result. Checking whether a number is a power of two uses x > 0 && (x & (x - 1)) == 0, which works because a power of two has exactly one set bit, so subtracting 1 flips all lower bits and the AND becomes zero.
Swapping two values without a temporary variable is the most famous bit trick, using XOR three times: x = x ^ y; y = x ^ y; x = x ^ y;. This works because XOR is its own inverse, so applying the same operation twice restores the original value, and it is safe when x and y are distinct variables, though not when they refer to the same memory location. Another useful pattern is computing absolute value in two's complement without branching: abs = (x + (x >> (n-1))) ^ (x >> (n-1)), where n is the bit width, though this only works for signed integers and overflows for the most negative value. These tricks are not just academic exercises, they appear in hash table sizing, random number generators, and compression algorithms.
Language-Specific Quirks and Portability
Bitwise behavior varies across languages, and code written for one may misbehave in another, even when the underlying binary arithmetic is the same. In C, the width of the operation is the width of the promoted operands, and signed right shifts are implementation-defined for negative values, so portable code must cast to unsigned before shifting. JavaScript forces all bitwise operations to 32-bit signed integers, so the result is always a signed 32-bit integer even if the original value was a double. Python has arbitrary-precision integers, so there is no overflow, but the bitwise NOT of any integer x is always -(x+1), which means ~5 is -6, and the right shift always fills with the sign bit, making it arithmetic.
These differences matter when you are porting a hash function, a checksum, or a random number generator that relies on fixed-width behavior. In JavaScript, the lack of a 64-bit integer type in the original standard meant that bitwise operations on numbers above 2^31 were lossy, though BigInt now provides a workaround. In C, the undefined behavior for left-shifting a negative value means the compiler is allowed to assume it never happens, which can lead to surprising optimizations that break code relying on two's complement wrap-around. The C17 standard is explicit: right shift of a negative value is implementation-defined, and left shift of a negative value is undefined behavior, so the result can be anything, including a program that crashes.
When you are debugging a bitwise operation that behaves differently than expected, the first thing to check is the type and signedness of your operands, and the second is the width. A common error is assuming that 0xFFFFFFFFThese are not bugs in the language, they are consequences of how each language defines integer semantics, and understanding them is the difference between code that works and code that fails only on certain inputs.
Frequently Asked Questions
What is the result of ~0 in two's complement, and why does it trip up programmers?
In two's complement, ~0 equals -1, not 0. This trips up programmers because using it in a conditional like if (~x) tests for x being -1, not for x being zero, which is the opposite of what logical negation would do.
How does JavaScript's right shift differ from Python's for negative numbers?
JavaScript has both a signed right shift (>>) that preserves the sign bit and an unsigned right shift (>>>) that fills with 0. Python's right shift (>>) is always arithmetic, preserving the sign for negative numbers, because its integers are arbitrarily large.
What is the formula for checking if a number is a power of two using bitwise operators?
A number is a power of two if (x & (x - 1)) equals 0. This works because a power of two has exactly one set bit, so subtracting 1 flips all lower bits and the AND becomes zero.
Why is right-shifting a negative value in C considered implementation-defined?
The C17 standard (section 6.5.7) states that right-shifting a negative value is implementation-defined, meaning the result depends on the compiler. On one compiler, -8 >> 1 might give -4, while on another it could give a large positive number.
How do you isolate a specific field within a larger integer using a bit mask?
To isolate a field, AND it with a mask that has 1s only in the positions you care about. To clear a field, AND with the complement of the mask. These operations are the basis for extracting network prefixes or separating color channels in a 32-bit integer.
What happens when you apply bitwise NOT to 5 in Python?
In Python, the bitwise NOT of any integer x is always -(x+1), so ~5 is -6. This is because Python uses arbitrary-precision integers with no overflow, and the NOT operation inverts all bits including the sign.