Binary, Hex and Two's Complement Explained
Binary, hexadecimal and two's complement explained: place values, converting between bases, signed integers, and why 0.1 + 0.2 is not 0.3 in Python.
Every number a computer stores is a pattern of bits. Binary, octal and hexadecimal are three ways to write that pattern, and two's complement is how the pattern represents a negative number. None of it is hard once you see that all of them work the same way: each digit has a place value, and the value of the number is the sum of the digit times its place value.
This post covers the number systems underneath an intro programming course. Encodr's free Intro to Computer Science deck is a Python course, and the place where binary shows up directly is the floating-point chapter, covered near the end. The conversions and two's complement below are general computer science, not Python-specific.
Place value in any base
In decimal, the number 214 means 2 x 100 + 1 x 10 + 4 x 1. The places are powers of 10. In binary the places are powers of 2, and each digit is either 0 or 1:
| Place | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
|---|---|---|---|---|---|---|---|---|
| Power of 2 | 2^7 | 2^6 | 2^5 | 2^4 | 2^3 | 2^2 | 2^1 | 2^0 |
| Bit | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
The bits above are 11010110. Add the place values wherever there is a 1: 128 + 64 + 16 + 4 + 2 = 214. So 11010110 in binary is 214 in decimal. The number base converter shows this same working for any whole number up to 32 bits.
Decimal to binary: divide by 2
To go the other way, divide by 2 repeatedly and write down the remainders. Read them from the last one back to the first. For 13:
- 13 / 2 = 6 remainder 1
- 6 / 2 = 3 remainder 0
- 3 / 2 = 1 remainder 1
- 1 / 2 = 0 remainder 1
Reading the remainders from the bottom up gives 1101. Check: 8 + 4 + 0 + 1 = 13.
Hexadecimal and octal
Hexadecimal is base 16. It needs sixteen digits, so after 0 to 9 it uses A to F for the values 10 to 15. Octal is base 8 and uses the digits 0 to 7. The reason programmers use hex is that 16 is 2 to the 4th power, so one hex digit stands for exactly four bits.
| Decimal | Binary | Hex |
|---|---|---|
| 0 | 0000 | 0 |
| 1 | 0001 | 1 |
| 2 | 0010 | 2 |
| 3 | 0011 | 3 |
| 4 | 0100 | 4 |
| 5 | 0101 | 5 |
| 6 | 0110 | 6 |
| 7 | 0111 | 7 |
| 8 | 1000 | 8 |
| 9 | 1001 | 9 |
| 10 | 1010 | A |
| 11 | 1011 | B |
| 12 | 1100 | C |
| 13 | 1101 | D |
| 14 | 1110 | E |
| 15 | 1111 | F |
To convert binary to hex, split the bits into groups of four from the right and look up each group. Our number 11010110 splits into 1101 and 0110, which are D and 6, so it is D6 in hex. Check by place value: 13 x 16 + 6 = 214. In octal the same number is 326, because 3 x 64 + 2 x 8 + 6 = 214. A larger example: the biggest 32-bit unsigned value, 4,294,967,295, is FFFFFFFF in hex, which is thirty-two 1 bits.
Python writes these with prefixes. 0b11010110, 0xD6 and 0o326 are all the integer 214, and bin(214), hex(214) and oct(214) go the other way.
Two's complement: negative numbers in fixed width
A computer stores an integer in a fixed number of bits, so it needs a way to mark a negative number inside those bits. Two's complement is the standard answer. The top bit is the sign bit, and it works like this for an n-bit value:
- A non-negative number is its ordinary binary form, padded with zeros on the left to n bits.
- A negative number is found by writing its magnitude in n bits, inverting every bit, then adding 1.
- The range is -2^(n-1) to 2^(n-1) - 1. For 8 bits that is -128 to 127. For 32 bits it is -2,147,483,648 to 2,147,483,647.
Worked example: encode -42 in 8 bits.
- 42 in 8 bits is 00101010.
- Invert every bit: 11010101.
- Add 1: 11010110.
That is the same bit pattern as the unsigned 214 above, and that is the point. The pattern 11010110 is 214 if you read it as unsigned and -42 if you read it as signed 8-bit two's complement. The difference is 256, which is 2^8. To decode a pattern whose sign bit is 1, subtract 2^n: 214 - 256 = -42. All 1s is always -1: in 16 bits, FFFF is 65,535 unsigned and -1 signed.
A value outside the range does not fit. In 8 bits, 128 has no signed pattern, and -128 is 10000000. The converter flags out-of-range values instead of wrapping them.
Why 0.1 + 0.2 is not 0.3
Binary is also the reason for one of the best-known traps in Python. The course's floating-point chapter explains that Python stores floats in binary, and that many decimal fractions, 0.1 among them, have no exact binary form. In base 2, 0.1 is an infinitely repeating fraction, so the stored value is only a very close approximation. The difference between the stored value and the true value is called round-off error.
``python print(0.1 + 0.2) # 0.30000000000000004 print(0.1 + 0.2 == 0.3) # False print(0.5 + 0.25 == 0.75) # True ``
The last line is True because 0.5 and 0.25 are powers of two, which have exact binary forms. The rule that follows is to never compare the result of float arithmetic with ==. This is not a Python bug: the same error appears in any language that uses binary floating point. The course also notes that Python ints never overflow, while floats can, which is a different behavior from the fixed-width integers above.
How to practice this
Conversions are a skill you build by doing them, not by reading them. Convert ten numbers by hand, then check each one with the converter. Then switch to two's complement mode and encode a few negative numbers, checking the invert-and-add-one steps against the tool's working. Quizzing yourself this way is active recall, and it works better than rereading a worked example. For the rest of the course, see what's in Intro to Computer Science and how to study for Intro to Computer Science.
Number Base Converter
Convert between binary, octal, decimal and hex with the working shown, and encode or decode 8, 16 and 32-bit two's complement values.
Try it →Encodr turns this into a habit: study anything in a feed, and it schedules the rest.
Get started free