Skip to content
devtoolbox.cc

Binary, hex and octal: number bases every developer uses

· 6 min read

Open the Number Base tool

Most of the time you can ignore how numbers are written. Then a colour is #FF8800, a file mode is 0755, a hash is 64 hex characters, a bit flag has to be checked with &, and -1 shows up in a debugger as 0xFFFFFFFF. At that point binary, octal and hex stop being school maths and become things you read every day.

This article covers the number bases developers actually run into, why each one exists, and the conversion traps in JavaScript and other languages. The number base converter converts between all of them live, including negative numbers and arbitrarily large values.

One idea: positional notation

Every base works the same way. Each digit is multiplied by a power of the base, and the results are added:

decimal 255 = 2·10² + 5·10¹ + 5·10⁰
binary  11111111 = 1·2⁷ + 1·2⁶ + … + 1·2⁰ = 255
hex     FF = 15·16¹ + 15·16⁰ = 255
octal   377 = 3·8² + 7·8¹ + 7·8⁰ = 255

Bases above 10 need more digit symbols, so hex uses A to F for 10 to 15. Base 36 uses all of 0–9 and A–Z, which is why it turns up in short ids: 1234567890 in base 36 is kf12oi.

Why hex is everywhere

One hex digit is exactly 4 bits (a nibble), so two hex digits are exactly one byte. That makes hex the natural way to show raw bytes: each byte always takes two characters, and you can read the bits of each digit without doing arithmetic.

Hex Binary Hex Binary
0 0000 8 1000
1 0001 9 1001
2 0010 A 1010
3 0011 B 1011
4 0100 C 1100
5 0101 D 1101
6 0110 E 1110
7 0111 F 1111

Once you know that table, you can read hex at a glance:

  • Colours: #FF8800 is three bytes: red FF (255), green 88 (136), blue 00 (0).
  • Hashes: a SHA-256 digest is 32 bytes, so it’s 64 hex characters. The hash generator shows digests in hex for exactly this reason.
  • UUIDs: 128 bits written as 32 hex digits in five groups. The version is a single hex digit you can read directly, as described in the UUID tool.
  • Memory addresses, byte dumps and Unicode code points (U+1F600): all hex.

Octal: mostly file permissions

Octal (base 8) digits are exactly 3 bits each. That fits Unix file permissions, which come in groups of three bits: read, write, execute for owner, group and others.

chmod 755 file
7 = 111 = rwx   (owner)
5 = 101 = r-x   (group)
5 = 101 = r-x   (others)

So 755 is rwxr-xr-x and 644 is rw-r--r--. In code, octal literals have a prefix: 0o755 in JavaScript and Python 3, 0755 in C and older languages. That leading zero causes trouble, as the next section shows.

Binary: flags and masks

Binary is what you reach for when individual bits have meanings. Feature flags, permission sets and protocol headers often pack several booleans into one integer:

const READ = 0b001,
  WRITE = 0b010,
  EXEC = 0b100;
let mode = READ | WRITE; // 0b011
const canWrite = (mode & WRITE) !== 0; // true
mode &= ~WRITE; // clear the flag: 0b001

Network masks are the same idea. A /24 subnet mask is 24 one-bits followed by 8 zero-bits: 11111111 11111111 11111111 00000000, which is 255.255.255.0.

Negative numbers: two’s complement

Computers store signed integers in two’s complement. To get −x in n bits, invert all the bits of x and add 1. In 8 bits:

Decimal Binary (8-bit) Hex
127 01111111 7F
1 00000001 01
0 00000000 00
−1 11111111 FF
−128 10000000 80

The top bit works as the sign, and an 8-bit signed integer holds −128 to 127. The same bit pattern 11111111 is 255 as unsigned and −1 as signed. That’s why a debugger shows −1 as 0xFFFFFFFF in 32 bits, and why a “very large” number in a log sometimes turns out to be a small negative one read as unsigned.

The number base converter has a two’s-complement panel for 8, 16, 32 and 64 bits, which shows both readings of the same bits.

Traps in JavaScript

JavaScript has several ways to turn text into a number, and they disagree.

parseInt('0x1F'); // 31   (hex prefix is understood)
parseInt('0b101'); // 0    (binary prefix is not)
Number('0b101'); // 5    (Number understands 0b, 0o and 0x)
parseInt('1e3'); // 1    (stops at the first non-digit)
Number('1e3'); // 1000
parseInt('08'); // 8    (in old engines this could be 0: octal!)
parseInt('ff', 16); // 255  (always pass the radix)
(255).toString(2); // "11111111"

The safe habit is parseInt(text, radix) with an explicit radix, or Number(text) when you want the whole string to be a number.

The 2^53 limit

JavaScript numbers are 64-bit floats. They represent every integer exactly only up to 2^53 − 1 = 9,007,199,254,740,991. Past that, gaps appear:

2 ** 53 + 1; // 9007199254740992 (not ...993)
0.1 + 0.2; // 0.30000000000000004

For 64-bit ids, bit masks wider than 32 bits, or any arithmetic that must be exact, use BigInt:

(2n ** 64n - 1n).toString(16); // "ffffffffffffffff"

Bitwise operators (&, |, >>) convert their operands to 32-bit integers, which is another source of surprises. -1 >>> 0 is 4294967295, the unsigned reading of the same 32 bits.

The converter on this site uses BigInt internally, so a 100-digit hex number converts without losing a digit.

Readability: grouping digits

Long binary and hex values are hard to read, so most languages allow separators: 0b1111_0000, 0xFFFF_FFFF, 1_000_000 (JavaScript since ES2021, Python since 3.6, Java since 7). Group binary in 4s (nibbles) or 8s (bytes), hex in 2s or 4s, decimal in thousands. The converter accepts _ and spaces in its input and can group its output the same way.

Quick reference

Base Prefix (JS/Python) Digits Bits per digit Typical use
2 0b 0–1 1 Flags, masks, protocol fields
8 0o 0–7 3 Unix permissions
10 none 0–9 n/a Humans
16 0x 0–9 A–F 4 Bytes, colours, hashes, addresses
36 none 0–9 A–Z n/a Short ids and codes

When you need to check a value, paste it into the number base converter. Every field updates as you type, prefixes like 0x and 0b are understood, and invalid digits are flagged on the field where they appear.

← All posts