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.
Look at this number.
173
You know exactly what it means. One hundred and seventy three. Youve known it since you were five years old.
But you only know one of its costumes.
The same value can dress as 0xAD. As 0o255. As 0b10101101. Four completely different faces. One identical number underneath.
And heres the part nobody tells you.
The costume you grew up in, base ten, is the one outfit your computer never wears. Not once. Not anywhere. You count in tens because you happen to have ten fingers. A machine has no fingers. It has switches.
This is page one. The bottom of the whole stack.
Everything else on this site, every byte, every address, every hash, every block, is just numbers wearing different clothes for different readers. Decimal for you. Binary for the circuits. Hex for the byte dumps. Octal for the old Unix hands.
Before you can read what a machine is saying, you have to learn to see past the costume. To recognise the same value no matter what base it shows up wearing.
So forget that 173 is special.
Its not.
Its one outfit out of infinitely many. Lets learn the rest.
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.
Number Systems across ScrapyBytes
The same ideas surface all over ScrapyBytes. Here is where this page connects to the rest of the curriculum, and how to follow each thread.
Binary is base two, the one base hardware actually uses. This page is the general theory; binary is the special case with two symbols, because a transistor has two states.
scrapybytes.vercel.app/binary →Logic GatesLogic gates do binary arithmetic in hardware. The adder sums base-two numbers one bit at a time. Number bases are where that math starts.
scrapybytes.vercel.app/logic-gates →ASCIIASCII maps numbers to characters. The letter A is just 65. Once you can read a number in any base, a character is a number wearing a costume.
scrapybytes.vercel.app/ascii →MemoryEvery memory address is a number, almost always printed in hexadecimal. 0x7fffd8 is base sixteen. This page is why addresses look the way they do.
A pointer is a number that names an address, and addresses are written in hex. Hex is the base the entire pointers page lives in.
scrapybytes.vercel.app/pointers →CPUThe CPU computes in binary but reports its registers and addresses in hex. Base conversion is the translation layer between silicon and the human reading it.
scrapybytes.vercel.app/cpu →HashingA SHA-256 hash is a 256-bit number shown as 64 hex digits. Hex exists because reading 256 raw bits is impossible. Number bases are why every hash is written in hex.
scrapybytes.vercel.app/hashing →NetworkingAn IPv4 address is four base-ten numbers, IPv6 is eight base-sixteen groups, a MAC address is hex. Networking is number bases printed on the wire.
scrapybytes.vercel.app/networking →BlockchainBitcoin private keys, hashes, and block IDs are all 256-bit hex numbers. The blockchain page is base sixteen all the way down.
scrapybytes.vercel.app/blockchain →