skylib index
A working introduction · binary · hexadecimal · bytes

Two States only

Every number, letter and instruction in a computer is built from one trick repeated: a thing that can be in exactly two states. Everything else — binary, hex, bytes, text — is notation we invented so humans can read the result. Start by touching one.

One byte · click any switch

Binary0000 0000base 2 — the real thing
Hexadecimal0x00base 16 — shorthand for the above
Decimal0base 10 — what you grew up with
As textNULif read as a character
BinaryBase 2. What is physically there. Only ever 0 or 1.
HexBase 16. Not a new number system — a compact way to write binary.
DecimalBase 10. Convenient for humans, awkward for machines.
TextThe same number, looked up in a table of characters.
PhysicalVoltage, charge, magnetism. The thing the notation describes.
01

A bit is a physical thing

The only real idea on this page · everything else is notation

A bit is not the number 0 or the number 1. A bit is something in the world that can be in two clearly different states, and we agreed to call those states 0 and 1.

Why two? Because two is the easiest number of states to build reliably. If you try to build a component that holds ten distinguishable voltage levels, small amounts of heat, interference and manufacturing variation start making level 6 look like level 7. If you only have to tell high from low, you can be wrong by an enormous margin and still get the right answer. Two states is the engineering choice that makes everything else possible.

WhereState we call 0State we call 1
RAMcapacitor dischargedcapacitor charged
CPU registerlow voltage (~0 V)high voltage (~1 V)
SSDno charge trapped in the cellcharge trapped
Hard diskmagnetised one waymagnetised the other
Network cableone signal level or phasethe other
CD / DVDflat surfacea pit that scatters the laser

A refinement, because that table is simpler than reality. Modern SSDs and fast network links cheat. Consumer flash is usually TLC or QLC — 8 or 16 distinguishable charge levels per cell, storing three or four bits at once. Gigabit Ethernet uses five signal levels, 10-gigabit uses sixteen, PCIe 6.0 uses four. Each of them accepts exactly the fragility described above, and pays for it with heavy error correction, because density and bandwidth are worth the trouble.

But watch what happens the moment the signal is recovered: it is converted straight back into plain bits, and everything above that point is two-state again. Two states is not where the physics stops — it is what everything is normalised to. Which is a stronger claim than the one the table makes on its own.

Nothing in that table is a number. Charge, voltage, magnetism, a physical dent in plastic. The number is what we project onto it. When people say a computer "works in ones and zeros," this is what is actually meant: it works in two reliably distinguishable physical conditions, and we find it convenient to write them down as 0 and 1.

One bit answers exactly one yes-or-no question. That's it. To hold anything more interesting you need more of them — and the whole rest of this page is about what happens when you line bits up in a row.

02

Counting with bits

You already know how this works · you just do it in tens

Binary is not exotic. It is the counting system you already use, with one number changed.

Look at how you read the decimal number 3 0 7. You don't treat those as three separate digits — you read each position as being worth ten times the one to its right:

Decimal · each position is worth 10× the one to its right
307
3 × 100  = 300
0 ×  10  =   0
7 ×   1  =   7
─────────────
total   = 307
The place values are 100, 10, 1 — that is 10², 10¹, 10⁰. Ten, because we have ten fingers. There is nothing else special about it.

Binary does exactly the same thing with two instead of ten. Each position is worth twice the one to its right: 1, 2, 4, 8, 16, 32, 64, 128. And because each digit can only be 0 or 1, you never multiply — you just decide whether to include that place value or not.

Binary · each position is worth 2× the one to its right
10011
1 × 16  = 16   ← include
0 ×  8  =  0
0 ×  4  =  0
1 ×  2  =  2   ← include
1 ×  1  =  1   ← include
─────────────
total  = 19
Adding up the place values wherever there is a 1. That is the entire conversion from binary to decimal. Scroll back to the byte at the top of the page — the running sum under the switches is doing exactly this.

Watch it count

The clearest way to feel binary is to watch it tick over. Step this forward and notice something: the rightmost bit flips on every step, the next one every 2 steps, the next every 4, the next every 8. Each column flips half as often as the one to its right.

