Bytes and Hex
Binary File Formats in Practice ๐
Chapter 1 ยท Bytes and Hex
Images, audio, archives and documents are all binary files: sequences of bytes with an agreed layout. Once you can read those bytes yourself, a file format stops being a mystery and becomes a specification you can follow. This course builds that skill step by step, starting here with the raw material. All examples use Python 3 with only the standard library, so there is nothing to install.
Bytes Are Small Integers
A byte is a number from 0 to 255. Python's bytes type is an immutable sequence of them. A literal looks like a string with a b in front, and bytes that are not printable characters are shown as \x followed by two hex digits.
Notice the difference between indexing and slicing: data[0] is an int (72), while data[0:1] is a one-byte bytes object. Forgetting this is the most common early mistake, for example comparing data[0] == b"H", which is never true.
| Type | Holds | Mutable? | Example |
|---|---|---|---|
| str | characters (text) | no | "Hi" |
| bytes | bytes, 0 to 255 | no | b"Hi" |
| bytearray | bytes, 0 to 255 | yes | bytearray(b"Hi") |
Making Bytes
Besides literals, you can build bytes from a list of integers, from hex text, or by encoding a string. Values outside 0 to 255 are rejected, and a bytes object cannot be changed in place.
When you need to change bytes, use a bytearray. It behaves like bytes but allows assignment, append and similar edits. Convert back with bytes(...) when you are done.
Reading Hex and Binary
Hexadecimal (base 16) is the standard way to write bytes because one byte is exactly two hex digits, from 00 to ff. Python accepts 0x and 0b prefixes and has formatting helpers for the other direction.
Files in Binary Mode
Open a file with "rb" or "wb" and Python gives you bytes with no text decoding and no newline translation. read(n) returns the next n bytes, tell() reports your position, and seek() moves it. Binary mode matters: opening a binary file in text mode can corrupt data or raise decoding errors.
The odd-looking output is just repr choosing its quoting. Byte 9 is a tab, shown as \t, and bytes 33, 36 and 39 are the printable characters !, $ and '. Because one of them is an apostrophe, Python switched the whole display to double quotes.
Build a Hex Dump
A hex dump shows a file the way a format specification describes it: an offset, the bytes in hex, and a text column where printable characters appear and everything else is a dot. This small function is the tool you will reach for throughout the course.
The output, exactly as printed:
The first column is the offset in hex, so the second row starts at byte 16 (0x10) and the third at byte 32 (0x20). In the second row the final 2e is a real full stop, while the 0a (a newline) and 00 after it are not printable, so they show as two more dots, giving ... after "text". The padding in the last row (:<{width * 3 - 1}) keeps the text column lined up when the final chunk is short.
"rb", remember that indexing gives an int and slicing gives bytes, and use a bytearray only when you must edit in place.
Challenges
Take the string "Cafรฉ". Encode it to UTF-8 and print the bytes, print the hex with a space between bytes (look at the optional separator argument of hex()), print the length of the string and the length of the bytes, and confirm that decoding gives back the original string. Predict the two lengths first.
Write every byte value from 0 to 255 once into a temporary file, then read it back in binary mode. Print the length, how many bytes are 128 or greater, and the first four bytes. Then reopen the file, use seek to jump to five bytes before the end, and print those five bytes as spaced hex. Delete the file afterwards.
Write find_all(data, pattern) that returns a list of every offset where pattern occurs in data, including overlapping matches. Use bytes.find in a loop. Test it with b"abcabcab" and b"ab", with b"aaaa" and b"aa", and with a pattern that is not present.
Quick Reference
- bytes is immutable, bytearray is mutable; both hold values 0 to 255
- Index gives an
int; slice givesbytes - Convert:
.hex()andbytes.fromhex();str.encode()andbytes.decode() - Hex and binary:
0x..,0b..,f"{n:02x}",f"{n:08b}" - Files:
"rb"and"wb";read(n),tell(),seek(offset, whence) - Hex dump your data before you try to parse it
int.from_bytes works, what endianness means, and how signed values are stored.