---
title: 'Hexadecimal and Octal Bases'
source: 'https://academia.sh/en/courses/how-computers-work/hexadecimal-and-octal-bases'
course: 'How Computers Work'
language: en
updated: '2026-08-17T18:08:11+00:00'
license: 'CC BY-SA 4.0'
---

# Hexadecimal and Octal Bases

Hexadecimal and octal representations, which give compact notation without breaking bit boundaries, and how to convert between them.

The previous lesson established that binary representation matches the hardware
exactly. That correspondence costs length: a 32-bit value is a thirty-two-symbol
sequence such as `01000001010000100100001101000100`, and reading, writing, or
comparing this sequence without error is inconvenient for a person.

This lesson asks the following question: how can the same value be written with fewer
symbols without hiding the bit structure? The answer lies in restricting the base
choice to a power of two.

## Why Powers of Two

If the base is chosen as $2^k$, every digit in that base corresponds to exactly $k$
bits. This reduces conversion from computation to grouping: it suffices to split the
bits into blocks of $k$ and replace each block with a single digit.

This does not hold for the decimal system, since $10$ is not a power of two. Looking
at the decimal digits of `4703` says nothing about its bit pattern; conversion
requires division. Hexadecimal and octal systems are therefore preferred: they give
compactness while doing so without breaking bit boundaries.

## Hexadecimal System

In the **hexadecimal** system the base is $16$. Sixteen digits are required; since the
ten symbols of the decimal system are insufficient, the letters `A`–`F` are used for
the remaining six values:

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

Since $16 = 2^4$, every hexadecimal digit carries exactly four bits. A group of four
bits is called a **nibble**. A byte is eight bits, that is, two nibbles; every byte is
therefore written with exactly two hexadecimal digits. This is the real reason for the
widespread use of hexadecimal notation: byte boundaries remain visible in the
representation.

Whether the letters are written in upper or lower case does not change the value;
`0xFF` and `0xff` denote the same value. Within a single text, one form is chosen for
consistency.

## Converting by Grouping

To go from binary to hexadecimal, the bits are grouped in fours **starting from the
right**, and each group is replaced by its counterpart in the table. If the leftmost
group has fewer than four bits, it is padded with leading zeros.

$$
\underbrace{0100}_{4}\ \underbrace{0001}_{1}\ \underbrace{0100}_{4}\ \underbrace{0010}_{2}\ \underbrace{0100}_{4}\ \underbrace{0011}_{3}\ \underbrace{0100}_{4}\ \underbrace{0100}_{4}
$$

A thirty-two-bit sequence collapses to eight hexadecimal digits: `41424344`. The
reverse direction applies the same rule in the opposite order: each hexadecimal digit
is replaced by four bits.

Grouping from the right matters. Grouping from the left, when the bit count is not a
multiple of four, shifts every positional value and produces an incorrect result.

## The Course's Shared Example

The value obtained above is the example this course will return to again and again:

$$
\texttt{0x41424344}
$$

Its decimal counterpart can be computed from the positional values:

$$
4 \times 16^7 + 1 \times 16^6 + 4 \times 16^5 + 2 \times 16^4 + 4 \times 16^3 + 3 \times 16^2 + 4 \times 16^1 + 4 \times 16^0 = 1{,}094{,}861{,}636
$$

This is the interpretation of the 32-bit pattern **as an unsigned integer**. The same
pattern will be interpreted differently in later lessons: as a signed integer, as a
real number, as a four-letter piece of text, and as a sequence that looks different
depending on the byte order in memory. The bit pattern stays the same across all these
readings; what changes is only the interpretation rule applied to it.

This distinction carries the central idea of the lesson about the nature of data:
memory contains no such thing as a "number" or "text" — it contains bit patterns and
the interpretations applied to them.

## Octal System

In the **octal** system the base is $8$; the digits range from $0$ to $7$. Since
$8 = 2^3$, every octal digit carries three bits, and conversion again proceeds by
grouping — this time in threes.

$$
\underbrace{111}_{7}\ \underbrace{101}_{5}\ \underbrace{101}_{5} = 755_8
$$

Because groups of three do not align with byte boundaries, octal representation lags
behind hexadecimal for general-purpose use: 8 bits, grouped in threes, yield two full
digits and two leftover bits.

Octal survives in domains whose natural unit is three bits. File permissions on
Unix-derived systems are the standard example: read, write, and execute permissions
form a group of three bits, and this group is repeated separately for owner, group,
and others. The notation `755` encodes the permission `rwxr-xr-x`; the first digit
$7 = 111_2$ states that all three permissions are granted, and the other two digits
$5 = 101_2$ state that write permission is off. The permission model is treated in
detail in the Linux curriculum.

## Notational Conventions

Which base a number is written in must be stated in code just as it is in text.
Common prefixes:

| Prefix | Base | Example | Value |
|---|---|---|---|
| `0b` | 2 | `0b1011` | 11 |
| `0o` | 8 | `0o755` | 493 |
| `0x` | 16 | `0x1F` | 31 |
| (none) | 10 | `31` | 31 |

Prefixes are not part of the value; they only state which base to read. When the same
value is written in four different forms, the same bit pattern results in memory.

The following program shows the conversions and formatting:

```python
value = 0x41424344

print(value)                      # 1094861636
print(hex(value))                 # 0x41424344
print(bin(value))                 # 0b1000001010000100100001101000100
print(f"{value:#010x}")           # 0x41424344  (at least 8 digits, prefixed)
print(f"{value:032b}")            # 01000001010000100100001101000100

# Reading byte by byte: each byte is two hexadecimal digits.
for shift in (24, 16, 8, 0):
    byte = (value >> shift) & 0xFF
    print(f"{byte:#04x}", byte)   # 0x41 65 / 0x42 66 / 0x43 67 / 0x44 68
```

It is worth noting that the `bin` output has 31 digits rather than 32: the built-in
function does not print leading zeros. When fixed-width representation is required, a
format specifier (`032b`, `#010x`) is used. Fixed width is needed to keep columns
aligned when memory contents are inspected.

The `>>` and `&` operators in the loop extract a specific byte from the pattern; these
operators will be defined in the lesson on bit-level operations.

## Summary

- When the base is chosen as $2^k$, conversion between bases is not computation but
  grouping in blocks of $k$.
- In the hexadecimal system every digit carries four bits, so every byte is exactly
  two digits; byte boundaries remain visible in the representation.
- Converting binary to hexadecimal groups from the right; a leftover left group is
  padded with zeros.
- In the octal system every digit is three bits; because it does not align with byte
  boundaries, its use is limited to naturally three-bit fields such as file
  permissions.
- The `0b`, `0o`, `0x` prefixes state the base to be read, not the value.
- The course's shared example, `0x41424344`, has the value $1{,}094{,}861{,}636$ under
  an unsigned interpretation.

## Next Step

Up to this point every number has been treated as non-negative. Hardware, however,
has no separate place to hold a minus sign; the sign, too, must be encoded within the
bit pattern. The next lesson takes up how this is done, and why the chosen method
leaves the addition circuit unchanged.
