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.

What was run
Every snippet and solution in this chapter was executed, and the output shown in the comments is what Python printed.

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:

print(int.from_bytes(b"\x01\x00", "big")) # 256 print(int.from_bytes(b"\x01\x00", "little")) # 1 print((258).to_bytes(2, "big")) # b'\x01\x02' print((258).to_bytes(2, "little")) # b'\x02\x01' print((258).to_bytes(4, "big")) # b'\x00\x00\x01\x02'

The length is a promise about the file layout, not a suggestion. If the number does not fit, Python refuses rather than silently truncating:

try: (256).to_bytes(1, "big") except OverflowError as e: print("OverflowError:", e) # OverflowError: int too big to convert
Always pass the byte order
In recent Python versions the byte order argument defaults to "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.

n = 0x12345678 print(n.to_bytes(4, "big").hex(" ")) # 12 34 56 78 print(n.to_bytes(4, "little").hex(" ")) # 78 56 34 12 print(int.from_bytes(bytes.fromhex("78 56 34 12"), "little") == n) # True import sys print(sys.byteorder) # little

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.

OrderFirst byte stored0x12345678 on diskSeen in
bigmost significant12 34 56 78PNG and many network protocols (check)
littleleast significant78 56 34 12WAV 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.

print(int.from_bytes(b"\xff\xfe", "big")) # 65534 print(int.from_bytes(b"\xff\xfe", "big", signed=True)) # -2 print((-2).to_bytes(2, "big", signed=True)) # b'\xff\xfe' print((-2).to_bytes(2, "little", signed=True).hex(" ")) # fe ff try: (-1).to_bytes(1, "big") except OverflowError as e: print("OverflowError:", e) # OverflowError: can't convert negative int to unsigned for v in (0x7f, 0x80, 0xff): print(v, int.from_bytes(bytes([v]), "big", signed=True)) # 127 127 # 128 -128 # 255 -1

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:

def to_signed(value, bits): if value >= 1 << (bits - 1): value -= 1 << bits return value print(to_signed(0xfffe, 16), to_signed(0x7fff, 16), to_signed(0x80, 8)) # -2 32767 -128
Three questions for every numeric field
How many bytes is it? Which byte order? Signed or unsigned? A format description that answers all three lets you read the field with one line of Python.

Challenges

Exercise 1

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.

๐Ÿ“„ View solution
Exercise 2

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?

๐Ÿ“„ View solution
Exercise 3

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.

๐Ÿ“„ View solution

Quick Reference

  • Decode: int.from_bytes(raw, "big" | "little", signed=False)
  • Encode: n.to_bytes(length, "big" | "little", signed=False); raises OverflowError if 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.byteorder is your machine's order, not the file's
Coming next
Binary File Formats in Practice 3 introduces the struct module, which reads and writes whole groups of fields at once from a compact format string, including how alignment and padding affect sizes.