Binary counter

decimal 0  ·  hex 0x00

Press +1 and watch which switches change.

Carrying works the same as it does in decimal. When you count 7 → 8 → 9 in decimal, the next step forces a carry: 9 rolls to 0 and the column to its left goes up by one. Binary just runs out of digits much sooner — it carries after 1 instead of after 9. That is why 0111 + 1 becomes 1000: three columns roll over at once.

03

Eight bits make a byte

256 possibilities · numbered 0 to 255

One bit gives you two possibilities. Add a second and you get four, because the second bit doubles every option the first one had. Every bit you add doubles the total.

How much can 8 bits hold?
Bits8
Combinations2562 × 2 × 2 …
Counts from0 – 255zero is one of them
Written as hex2 digits

Why 256 stops at 255. Eight bits give you 256 different patterns, and the first one is 00000000 — which we read as zero. So the patterns are numbered 0 through 255, not 1 through 256. This off-by-one is the source of an enormous amount of real-world weirdness, and it is why the largest value a single byte can hold is 255.

You met that limit on the previous page without knowing it. Python's bytecode uses two bytes per instruction: one for the opcode, one for its argument. Because the argument is a single byte, it cannot exceed 255 — so when an instruction needs a bigger number, the compiler emits an extra EXTENDED_ARG instruction in front of it to supply the higher bits. That whole mechanism exists because of the number in the table above.

Why eight in particular?

Partly history — IBM's System/360 settled on it in 1964 and the industry followed. But it stuck because eight is genuinely convenient: it is a power of two (so bytes tile neatly into larger units), it splits evenly in half (two groups of four, which matters enormously in the next section), and 256 values is just enough to cover the Latin alphabet in both cases, the digits, punctuation and a pile of control codes.

04

Hexadecimal, decoded

The one thing to understand · hex is not a new number system

Hex looks like a foreign language and is usually taught as one. It isn't. Hex is a compression scheme for writing binary down — and once you see the mechanism, there is essentially nothing left to learn.

Start with the problem. Here is one byte in binary:

11010010

That is miserable to read, worse to say aloud, and very easy to miscount. The obvious fix is to write it in decimal — 210 — but decimal creates a new problem: 10 is not a power of 2, so decimal digits do not line up with bit boundaries. Change one bit and the decimal number changes unpredictably. You cannot look at 210 and see which bits are on.

So pick a base that is a power of two. Sixteen is 2⁴ — which means one hex digit encodes exactly four bits, always. Not approximately. Not usually. Exactly, every time.

The whole trick. Cut the byte down the middle into two groups of four bits. Each group of four has 16 possible patterns (0000 through 1111), so each gets its own single symbol. Two symbols, one byte, no arithmetic required. A group of four bits is called a nibble — half a byte, and yes that is the actual technical term.

Split a byte into two nibbles
High nibble · left four
1101
D
= 13 in decimal
Low nibble · right four
0010
2
= 2 in decimal

0xD2  =  210

Click the switches. Notice that changing a bit on the left never touches the right hex digit — the two halves are completely independent. That is the property decimal cannot give you.

Where the letters come from

A nibble has 16 possible values, and we need 16 distinct symbols to name them. We only have ten digit characters, 0 through 9. So the remaining six borrow the first six letters of the alphabet: A is ten, B is eleven, and so on up to F, which is fifteen. They are not letters in any meaningful sense — they are digits that happen to be shaped like letters, because we ran out of digit shapes.

All sixteen nibbles · the entire table you need

You do not need to memorise this. Four patterns carry the rest: 8 is 1000, 4 is 0100, 2 is 0010, 1 is 0001. Everything else is those added together — D is 8+4+1, so 1101.

Converting between all three

Type into any of these and the other two follow. The bottom row shows why hex is worth the trouble: the binary is grouped into nibbles, each sitting directly under the hex digit that names it.

Three notations, one number

