A byte is eight bits¶
Level: 101 · for anyone starting from zero
One line: A byte is eight switches, and the pattern they make has no meaning of its own: 0100 0001 is the number 65, the letter A, or a quarter of a float, depending only on who is reading it and what they agreed to.
Eight switches, each worth a power of two¶
A bit is one switch: off or on, 0 or 1. A byte is eight of them in a row, and each position is worth a power of two, largest on the left:
place value 128 64 32 16 8 4 2 1
65 0 1 0 0 0 0 0 1 = 64 + 1
200 1 1 0 0 1 0 0 0 = 128 + 64 + 8
255 1 1 1 1 1 1 1 1 = all of them
To read a byte, add the place values under the 1s. To write a number as a byte, walk the place values from the left: if the number is at least that big, write 1 and subtract; otherwise write 0. Section 5 of the Python output below does exactly that for 200, one step per line.
Eight switches make 2⁸ = 256 patterns, so a byte holds the numbers 0 through 255 and nothing else. 256 needs a ninth bit, and there is no ninth bit. That fact is the root of half the surprises in this library: a wider number takes several bytes, and then somebody has to decide which byte comes first. That is a lesson of its own, later.
The byte does not know what it means¶
The pattern 0100 0001 is 65. It is also the letter A — because in 1963 a committee agreed that 65 would stand for A, and every keyboard since has kept the promise. Nothing in the byte says which reading is correct. The program that reads the byte decides, and that is the whole reason "encoding" is a subject: two programs reading the same bytes under different agreements see different text.
Python shows this in one line. bytes([65]) prints as b'A': Python looked at the byte 65, applied the ASCII agreement, and showed you a letter. The byte did not change.
In Python¶
Verified output of a_byte_is_eight_bits_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. EIGHT SWITCHES, EACH WORTH A POWER OF TWO
place value : 128 64 32 16 8 4 2 1
0 : 0 0 0 0 0 0 0 0 = 0
1 : 0 0 0 0 0 0 0 1 = 1
2 : 0 0 0 0 0 0 1 0 = 2
65 : 0 1 0 0 0 0 0 1 = 64 + 1
127 : 0 1 1 1 1 1 1 1 = 64 + 32 + 16 + 8 + 4 + 2 + 1
128 : 1 0 0 0 0 0 0 0 = 128
200 : 1 1 0 0 1 0 0 0 = 128 + 64 + 8
255 : 1 1 1 1 1 1 1 1 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
2. HOW MANY PATTERNS EIGHT SWITCHES CAN MAKE
2 ** 8 = 256, so one byte holds 0..255 and nothing else.
256 needs a ninth bit: 100000000 (9 digits)
3. TWO WAYS TO WRITE THE SAME NUMBER
format(65, '08b') -> '01000001' (number -> its eight bits)
int('01000001', 2) -> 65 (eight bits -> number)
0b0100_0001 -> 65 (a binary literal in source)
4. THE SAME BYTE, THREE READINGS
as a number : 65
as bits : 0100 0001
as ASCII text : b'A' (Python shows the byte 65 as the letter A)
Nothing in the byte says which reading is right. The program does.
5. DECIMAL -> BINARY BY HAND, FOR 200
200 >= 128 -> write 1, subtract, 72 left
72 >= 64 -> write 1, subtract, 8 left
8 < 32 -> write 0
8 < 16 -> write 0
8 >= 8 -> write 1, subtract, 0 left
0 < 4 -> write 0
0 < 2 -> write 0
0 < 1 -> write 0
result: 1100 1000 == format(200, '08b') -> True
format(n, '08b') turns a number into its eight bits; int('01000001', 2) turns eight bits back into the number; 0b0100_0001 writes the bits directly in source (the underscore is ignored, and is there so you can see the two halves). Python's int has no width — it grows as needed — so a plain int is not a byte. The one-byte value lives in a bytes object, where each element is 0..255 and cannot be anything else.
In the terminal¶
xxd -b shows a byte as its eight bits; od shows it under three readings at once — decimal, hex, and "as a character" — which is a preview of the next two lessons. Going the other way, printf takes a byte written as an escape and hands the terminal the byte, and the terminal reads it as text.
Verified output of a_byte_is_eight_bits_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
1. THE LETTER A, AS THE EIGHT BITS IT IS STORED AS
$ printf 'A' | xxd -b
00000000: 01000001 A
2. THE SAME BYTE AS A DECIMAL, A HEX PAIR, AND A CHARACTER
$ printf 'A' | od -An -tu1 -tx1 -c | tidy
65
41
A
3. GOING THE OTHER WAY: WRITE THE BYTE 65 AND LET THE TERMINAL READ IT AS TEXT
$ printf '\101'; echo # octal escape: 101 base 8 = 65
A
$ printf '\x41'; echo # hex escape: 41 base 16 = 65
A
4. BASH ARITHMETIC UNDERSTANDS BASES
$ echo $(( 2#01000001 ))
65
$ echo $(( 2#11111111 ))
255
$ echo $(( 2#11111111 + 1 )) # bash integers are 64-bit: no wrap here
256
a_byte_is_eight_bits_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# A byte is eight switches. The terminal can show you all eight.
#
# Run: bash a_byte_is_eight_bits_sh.sh
set -eu
show() { printf '\n$ %s\n' "$1"; eval "$1"; }
# od lays its columns out differently on macOS (BSD) and Linux (GNU): BSD keeps a
# blank address column under -An, pads every line, and left-aligns the -c row
# where GNU right-aligns it. `tidy` re-prints every field four wide, so the same
# bytes give the same picture on both. One limit: a SPACE byte prints as blanks
# under -c and would vanish, so these examples feed od inputs with no spaces.
tidy() { awk '{ for (i = 1; i <= NF; i++) printf "%4s", $i; print "" }' | sed -e '/^$/d'; }
echo "1. THE LETTER A, AS THE EIGHT BITS IT IS STORED AS"
show "printf 'A' | xxd -b"
echo
echo "2. THE SAME BYTE AS A DECIMAL, A HEX PAIR, AND A CHARACTER"
show "printf 'A' | od -An -tu1 -tx1 -c | tidy"
echo
echo "3. GOING THE OTHER WAY: WRITE THE BYTE 65 AND LET THE TERMINAL READ IT AS TEXT"
show "printf '\\101'; echo # octal escape: 101 base 8 = 65"
show "printf '\\x41'; echo # hex escape: 41 base 16 = 65"
echo
echo "4. BASH ARITHMETIC UNDERSTANDS BASES"
show "echo \$(( 2#01000001 ))"
show "echo \$(( 2#11111111 ))"
show "echo \$(( 2#11111111 + 1 )) # bash integers are 64-bit: no wrap here"
In Rust¶
Rust's name for one byte is u8 — unsigned, 8 bits. Unlike Python's int, the width is part of the type: 255u8 + 1 is not 256, it is a compile-time error in a constant and a panic in a debug build, and you have to say wrapping_add or checked_add to state what you want instead. The sibling Rust library has a full page on that: Meet the byte ↗.
Verified output of a_byte_is_eight_bits_rs.rs — regenerated by tools/run_examples.py, never hand-typed.
1. ONE VALUE, THREE SPELLINGS
decimal 65
binary 01000001 ({:08b} pads to eight digits)
as text A ({as char} reads the same byte as ASCII)
2. THE WIDTH IS PART OF THE TYPE
size_of::<u8>() = 1 byte
u8::MIN ..= u8::MAX = 0 ..= 255
255u8.checked_add(1) = None (a ninth bit does not exist)
255u8.wrapping_add(1) = 0 (asked to wrap, it wraps)
3. BINARY LITERALS AND PARSING
0b0100_0001 = 65
u8::from_str_radix("01000001", 2) = 65
4. EVERY PLACE VALUE, READ OFF THE BITS OF 200
200 = 11001000 = 128 + 64 + 8
A byte was not always eight bits¶
The title of this page is a fact about the machines that are left, not a law. "Byte" originally meant the group of bits you fetched to get one character — Werner Buchholz coined it at IBM in 1956, deliberately misspelled so nobody would read it as "bit" — and the group was whatever width that machine's characters were. Six bits on the 36-bit mainframes, which is where octal came from. Twelve on a PDP-8. The PDP-10 went further and let a program choose: a byte pointer carried the width, so the same machine handled 6-, 7-, 8- and 9-bit bytes depending on what you were reading.
IBM's System/360 settled it at eight in 1964 and the industry followed, but the standards written afterwards could not assume it. That is why the network specifications say octet and not byte: RFC 791 defines IP in octets because in 1981 a byte was still a machine-dependent quantity, and a protocol cannot be. The word looks like pedantry and is actually a date stamp.
The one place the question is still open is the language closest to the metal.
Verified output of byte_width_is_a_choice_c.c — regenerated by tools/run_examples.py, never hand-typed.
1. C HAS A NAME FOR THE NUMBER EVERYONE THINKS IS SETTLED
CHAR_BIT = 8 <limits.h> -- the bits in a char, on THIS machine
UCHAR_MAX = 255 so an unsigned char holds 0..255
The C standard requires CHAR_BIT >= 8 and no more. POSIX requires exactly 8,
which is why every machine you will meet says 8 -- by a promise the operating
system makes, not one the language does.
2. WHICH IS WHY sizeof(char) IS 1 BY DEFINITION, NOT BY MEASUREMENT
sizeof(char) = 1 <- 1 always, on every machine C has ever targeted
sizeof(int) = 4 <- and this one is measured, in units of char
On a machine with 9-bit chars, sizeof(char) would still be 1 and a char would
still hold 9 bits. sizeof counts CHARS, and a char is whatever this machine's
byte is. That is the whole reason the network standards say 'octet' instead.
3. THE BITS IN AN int, WORKED OUT RATHER THAN ASSUMED
sizeof(int) * CHAR_BIT = 4 * 8 = 32 bits
That multiplication is the portable spelling. Writing 32 is a guess that has
been right for a long time on the machines that are left.
sizeof(char) == 1 is the part worth keeping. It is true by definition rather than by measurement — sizeof counts chars, and a char is one byte of whatever this machine's byte is — so on a hypothetical 9-bit machine a char would still be 1 and would still hold 9 bits. Which means sizeof cannot tell you how many octets something is, only how many chars, and the two happen to agree everywhere you will run. That is the whole reason CHAR_BIT exists, and the reason the portable spelling of "how many bits in an int" is a multiplication rather than the number 32.
If you are coming from Python or ABAP¶
Python. You already have every piece of this lesson: format(n, '08b'), int(s, 2), bin(n), and the bytes type. What Python hides is that its int is not a byte. 65 in Python is an object of unbounded size; the byte is bytes([65])[0], which is still an int when you index it back out. The width lives in the container, never in the number. Everything in this library about "a byte" is about what sits inside bytes, bytearray, and a file opened in 'rb' mode.
ABAP. ABAP has the width in the type, the way Rust does. TYPE x LENGTH 1 is exactly one byte; xstring is a byte sequence of any length; int1 is a one-byte unsigned integer, 0..255, and i is four bytes. The trap ABAP shares with this lesson is the third reading: a byte in an x field is displayed as two hex digits, never as a character, so '41' in an x literal is the byte 65, while '41' in a c field is two characters, the digit four and the digit one. Which reading applies is decided by the type, and the next lesson is about why the hex spelling is the one ABAP chose. (ABAP claims on this page are not machine-checked — CI cannot run ABAP.)
Try it¶
cd 01_Bits_and_Bytes/a_byte_is_eight_bits/examples
python3 a_byte_is_eight_bits_py.py
bash a_byte_is_eight_bits_sh.sh
rustc --edition 2024 a_byte_is_eight_bits_rs.rs -o /tmp/byte && /tmp/byte
cc -std=c11 -Wall -Wextra byte_width_is_a_choice_c.c -o /tmp/bw && /tmp/bw
Then, without the machine: write 100 as eight bits. Write 0111 1111 as a number. Check both with format(100, '08b') and int('01111111', 2).
Practice¶
One byte, four readings. Here is a byte: 1010 1100. Without a converter, write down four things — its value as an unsigned number, the same value in hex, its value read as a signed 8-bit number, and the character Latin-1 would draw for it.
Then answer the question the four are really for: which of the four does the byte itself decide?
Answers
Verified output of a_byte_is_eight_bits_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.
the byte 10101100
unsigned 172
hex 0xac (1010 1100 -- one digit per nibble)
signed (two's compl.) -84 (top bit set, so 172 - 256)
as Latin-1 text '¬'
Three of those four are the same fact written four ways: 172 and 0xac
and 1010 1100 are one number in three notations, and nothing had to be
decided to get from any of them to any other.
The fourth one is a DECISION. -84 is not hiding inside the switches; it
is what you get if somebody agreed in advance that the top bit means a
sign. Ask C: `char` is signed on most machines and unsigned on some, and
the same byte then holds 172 or -84 with no cast anywhere in sight.
So the answer to 'which of the four does the byte decide?' is NONE of
them. It holds eight switches. Everything else is a reader's agreement.
See also¶
- Hex is a shorthand — the same eight bits written as two characters instead of eight
- Reading a hex dump — a whole file, one byte at a time
- Meet the byte ↗ — the Rust library's page on
u8, overflow, and why.len()counts bytes