Integers and Endianness
Binary File Formats in Practice
Chapter 2 ยท Integers and Endianness
Most fields in a binary format are numbers: a length, a width, a count, a timestamp. A number larger than 255 needs more than one byte, which raises two questions every format must answer: which byte comes first, and how are negative numbers stored? This chapter answers both using only Python's built-in integer methods.
From Bytes to an Integer
int.from_bytes(raw, byteorder) turns a group of bytes into one integer, and n.to_bytes(length, byteorder) does the reverse. You always say how many bytes you want and which order to use. The same two bytes give different answers depending on the order:
The length is a promise about the file layout, not a suggestion. If the number does not fit, Python refuses rather than silently truncating:
"big", but older versions require it (check which version first made it optional). Writing it explicitly works everywhere and documents your intent, which matters more in file code than saving a few keystrokes.
Big and Little Endian
Endianness is the order in which a multi-byte number's bytes are stored. Big-endian puts the most significant byte first, the way we write decimal numbers. Little-endian puts the least significant byte first. Neither is wrong; a format simply picks one and every reader must follow it.
sys.byteorder reports your own machine's native order; it printed little here, as it does on common desktop processors (check on yours). Do not use it to decide how to read a file. The file format fixes the order, whatever machine happens to be reading it.
| Order | First byte stored | 0x12345678 on disk | Seen in |
|---|---|---|---|
| big | most significant | 12 34 56 78 | PNG and many network protocols (check) |
| little | least significant | 78 56 34 12 | WAV and BMP files (check) |
A wrong guess rarely crashes. It just produces a plausible, wrong number. One easy clue is to read both ways and see which result is sensible: a width of 256 pixels is believable, 65536 times that is not.
Signed Values
By default the bytes are treated as unsigned. Most formats store negative numbers in two's complement: the top bit of the field acts as a sign, and a pattern with that bit set means the unsigned value minus 2 to the power of the bit count. Pass signed=True to both methods to use it.
The same two bytes ff fe are 65534 or -2; nothing in the bytes themselves says which. Only the format specification does, so reading it carefully is part of the job. The range of a signed one-byte field is -128 to 127, and the byte 0x80 is the boundary where the sign flips.
Doing It by Hand
Knowing the arithmetic behind signed=True helps when you meet odd field sizes or want to check your understanding. Subtract 2 to the power of the bit count when the top bit is set:
Challenges
Start from bytes.fromhex("00 00 01 f4 ff ff ff 9c 2a 00"). Read bytes 0 to 3 as an unsigned big-endian integer, bytes 4 to 7 as a signed big-endian integer, and bytes 8 to 9 as an unsigned little-endian integer, and print the three results. Then print bytes 4 to 7 read as unsigned, and explain the difference from the signed reading.
Write the values [1, 256, 65535, 1000] to a temporary file as two-byte little-endian integers, read the file back and print the raw bytes as spaced hex. Decode the bytes in pairs both as little-endian and as big-endian, print both lists, and check that the little-endian list equals the original. Delete the file afterwards. Which value is unaffected by the wrong order, and why?
Without using signed=True, write decode_signed(raw) (big-endian, any length) and encode_signed(value, size) using only unsigned conversion and arithmetic. Verify them against signed=True for every value from -128 to 127 in one byte, then try b"\xff\xfe", b"\x80\x00\x00\x00" and the value -1 in four bytes.
Quick Reference
- Decode:
int.from_bytes(raw, "big" | "little", signed=False) - Encode:
n.to_bytes(length, "big" | "little", signed=False); raisesOverflowErrorif it does not fit - Big-endian stores the most significant byte first; little-endian stores the least significant first
- Two's complement: top bit set means unsigned value minus 2bits
- Ask of every field: size, byte order, signed or unsigned
sys.byteorderis your machine's order, not the file's
struct module, which reads and writes whole groups of fields at once from a compact format string, including how alignment and padding affect sizes.