What 0x means. Nothing at all — it is punctuation. Since 10 could be ten in decimal or sixteen in hex, the prefix 0x announces "read what follows as hex." Binary gets 0b for the same reason. On the previous page, 0xC0000000 and 0x15faeb8 were just numbers wearing a label that said how to read them.

05

Bytes as text

A number becomes a letter only because a table says so

There is nothing letter-like about the byte 01000001. It becomes the letter A because in 1963 a committee wrote down that 65 means A, and everyone agreed to use the same list.

That list is ASCII. It assigns a number to each of 128 characters — the Latin alphabet in both cases, the digits, punctuation, and control codes like newline. A few anchor points are worth knowing because they show up constantly once you start reading bytes:

CharacterDecimalHexBinaryWhy it matters
space320x200010 0000First printable character
0480x300011 0000Digits run 48–57, so the low nibble is the digit
A650x410100 0001Uppercase runs 65–90, alphabetically
a970x610110 0001Exactly 32 more than A — one single bit
newline100x0a0000 1010The \n at the end of printed output

Look at A and a again. 0100 0001 and 0110 0001. They differ in exactly one bit — the one worth 32. That is not a coincidence; the ASCII designers arranged it deliberately so that changing case is a single bit flip rather than a table lookup. Whole categories of software still exploit this.

Type something and watch it become bytes

Text → bytes

Beyond 127: UTF-8. ASCII only defines 128 characters, which does not get you far outside English. UTF-8 extends it by letting a character use several bytes — but it was designed so the original 128 keep their exact old values as single bytes. That is why the encoder above shows café as five bytes for four characters: the é needs two, while c, a and f are unchanged since ASCII first assigned them. (Uppercase letters were in the original 1963 standard; lowercase only arrived with the 1967 revision — so the A in the table above is four years older than the a beneath it.) Type an emoji and you will most often see four, though a few older ones — and among them — need only three.

Which brings back a line from the previous page. Python printed total = 5 and the bytes handed to the kernel were:

74 6f 74 61 6c 20 3d 20 35 0a

You can now read that directly. 74 is 116, which ASCII says is t. 6f is 111 — o. 20 is 32, a space. 35 is 53, the character 5not the number five, which would be 0x05. And 0a is 10, the newline. Paste total = 5 into the encoder above and the first nine bytes come back exactly; the tenth, 0a, is the newline print adds for you and cannot be typed into a single-line box.

06

Doing things to bits

Six operations · they work on every column independently

Once numbers are rows of bits, you can operate on the bits directly rather than on the number they represent. These operations are strange the first time and obvious the second.

The key thing about AND, OR and XOR is that each column is decided entirely on its own. Bit 3 of the answer depends only on bit 3 of the two inputs. There is no carrying, no interaction between columns, nothing like ordinary arithmetic.

Bitwise playground
A
B
A AND B

Click any switch in row A or B to change it.

What each one is actually for

OperationRuleWhat it is used for
AND &1 only if both are 1Masking — keeping some bits and forcing the rest to zero. x & 0b00001111 keeps the low nibble and discards the high one.
OR |1 if either is 1Setting — turning specific bits on without disturbing the others.
XOR ^1 if exactly one is 1Toggling and difference. XOR with the same value twice returns the original — the basis of a lot of cheap encryption and checksums.
NOT ~flip every bitInverting a mask, and building negative numbers.
<< leftslide bits leftMultiply by 2 per position. Bits that fall off the left are gone.
>> rightslide bits rightDivide by 2 per position, discarding the remainder.

Why shifting multiplies. It is the same reason adding a zero to the end of a decimal number multiplies it by ten. Each place is worth twice the one to its right, so sliding every bit one position left doubles every place value at once. 0000 0101 is 5; shift it left and you get 0000 1010, which is 10.

A mask is just a stencil. 15 in binary is 0000 1111. AND-ing anything with it gives zero everywhere the stencil is zero, and leaves the original bits everywhere the stencil is one. So x & 15 means "give me only the bottom four bits of x." Try it in the playground — set B to 15 and pick AND.

07

Packing several things into one number

Where shifts and masks earn their keep · and where the last page suddenly makes sense

