A binary number system is a positional numeral system with a radix of 2, using only the digits 0 and 1, in which each digit position represents a successive power of 2 — the numeral system used by all digital computers because the two digits correspond directly to the two stable states (on/off) of the transistors from which computers are built.
Computers count in binary — using only 1s and 0s — not because binary is elegant, but because every switch inside a computer has exactly two positions: on and off.
You already understand positional notation
The number system most people use every day is decimal — base 10. It uses ten digits: 0 through 9. The value of each digit depends on its position: the rightmost digit represents ones, the next column represents tens, the next hundreds, and so on.
The number 347 means: 3 × 100, plus 4 × 10, plus 7 × 1. Each column is a power of 10: 10⁰ = 1, 10¹ = 10, 10² = 100. We use ten symbols because humans have ten fingers. There is nothing mathematically special about base 10.
Binary: the same idea, base 2
Binary works identically — except there are only two digits (0 and 1) and each column represents a power of 2 instead of 10.
The binary number 1011 means: 1 × 8, plus 0 × 4, plus 1 × 2, plus 1 × 1 = 8 + 0 + 2 + 1 = 11 in decimal.
Every light switch is either on or off. Computers use billions of switches — instead of light, each stores a single piece of information: on means 1, off means 0. Counting in binary is the natural language of a machine made of switches.
Reading and writing binary
Converting binary to decimal: write out the powers of 2 from right to left, multiply each digit by its column value, and add them up.
| Binary | Calculation | Decimal |
|---|---|---|
0001 | 1 | 1 |
0010 | 2 | 2 |
0101 | 4 + 1 | 5 |
1000 | 8 | 8 |
1111 | 8 + 4 + 2 + 1 | 15 |
11111111 | 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 | 255 |
Eight binary digits (8 bits) form one byte. A byte can represent 2⁸ = 256 different values (0 through 255). Two bytes = 16 bits = 65,536 values. Four bytes = 32 bits = over 4 billion values. Eight bytes = 64 bits = over 18 quintillion values — enough to address every byte of memory in every computer ever built.
Hexadecimal: shorthand for binary
Binary numbers get long fast. A memory address in a modern 64-bit computer requires 64 binary digits to write out. To keep things readable, programmers use hexadecimal — base 16. It uses sixteen digits: 0–9, then A–F (where A = 10, B = 11, C = 12, D = 13, E = 14, F = 15).
Four binary digits map exactly to one hexadecimal digit. The binary number 1010 1111 is AF in hexadecimal. Memory addresses in code often appear as 0xFF3A00C4 — the 0x prefix signals hexadecimal. This is the same number as 4281352388 in decimal, but 0xFF3A00C4 is far easier for a human to read and cross-reference against documentation.
Representing negative numbers
Binary digits are just 0s and 1s — there is no minus sign. So how does a computer store the number −5?
The answer is two's complement. In an 8-bit number, the leftmost bit is used as a sign bit. If it is 0, the number is positive. If it is 1, the number is negative. The range is −128 to +127. The decimal number 5 is 00000101 in 8-bit binary. The number −5 in two's complement is 11111011.
This encoding has a useful property: adding a positive and a negative number works correctly using ordinary binary addition, with no special case needed. The hardware that adds two positive numbers can also subtract, for free.
Representing fractions: floating point
Integers are straightforward. But how does a computer store 3.14159 or 0.0000001?
Computers use floating-point representation — a binary equivalent of scientific notation. A number like 3.14 is stored as a sign, a significand (the significant digits), and an exponent (the power of 2 to multiply by). Most computers use the IEEE 754 standard, which specifies exactly how 32-bit and 64-bit floating-point numbers are stored.
Floating-point numbers are not exact. The decimal fraction 0.1 cannot be represented exactly in binary floating point — just as 1/3 cannot be represented exactly in decimal. This causes surprising results: in most programming languages, 0.1 + 0.2 does not equal exactly 0.3. This is not a bug — it is the mathematically correct result of IEEE 754 arithmetic. Never use floating-point numbers to represent money.
How this appears in programming
Every integer variable in every programming language is a sequence of bits. In Python, int values are stored with arbitrary precision (as many bits as needed). In C, int is typically 32 bits, long long is 64 bits. In Java, int is always exactly 32 bits and long is always exactly 64 bits.
In many languages, binary and hexadecimal literals can be written directly in code:
decimal = 255 # Base 10 — the default
binary = 0b11111111 # Base 2 — prefix: 0b
octal = 0o377 # Base 8 — prefix: 0o
hexadecimal = 0xFF # Base 16 — prefix: 0x
# All four refer to the same value: 255
print(decimal == binary == octal == hexadecimal) # True
# Converting between bases
print(bin(255)) # '0b11111111'
print(hex(255)) # '0xff'
print(oct(255)) # '0o377'
print(int('11111111', 2)) # 255 (parse binary string)
print(int('FF', 16)) # 255 (parse hex string)
Positional numeral systems: formal definition
A positional numeral system with radix b represents a number as a sequence of digits dndn-1...d1d0, where each digit di satisfies 0 ≤ di < b, and the value of the sequence is:
Value = Σ (di × bi) for i = 0 to n
For decimal (b=10), binary (b=2), octal (b=8), and hexadecimal (b=16), this same formula applies. The choice of base is a representation convention — the underlying mathematical value is the same.
Binary arithmetic
Binary addition follows the same rules as decimal addition, with carry:
| A | B | Sum | Carry |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
Adding 0110 (6) and 0111 (7): working right to left: 0+1=1, 1+1=10 (write 0 carry 1), 1+1+1=11 (write 1 carry 1), 0+0+1=1. Result: 1101 = 13. Correct.
Bitwise operations — AND, OR, XOR, NOT, and bit shifts — operate on individual bits and are fundamental to systems programming, cryptography, and graphics:
a = 0b1010 # 10
b = 0b1100 # 12
print(bin(a & b)) # 0b1000 (8) — AND: both bits must be 1
print(bin(a | b)) # 0b1110 (14) — OR: either bit is 1
print(bin(a ^ b)) # 0b0110 (6) — XOR: bits differ
print(bin(~a)) # -0b1011 (-11) — NOT: flip all bits (two's complement)
print(bin(a << 1)) # 0b10100 (20) — left shift: multiply by 2
print(bin(a >> 1)) # 0b101 (5) — right shift: integer divide by 2
Hexadecimal in practice
Four binary bits map exactly to one hexadecimal digit because 2⁴ = 16. This makes hexadecimal a compact human-readable representation of binary data. The correspondence:
| Binary | Hex | Decimal | Binary | Hex | Decimal |
|---|---|---|---|---|---|
| 0000 | 0 | 0 | 1000 | 8 | 8 |
| 0001 | 1 | 1 | 1001 | 9 | 9 |
| 0010 | 2 | 2 | 1010 | A | 10 |
| 0011 | 3 | 3 | 1011 | B | 11 |
| 0100 | 4 | 4 | 1100 | C | 12 |
| 0101 | 5 | 5 | 1101 | D | 13 |
| 0110 | 6 | 6 | 1110 | E | 14 |
| 0111 | 7 | 7 | 1111 | F | 15 |
Hexadecimal appears throughout programming: memory addresses (0x7FFD3A40), RGB colours (#FF6633), Unix file permissions, network MAC addresses, SHA256 hashes, and UUID identifiers all use hex notation.
Two's complement: negative integers
Two's complement is the universal method for representing signed integers in hardware. For an n-bit integer:
- The range is −2n−1 to 2n−1 − 1
- For 8 bits: −128 to 127
- For 16 bits: −32,768 to 32,767
- For 32 bits: −2,147,483,648 to 2,147,483,647
- For 64 bits: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
To find the two's complement representation of a negative number, take the positive value, flip all bits (one's complement), then add 1:
Find -5 in 8-bit two's complement:
Step 1: +5 in binary: 0000 0101
Step 2: Flip all bits: 1111 1010 (one's complement)
Step 3: Add 1: + 1
Result: -5 two's comp: 1111 1011
Verify: -5 + 5 should = 0
1111 1011
+ 0000 0101
-----------
0000 0000 (carry discarded — correct!)
Why two's complement over simpler sign-magnitude representations? Because addition of two's complement numbers requires no special case — the same adder circuit works for both positive and positive, and positive and negative. Subtraction becomes addition of the negative. There is only one representation of zero (sign-magnitude has +0 and −0). Hardware is simpler and faster.
IEEE 754 floating-point representation
The IEEE 754 standard (IEEE Std 754-2019) defines how real numbers are approximated in binary. A 32-bit single-precision float consists of:
- 1 sign bit — 0 for positive, 1 for negative
- 8 exponent bits — stored with a bias of 127 (to represent both positive and negative exponents)
- 23 mantissa bits — the fractional part of the significand (with an implicit leading 1)
The value is: (−1)sign × 2exponent − 127 × 1.mantissa
The number 0.1 in decimal cannot be represented exactly in binary because it is a repeating fraction in base 2 (just as 1/3 is a repeating decimal: 0.3333...). The closest representable 32-bit float to 0.1 is approximately 0.100000001490116119384765625. When you add 0.1 and 0.2 in a computer, the rounding errors in both representations accumulate, and the result is not exactly 0.3.
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
# For exact decimal arithmetic, use Python's Decimal module
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
# IEEE 754 special values
import math
print(float('inf')) # inf
print(float('nan')) # nan
print(math.isnan(float('nan'))) # True
IEEE 754 also defines special values: positive and negative infinity (produced by dividing by zero for non-zero values), Not a Number (NaN, produced by 0/0 or sqrt(−1)), and positive and negative zero. NaN is the only value not equal to itself: float('nan') == float('nan') is False in every IEEE 754-compliant language.
0.1 + 0.2 is the mathematically correct answer given IEEE 754 double-precision arithmetic. It is not a language bug. It is the inevitable consequence of representing infinite real numbers in finite binary storage.IEEE 754-2019: formal specification
The IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019, revision of the 1985 original) defines five basic formats: binary16, binary32, binary64, binary128, and decimal128. The binary32 (single-precision) and binary64 (double-precision) formats are used by virtually all hardware and programming languages.
Binary64 (double-precision) structure:
- 1 sign bit (s)
- 11 exponent bits (e), biased by 1023
- 52 mantissa bits (f), with implicit leading 1 for normalised values
Normalised value: (−1)s × 2e − 1023 × 1.f
Range: approximately ±5 × 10−324 to ±1.8 × 10308. Precision: ~15–17 significant decimal digits.
Special encodings in binary64:
| Exponent bits | Mantissa bits | Value |
|---|---|---|
| all 0 | all 0 | ±0 (signed zero) |
| all 0 | nonzero | subnormal (denormalised) number |
| all 1 | all 0 | ±∞ |
| all 1 | nonzero | NaN (quiet or signalling) |
| other | any | normalised number |
Subnormal (denormalised) numbers extend the range near zero at the cost of reduced precision. They allow gradual underflow — the result of a very small computation does not abruptly become zero, but loses mantissa bits gradually.
Two's complement: formal arithmetic properties
For an n-bit two's complement integer, the most negative value has no positive counterpart — it is its own two's complement. For 8-bit integers: −128 represented as 10000000. Negating −128 in 8 bits produces 10000000 again (overflow). This asymmetry is a formal consequence of the range −2n−1 to 2n−1 − 1.
Two's complement addition is modular arithmetic modulo 2n. Overflow occurs when the true mathematical result falls outside the representable range. In C, signed integer overflow is undefined behaviour (ISO C17, §6.5). In Java, signed integer overflow wraps predictably (Java Language Specification §15.18.2). In Python, integers have arbitrary precision — overflow cannot occur.
Number representation in the context of data types
The connection between number systems and data types in programming languages:
| Type | Language | Bits | Range | Overflow behaviour |
|---|---|---|---|---|
int | C (typical) | 32 | −2³¹ to 2³¹−1 | Undefined behaviour (ISO C17) |
int | Java | 32 | −2³¹ to 2³¹−1 | Wraps (modular) |
int | Python | Arbitrary | Unlimited | No overflow |
i32 | Rust | 32 | −2³¹ to 2³¹−1 | Panic in debug; wraps in release |
uint8 | Go | 8 | 0 to 255 | Wraps (modular) |
float64 | Go / Java / C | 64 | ~±1.8×10³⁰⁸ | IEEE 754 (±∞, NaN) |
Unicode and UTF-8: text as binary
The Unicode standard (The Unicode Consortium, The Unicode Standard, current version at unicode.org/standard/standard.html) assigns a unique integer — a code point — to every character in every writing system: Latin, Devanagari, Chinese, Arabic, mathematical symbols, emoji. Code points are written in hexadecimal: U+0041 is Latin capital letter A (decimal 65). The full Unicode code space covers 1,114,112 code points (U+0000 to U+10FFFF).
UTF-8 (RFC 3629) encodes Unicode code points as variable-length sequences of 1 to 4 bytes. ASCII characters (U+0000 to U+007F) are encoded as single bytes identical to their ASCII values — making UTF-8 backward-compatible with ASCII. Code points above U+007F use multi-byte sequences with specific bit patterns identifying the sequence length.
IEEE Std 754-2019 — IEEE Standard for Floating-Point Arithmetic. IEEE Computer Society, July 2019. Available at ieeexplore.ieee.org. This is the authoritative specification for floating-point arithmetic on all modern hardware and in all conforming programming languages.