Everything inside a computer is ultimately bits — yet we routinely store negative numbers, fractions, Chinese characters, and emoji. The bridge between raw bits and meaning is encoding: an agreed-upon convention for what a pattern of bits represents. This chapter covers the encodings every engineer eventually trips over.
Integers
Unsigned integers are the straightforward case — plain binary, where 8 bits cover 0 to 255. The interesting question is how to represent negative numbers, and the answer the whole industry settled on is two's complement, where the most significant bit carries the sign:
To negate a number, flip all its bits and add 1. The reason two's complement won is elegant: addition works identically for positive and negative values, so the CPU needs only one adder circuit:
5 + (-3):
00000101 + 11111101 = 00000010 = 2 ✓ (the carry out is discarded)
Analogy: Two's complement is an odometer rolling over. At
00000, subtracting 1 gives99999— which stands in for −1. The arithmetic just works with the wraparound.
Integer Overflow
That same wraparound is a hazard. When a value exceeds its type's range, it silently rolls over:
8-bit unsigned: 255 + 1 = 0 (wraps to the bottom)
8-bit signed: 127 + 1 = -128 (wraps to the most negative)
This has caused real disasters: the Ariane 5 rocket exploded in 1996 when a 64-bit float was forced into a 16-bit integer; Pac-Man's level 256 "kill screen" is an 8-bit counter overflowing; and YouTube once had to widen its view counter when "Gangnam Style" blew past 2³². Overflow bugs are silent until they aren't.
Floating Point (IEEE 754)
How do you store 3.14 or 0.000001 in binary? The same way science writes very large and very small numbers — scientific notation — split into a sign, an exponent, and a mantissa:
This buys enormous range at the cost of exactness. The famous gotcha:
>>> 0.1 + 0.2
0.30000000000000004 # not exactly 0.3!
The reason: 0.1 in binary is a repeating fraction (0.0001100110011…), just as 1/3 is 0.333… in decimal — it can't be stored exactly in a finite number of bits. There are also special values: ±Infinity, and NaN (Not a Number), which is famously not equal to itself by design.
Analogy: Floating point is a ruler with fixed tick marks. It spans a huge range, but you can only land on the marks — most real lengths fall between them and get rounded.
Never use floats for money. Rounding errors compound into real financial bugs. Use integer cents or a dedicated decimal type:
BAD: $10.10 as a float → may become 10.099999999
GOOD: 1010 cents as an int → always exact
Character Encoding
ASCII (1963) used 7 bits for 128 characters — enough for English, and nothing else. 'A' is 65, 'a' is 97, '0' is 48. But the world writes in 中文, العربية, and 🎉.
Unicode solves this by assigning every character in every script a unique number called a code point — 'A' is U+0041, '中' is U+4E2D, '🎉' is U+1F389 — over 150,000 and counting. Unicode is the catalog; it doesn't say how to store those numbers as bytes. That's the job of an encoding, and the one that won is UTF-8:
UTF-8 won because it's backward-compatible with ASCII (English text is unchanged), has no byte-order ambiguity, stays compact, and is self-synchronizing — you can jump into the middle of a stream and find character boundaries. The practical trap is that "length" is ambiguous: "🎉".length is 2 in JavaScript (UTF-16 code units), 1 in Python 3 (code points), and 4 if you count UTF-8 bytes. Conflating bytes, code points, and characters is the source of countless string bugs.
Endianness
A final low-level convention: when a multi-byte value sits in memory, which byte comes first?
x86/x64 CPUs are little-endian; network protocols standardized on big-endian ("network byte order"); ARM is configurable. Mismatches here corrupt data silently when it crosses between systems.
Analogy: It's how you write a date. Month/day/year puts a less-significant field first; ISO 8601's year-month-day puts the most-significant first. Same information, opposite ordering — and you'd better agree which one you're using.