If you have eight bits and two facts to store, you do not need two bytes. You can give each fact its own stretch of bits and keep them in one.

This is why shifts and masks exist. To pack, you shift a value left until it sits in its own territory and OR it in. To unpack, you shift right to bring your field down to the bottom, and mask to discard everything above it.

Unpack a packed number
split at bit
High fieldvalue >> 31
Low fieldvalue & 74

The two you have already seen

On the previous page, a CPython integer stored its digit count and its sign in one field called lv_tag. Reading the memory of the integer 2 gave 12, and the page decoded it as "one digit, positive, plus a flag bit." Now you can do that yourself: 12 is 0000 1100. Shift right by 3 and you get 1 — one digit. The three bits left behind hold the sign and a flag.

The second one is smaller and neater. The instruction LOAD_FAST_BORROW_LOAD_FAST_BORROW loads two local variables at once, so it needs to name two of them in a single argument byte. It splits the byte in half: the high nibble is the first variable's slot number, the low nibble is the second's. The mini-interpreter on that page unpacked it in one line — local[arg >> 4] and local[arg & 15] — which is exactly the operation above with the split moved to bit 4.

Why bother. Partly memory: a Python program may hold millions of integers, and a saved word each is real. But mostly it is speed — pulling two fields out of one number that is already in a CPU register costs a couple of instructions, while reading a second number from memory can cost a hundred times that. The packing exists to avoid touching RAM.

08

Why powers of two are everywhere

256 · 1024 · 65536 · once you see them you cannot stop

Odd-looking numbers turn up constantly in computing — 256, 1024, 65535, 4096. None of them are arbitrary. Each is the number of things a particular count of bits can address.

Powers of two · hover a row

The KB / KiB annoyance. A kilobyte "should" be 1000 bytes, because kilo means thousand. But 1000 is not a power of two and 1024 is, so 1024 was what hardware actually gave you and the industry called it a kilobyte anyway. The units KiB, MiB, GiB were invented later to mean the powers of two unambiguously. This is why a "500 GB" drive shows up as roughly 465 GiB — nothing was stolen, the two numbers just count differently.

Reading a sentinel

One more from the previous page. Python marks immortal objects — the ones that must never be freed — by putting a specific value in their reference count field, and sys.getrefcount(2) returned 3221225472. As a decimal number that is meaningless. In hex it is 0xC0000000, and C is 1100, so the whole thing is:

1100 0000 0000 0000 0000 0000 0000 0000

Just the top two bits of a 32-bit field, and nothing else. That is the point of choosing it: a genuine reference count is a small number sitting at the bottom of the field, so a value with only the highest bits set can never be mistaken for one. The sentinel was picked to be structurally impossible, and you can see that at a glance in hex while decimal hides it completely.

──

Go back and read it

The eighteen bytes you could not read an hour ago

The other page opened a Python function and dumped its compiled bytecode as eighteen raw bytes. Here it is again.

80 00 57 01 2c 00 00 00 00 00 00 00 00 00 00 00 23 00

Each pair of hex characters is one byte, so eighteen pairs is eighteen bytes. Instructions come in twos — an opcode and its argument — so this is nine slots. And you can now convert every one of them:

HexBinaryDecimalWhat it is
801000 0000128opcode 128 — RESUME
570101 011187opcode 87 — LOAD_FAST_BORROW_LOAD_FAST_BORROW
010000 00011its argument — high nibble 0, low nibble 1, so locals 0 and 1
2c0010 110044opcode 44 — BINARY_OP
000000 00000ten of these — the empty inline cache
230010 001135opcode 35 — RETURN_VALUE

Nothing was hidden from you. The bytes were always just numbers, written two hex digits at a time because that happens to be exactly one byte, and every one of them is a row of eight switches that are either on or off.

One habit worth forming. When you meet an unfamiliar number in a technical context, convert it to hex and look at the shape. 3221225472 tells you nothing; 0xC0000000 tells you it is a deliberately chosen sentinel. 65535 is forgettable; 0xFFFF is obviously "every bit in two bytes." Hex makes structure visible, and structure is usually the answer to the question you were asking.