root.system / 0x01 / numbers

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.

Beginner// level 01

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

basenamesymbolswhere it shows up
10decimal0-9Everyday humans. The default everywhere except inside the machine.
2binary0, 1What the silicon literally does. See the next page.
8octal0-7Unix file permissions (chmod 755), older minicomputers.
16hexadecimal0-9, A-FMemory 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 just CAFEBE0D. 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

Rust• • •
// 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)
}
C• • •
#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;
}
// the prefixes
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.
Intermediate// level 02

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.

hexdecimalbinaryhexdecimalbinary
000000881000
110001991001
220010A101010
330011B111011
440100C121100
550101D131101
660110E141110
770111F151111

Where hex shows up in real life

addresses
0x7ffeef3a4c20
Pointers and memory addresses are always printed in hex; easier to spot alignment, page boundaries, and patterns.
colors
#FF2BD6
Web/CSS color codes are three pairs of hex bytes: red, green, blue. The neon-magenta on this page is exactly that.
byte dumps
DE AD BE EF
Network packets, file headers, hex editors. Anywhere raw bytes are inspected, hex is the format.
hashes
5d41402a…
MD5, SHA-256 and friends emit fixed-width hex strings. 32 hex chars = 128 bits = MD5.
mac
AC:DE:48:00:11:22
Network hardware addresses are six hex bytes, separated by colons.
magic numbers
0xCAFEBABE
Java <code>.class</code> files start with <code>CAFEBABE</code>; PNG starts with <code>89 50 4E 47</code>; ELF with <code>7F 45 4C 46</code>. File-format signatures are pure hex.
// connection: hashing
Every SHA-256 hash is 64 hex characters. 64 hex × 4 bits = 256 bits. That's the entire security model of Bitcoin — one 256-bit number that's practically impossible to reverse. ← See: Hashing

Octal: the survivor

Octal is rare today, but two places still use it constantly:

  • Unix file permissions. chmod 755 file sets rwxr-xr-x. That 755 is 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.

Rust• • •
// 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
}
C• • •
#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;
}
// the binary ↔ hex shortcut
You don't have to divide. Group the binary digits in fours from the right and replace each group with its hex digit. 101011011010 1101A D0xAD. That's the only conversion most working programmers ever do by hand.
Advanced// level 03

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 = exactly 0.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:

nbinarygray
0000000
1001001
2010011
3011010
4100110
5101111
6110101
7111100

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.

Rust• • •
// 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);
}
C• • •
#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;
}
// the full chain
Gray code uses XOR and bit shifts. XOR is a logic gate. A logic gate is transistors wired together. The same transistors that started this entire story. ← See: Logic Gates

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).

// base58: a safety decision disguised as a number system
Bitcoin addresses use Base 58. Base 58 removes 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: Blockchain

Larger and stranger bases

base 256
Raw bytes
Treat each byte as a digit in a base-256 number. Big-integer libraries do this internally; addition becomes byte-by-byte with carries, exactly like long addition in school.
base 36
0-9, A-Z
Maximum compact alphanumeric encoding. Used for short URL slugs, license plates, ID codes (TOTP secrets are often base32 for the same reason).
balanced ternary
−1, 0, +1
Three symbols, one of which is negative. The 1958 Soviet computer Setun used it; Knuth called it 'perhaps the prettiest number system of all'.
base φ
Phinary
Base equals the golden ratio. Every integer has a unique representation. A delightful curio with no practical use, yet.

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.

// from numbers to bits
Decimal lives in your head. Hex lives in your debugger. Binary lives in the wires. The next page goes all the way down to that last layer: what binary actually is, and what you can do with it once you're there.

Where number systems appear in BitRoot

next up / 0x02
Down to the wire: binary, bitwise tricks, and IEEE 754
binary