Same number.
Different bases.
173, 0xAD, 0o255, 0b10101101: four ways of writing the exact same value. Different bases are convenient for different jobs. Decimal for humans, binary for circuits, hex for byte dumps, octal for Unix permissions. This page is what every other base actually is, and why programmers move between them so often.
What's a base, really?
Your computer has never seen the number 173.
It has only ever seen 10101101.
But your browser shows you 173.
And your debugger shows you 0xAD.
And your Unix terminal shows you 0o255.
Same value.
Four different masks.
All hiding the same binary underneath.
This is number systems.
A base (or radix) is just the number of distinct symbols you use to write numbers, plus the rule that each position to the left is worth that many times more than the one to its right. That's the whole idea. Once you see it, every base in the world reduces to the same shape.
You write 237 in base 10 because you have ten symbols (0 to 9) and each position is a power of ten:
23710 = 2×10² + 3×10¹ + 7×10⁰
= 200 + 30 + 7
Swap the base for any other number, keep the same idea. Base 2: two symbols (0, 1), each place is a power of 2. Base 16: sixteen symbols (0 to 9, A to F), each place a power of 16.
The four bases you'll actually meet
| base | name | symbols | where it shows up |
|---|---|---|---|
| 10 | decimal | 0-9 | Everyday humans. The default everywhere except inside the machine. |
| 2 | binary | 0, 1 | What the silicon literally does. See the next page. |
| 8 | octal | 0-7 | Unix file permissions (chmod 755), older minicomputers. |
| 16 | hexadecimal | 0-9, A-F | Memory addresses, byte dumps, CSS colors, MAC addresses, MD5 hashes. |
Why so many?
Two reasons: physics and ergonomics.
- Physics picks binary. A switch is the simplest reliable building block; it has two states. Base 2 is the natural language of circuits.
- Ergonomics picks hex for humans reading machine state. Long binary strings are tiring to scan:
11001010111110101011111000001101. The same value in hex is justCAFEBE0D. Eight characters instead of thirty-two; same information, far easier to remember and compare.
Hex works for this because 16 = 2⁴: every hex digit is exactly four binary digits. Splitting a 32-bit value into 8 hex digits is a no-arithmetic operation. Octal works the same way (8 = 2³, three bits per octal digit), and that's the reason it ever existed at all.
Try it: convert any number
Same value, four ways of writing it
// Same number, four bases. The compiler accepts each form.
fn main() {
let a = 255; // decimal
let b = 0b1111_1111; // binary literal
let c = 0o377; // octal literal
let d = 0xff; // hexadecimal literal
println!("{} {} {} {}", a, b, c, d);
// 255 255 255 255 (same value, four notations).
// Format any number into any base:
let n = 173;
println!("dec {n}"); // 173
println!("hex {n:#x}"); // 0xad
println!("oct {n:#o}"); // 0o255
println!("bin {n:#010b}"); // 0b10101101 (10-char, zero-padded)
}#include <stdio.h>
int main(void) {
int a = 255; // decimal
int b = 0xff; // hexadecimal literal
int c = 0377; // octal literal: leading 0 means octal in C
// C has no native binary literal; use shifts or 0b... (gcc/clang ext.)
int d = 0b11111111;
printf("%d %d %d %d\n", a, b, c, d);
// 255 255 255 255
int n = 173;
printf("dec %d\n", n); // 173
printf("hex 0x%x\n", n); // 0xad
printf("oct 0%o\n", n); // 0255
// C has no %b, so print bit-by-bit (see /binary):
printf("bin 0b");
for (int i = 7; i >= 0; i--)
putchar((n >> i) & 1 ? '1' : '0');
putchar('\n');
return 0;
}0b… is binary, 0o… is octal (Rust), leading 0 alone is octal (C, historic and a famous footgun: 010 in C is 8, not 10), 0x… is hex. No prefix means decimal. These prefixes show up in source code, debugger output, network logs, everywhere.Hex & octal: the bases programmers live in
Once you know binary, hex is almost free. Every hex digit is exactly four bits. Memorise that table once and you will read memory dumps for the rest of your life.
| hex | decimal | binary | hex | decimal | binary |
|---|---|---|---|---|---|
0 | 0 | 0000 | 8 | 8 | 1000 |
1 | 1 | 0001 | 9 | 9 | 1001 |
2 | 2 | 0010 | A | 10 | 1010 |
3 | 3 | 0011 | B | 11 | 1011 |
4 | 4 | 0100 | C | 12 | 1100 |
5 | 5 | 0101 | D | 13 | 1101 |
6 | 6 | 0110 | E | 14 | 1110 |
7 | 7 | 0111 | F | 15 | 1111 |
Where hex shows up in real life
Octal: the survivor
Octal is rare today, but two places still use it constantly:
- Unix file permissions.
chmod 755 filesetsrwxr-xr-x. That755is octal: each digit is three bits (read, write, execute) for owner, group, and other.7=111= rwx.5=101= r-x. The grouping is why octal was chosen here. - Old hardware. The PDP-8 (12-bit words) and PDP-11 (16-bit words) grouped bits by threes, so all their assemblers and manuals wrote everything in octal.
Converting between bases, by hand
The universal algorithm: repeated division. To convert 173 to base 16, divide by 16 and read the remainders bottom-up:
173 ÷ 16 = 10 remainder 13 (D)
10 ÷ 16 = 0 remainder 10 (A)
read bottom-up: AD → 17310 = AD16
The reverse, base 16 to base 10, is just multiplication by place value: A×16 + D = 10×16 + 13 = 173. Same shape, same algorithm; works for any base.
// Convert any positive integer into any base 2..36, by hand.
// (The standard library covers 2/8/10/16; this works for any.)
fn to_base(mut n: u32, base: u32) -> String {
assert!((2..=36).contains(&base));
if n == 0 { return "0".into(); }
const DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let mut out = Vec::new();
while n > 0 {
out.push(DIGITS[(n % base) as usize]);
n /= base;
}
out.reverse();
String::from_utf8(out).unwrap()
}
fn main() {
let n = 1_000_000;
println!("base 2: {}", to_base(n, 2)); // 11110100001001000000
println!("base 8: {}", to_base(n, 8)); // 3641100
println!("base 16: {}", to_base(n, 16)); // f4240
println!("base 36: {}", to_base(n, 36)); // lfls
}#include <stdio.h>
#include <string.h>
#include <assert.h>
void to_base(unsigned int n, unsigned int base, char *out) {
assert(base >= 2 && base <= 36);
static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
if (n == 0) { strcpy(out, "0"); return; }
char buf[40];
int i = 0;
while (n > 0) {
buf[i++] = digits[n % base];
n /= base;
}
// reverse buf into out
for (int j = 0; j < i; j++) out[j] = buf[i - 1 - j];
out[i] = '\0';
}
int main(void) {
char out[40];
to_base(1000000, 2, out); printf("base 2: %s\n", out);
to_base(1000000, 8, out); printf("base 8: %s\n", out);
to_base(1000000, 16, out); printf("base 16: %s\n", out);
to_base(1000000, 36, out); printf("base 36: %s\n", out);
return 0;
}10101101 → 1010 1101 → A D → 0xAD. That's the only conversion most working programmers ever do by hand.Other number systems used in computing
Decimal, binary, octal and hex cover 99% of what you'll meet. The remaining 1% is full of clever ideas that solve specific problems.
Binary-Coded Decimal (BCD)
BCD encodes each decimal digit as 4 bits. The number 173 in BCD is three nibbles: 0001 0111 0011, not the binary value 173 (which is 10101101). Wasteful in storage (only 10 of 16 patterns used per nibble) but two genuine wins:
- Exact decimal arithmetic. Currency, accounting, and financial systems can't tolerate the rounding error of binary floats (see the binary page). BCD adds in decimal directly:
0.10 + 0.20= exactly0.30, no surprises. - Direct display. Old calculators and 7-segment displays drove their digits straight from BCD nibbles, with no binary-to-decimal conversion needed for output.
Modern decimal types (Java BigDecimal, Python Decimal, IEEE 754-2008's decimal floats) are spiritual descendants of BCD.
Gray code
Standard binary counts 011 → 100 and three bits flip simultaneously. If the bits are sampled mid-transition (a rotary sensor, an analogue circuit), the reader could see a transient glitch like 111: a value that's not even neighbouring. Gray code reorders the binary patterns so consecutive numbers differ by exactly one bit:
| n | binary | gray |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
Notice every step is a single-bit flip. Used in: rotary encoders, KVM matrix scanners, Karnaugh-map minimisation, and certain genetic-algorithm encodings where you want neighbouring values to also be neighbours in the search space.
The conversion is famously elegant: one XOR with a right shift.
// Gray code: a binary encoding where consecutive numbers
// differ by exactly one bit. Useful for rotary encoders,
// Karnaugh maps, and any setting where transition glitches matter.
fn to_gray(n: u32) -> u32 { n ^ (n >> 1) }
fn from_gray(g: u32) -> u32 {
let mut n = g;
let mut shift = 1;
while (g >> shift) > 0 { n ^= g >> shift; shift += 1; }
n
}
fn main() {
println!("n bin gray");
for n in 0u32..8 {
println!("{n} {n:03b} {:03b}", to_gray(n));
}
// 0 000 000
// 1 001 001
// 2 010 011 ← bit 1 flipped
// 3 011 010 ← bit 0 flipped
// 4 100 110 ← bit 2 flipped
// 5 101 111
// 6 110 101
// 7 111 100
assert_eq!(from_gray(to_gray(42)), 42);
}#include <stdio.h>
#include <stdint.h>
#include <assert.h>
uint32_t to_gray(uint32_t n) { return n ^ (n >> 1); }
uint32_t from_gray(uint32_t g) {
uint32_t n = g;
for (uint32_t s = 1; (g >> s) > 0; s++) n ^= g >> s;
return n;
}
int main(void) {
printf("n bin gray\n");
for (uint32_t n = 0; n < 8; n++) {
printf("%u ", n);
for (int i = 2; i >= 0; i--) putchar((n >> i) & 1 ? '1' : '0');
printf(" ");
uint32_t g = to_gray(n);
for (int i = 2; i >= 0; i--) putchar((g >> i) & 1 ? '1' : '0');
putchar('\n');
}
assert(from_gray(to_gray(42)) == 42);
return 0;
}Base 64 (and friends)
When binary data has to travel through a text-only channel (email bodies, JSON, URLs), it's encoded in base 64: 6 bits per character (2⁶ = 64 symbols, the alphabet A to Z, a to z, 0 to 9, +, /). Three bytes (24 bits) become four base-64 characters; size grows by 33%. Variants include base32 (case-insensitive, 5 bits per char) and base58 (Bitcoin addresses, omits visually ambiguous 0OIl).
0, O, I, l — the characters that look the same in most fonts. Because a single misread character means your Bitcoin is gone forever. The encoding choice is a user-safety decision disguised as a number system. ← See: BlockchainLarger and stranger bases
Fixed-point: a different way to handle decimals
Floats (IEEE 754, covered on the binary page) trade exactness for range. Fixed-point trades range for exactness: pick a fixed integer scale, then store all values as integers in that scale. Currency systems often store amounts as integer cents (never fractions of a cent) and never see floating-point rounding error.
It's not a different base; the underlying numbers are still binary integers. But it's a different numeric system built on top of binary. When correctness beats range (banking, blockchain ledgers, embedded control loops), fixed-point wins.