thecodex.expert  ·  The Codex Family of Knowledge
CS Foundations

Binary & Number Systems

Why computers count in 1s and 0s — and how those 1s and 0s represent integers, negative numbers, fractions, and text.

Foundations Requires: How Computers Work ~18 min read Last verified:
Canonical Definition

A binary number system is a positional numeral system with a radix of 2, using only the digits 0 and 1, in which each digit position represents a successive power of 2 — the numeral system used by all digital computers because the two digits correspond directly to the two stable states (on/off) of the transistors from which computers are built.

One sentence that captures it

Computers count in binary — using only 1s and 0s — not because binary is elegant, but because every switch inside a computer has exactly two positions: on and off.

You already understand positional notation

The number system most people use every day is decimal — base 10. It uses ten digits: 0 through 9. The value of each digit depends on its position: the rightmost digit represents ones, the next column represents tens, the next hundreds, and so on.

The number 347 means: 3 × 100, plus 4 × 10, plus 7 × 1. Each column is a power of 10: 10⁰ = 1, 10¹ = 10, 10² = 100. We use ten symbols because humans have ten fingers. There is nothing mathematically special about base 10.

Binary: the same idea, base 2

Binary works identically — except there are only two digits (0 and 1) and each column represents a power of 2 instead of 10.

The binary number 1011 means: 1 × 8, plus 0 × 4, plus 1 × 2, plus 1 × 1 = 8 + 0 + 2 + 1 = 11 in decimal.

Every light switch is either on or off. Computers use billions of switches — instead of light, each stores a single piece of information: on means 1, off means 0. Counting in binary is the natural language of a machine made of switches.

Reading and writing binary

Converting binary to decimal: write out the powers of 2 from right to left, multiply each digit by its column value, and add them up.

Binary to decimal conversion examples
BinaryCalculationDecimal
000111
001022
01014 + 15
100088
11118 + 4 + 2 + 115
11111111128 + 64 + 32 + 16 + 8 + 4 + 2 + 1255

Eight binary digits (8 bits) form one byte. A byte can represent 2⁸ = 256 different values (0 through 255). Two bytes = 16 bits = 65,536 values. Four bytes = 32 bits = over 4 billion values. Eight bytes = 64 bits = over 18 quintillion values — enough to address every byte of memory in every computer ever built.

Hexadecimal: shorthand for binary

Binary numbers get long fast. A memory address in a modern 64-bit computer requires 64 binary digits to write out. To keep things readable, programmers use hexadecimal — base 16. It uses sixteen digits: 0–9, then A–F (where A = 10, B = 11, C = 12, D = 13, E = 14, F = 15).

Four binary digits map exactly to one hexadecimal digit. The binary number 1010 1111 is AF in hexadecimal. Memory addresses in code often appear as 0xFF3A00C4 — the 0x prefix signals hexadecimal. This is the same number as 4281352388 in decimal, but 0xFF3A00C4 is far easier for a human to read and cross-reference against documentation.

Representing negative numbers

Binary digits are just 0s and 1s — there is no minus sign. So how does a computer store the number −5?

The answer is two's complement. In an 8-bit number, the leftmost bit is used as a sign bit. If it is 0, the number is positive. If it is 1, the number is negative. The range is −128 to +127. The decimal number 5 is 00000101 in 8-bit binary. The number −5 in two's complement is 11111011.

This encoding has a useful property: adding a positive and a negative number works correctly using ordinary binary addition, with no special case needed. The hardware that adds two positive numbers can also subtract, for free.

Representing fractions: floating point

Integers are straightforward. But how does a computer store 3.14159 or 0.0000001?

Computers use floating-point representation — a binary equivalent of scientific notation. A number like 3.14 is stored as a sign, a significand (the significant digits), and an exponent (the power of 2 to multiply by). Most computers use the IEEE 754 standard, which specifies exactly how 32-bit and 64-bit floating-point numbers are stored.

Common mistake

Floating-point numbers are not exact. The decimal fraction 0.1 cannot be represented exactly in binary floating point — just as 1/3 cannot be represented exactly in decimal. This causes surprising results: in most programming languages, 0.1 + 0.2 does not equal exactly 0.3. This is not a bug — it is the mathematically correct result of IEEE 754 arithmetic. Never use floating-point numbers to represent money.

How this appears in programming

Every integer variable in every programming language is a sequence of bits. In Python, int values are stored with arbitrary precision (as many bits as needed). In C, int is typically 32 bits, long long is 64 bits. In Java, int is always exactly 32 bits and long is always exactly 64 bits.

In many languages, binary and hexadecimal literals can be written directly in code:

Python number_bases.py
decimal     = 255        # Base 10 — the default
binary      = 0b11111111 # Base 2  — prefix: 0b
octal       = 0o377      # Base 8  — prefix: 0o
hexadecimal = 0xFF       # Base 16 — prefix: 0x

# All four refer to the same value: 255
print(decimal == binary == octal == hexadecimal)  # True

# Converting between bases
print(bin(255))   # '0b11111111'
print(hex(255))   # '0xff'
print(oct(255))   # '0o377'
print(int('11111111', 2))  # 255 (parse binary string)
print(int('FF', 16))       # 255 (parse hex string)

Positional numeral systems: formal definition

A positional numeral system with radix b represents a number as a sequence of digits dndn-1...d1d0, where each digit di satisfies 0 ≤ di < b, and the value of the sequence is:

Value = Σ (di × bi) for i = 0 to n

For decimal (b=10), binary (b=2), octal (b=8), and hexadecimal (b=16), this same formula applies. The choice of base is a representation convention — the underlying mathematical value is the same.

Binary arithmetic

Binary addition follows the same rules as decimal addition, with carry:

Binary addition truth table
ABSumCarry
0000
0110
1010
1101

Adding 0110 (6) and 0111 (7): working right to left: 0+1=1, 1+1=10 (write 0 carry 1), 1+1+1=11 (write 1 carry 1), 0+0+1=1. Result: 1101 = 13. Correct.

Bitwise operations — AND, OR, XOR, NOT, and bit shifts — operate on individual bits and are fundamental to systems programming, cryptography, and graphics:

Python bitwise.py
a = 0b1010  # 10
b = 0b1100  # 12

print(bin(a & b))   # 0b1000  (8)  — AND: both bits must be 1
print(bin(a | b))   # 0b1110  (14) — OR:  either bit is 1
print(bin(a ^ b))   # 0b0110  (6)  — XOR: bits differ
print(bin(~a))      # -0b1011 (-11) — NOT: flip all bits (two's complement)
print(bin(a << 1))  # 0b10100 (20) — left shift: multiply by 2
print(bin(a >> 1))  # 0b101   (5)  — right shift: integer divide by 2

Hexadecimal in practice

Four binary bits map exactly to one hexadecimal digit because 2⁴ = 16. This makes hexadecimal a compact human-readable representation of binary data. The correspondence:

Binary to hexadecimal mapping
BinaryHexDecimalBinaryHexDecimal
000000100088
000111100199
0010221010A10
0011331011B11
0100441100C12
0101551101D13
0110661110E14
0111771111F15

Hexadecimal appears throughout programming: memory addresses (0x7FFD3A40), RGB colours (#FF6633), Unix file permissions, network MAC addresses, SHA256 hashes, and UUID identifiers all use hex notation.

Two's complement: negative integers

Two's complement is the universal method for representing signed integers in hardware. For an n-bit integer:

  • The range is −2n−1 to 2n−1 − 1
  • For 8 bits: −128 to 127
  • For 16 bits: −32,768 to 32,767
  • For 32 bits: −2,147,483,648 to 2,147,483,647
  • For 64 bits: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

To find the two's complement representation of a negative number, take the positive value, flip all bits (one's complement), then add 1:

Example twos-complement.txt
Find -5 in 8-bit two's complement:
  Step 1: +5 in binary:     0000 0101
  Step 2: Flip all bits:    1111 1010  (one's complement)
  Step 3: Add 1:         +         1
  Result: -5 two's comp:    1111 1011

Verify: -5 + 5 should = 0
  1111 1011
+ 0000 0101
-----------
  0000 0000  (carry discarded — correct!)

Why two's complement over simpler sign-magnitude representations? Because addition of two's complement numbers requires no special case — the same adder circuit works for both positive and positive, and positive and negative. Subtraction becomes addition of the negative. There is only one representation of zero (sign-magnitude has +0 and −0). Hardware is simpler and faster.

IEEE 754 floating-point representation

The IEEE 754 standard (IEEE Std 754-2019) defines how real numbers are approximated in binary. A 32-bit single-precision float consists of:

  • 1 sign bit — 0 for positive, 1 for negative
  • 8 exponent bits — stored with a bias of 127 (to represent both positive and negative exponents)
  • 23 mantissa bits — the fractional part of the significand (with an implicit leading 1)

The value is: (−1)sign × 2exponent − 127 × 1.mantissa

The number 0.1 in decimal cannot be represented exactly in binary because it is a repeating fraction in base 2 (just as 1/3 is a repeating decimal: 0.3333...). The closest representable 32-bit float to 0.1 is approximately 0.100000001490116119384765625. When you add 0.1 and 0.2 in a computer, the rounding errors in both representations accumulate, and the result is not exactly 0.3.

Python float_precision.py
print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)   # False

# For exact decimal arithmetic, use Python's Decimal module
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2'))          # 0.3
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))  # True

# IEEE 754 special values
import math
print(float('inf'))   # inf
print(float('nan'))   # nan
print(math.isnan(float('nan')))  # True

IEEE 754 also defines special values: positive and negative infinity (produced by dividing by zero for non-zero values), Not a Number (NaN, produced by 0/0 or sqrt(−1)), and positive and negative zero. NaN is the only value not equal to itself: float('nan') == float('nan') is False in every IEEE 754-compliant language.

Commonly confused
Floating-point error is not a programming mistake. The result 0.30000000000000004 from 0.1 + 0.2 is the mathematically correct answer given IEEE 754 double-precision arithmetic. It is not a language bug. It is the inevitable consequence of representing infinite real numbers in finite binary storage.
Two's complement is not the only way to represent negative numbers. Sign-magnitude (one bit for sign, remaining bits for magnitude) and one's complement are alternatives. Two's complement is used in all modern hardware because of its arithmetic efficiency, but programmers working with network protocols or older data formats may encounter sign-magnitude representations.
Hexadecimal is not a different kind of number — it is the same number written differently. The value 255, 0xFF, 0b11111111, and 0o377 are four representations of the same mathematical quantity. The representation affects how it looks to a human; it does not affect how the computer stores or operates on it.
Overflow is not the same as a floating-point error. Integer overflow occurs when an arithmetic result exceeds the range of the integer type (e.g., adding 1 to 2,147,483,647 in a 32-bit signed integer wraps to −2,147,483,648). Floating-point imprecision is different — it is rounding within a finite representation. Both are distinct failure modes requiring different handling.
How this connects
Requires first
Enables

IEEE 754-2019: formal specification

The IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019, revision of the 1985 original) defines five basic formats: binary16, binary32, binary64, binary128, and decimal128. The binary32 (single-precision) and binary64 (double-precision) formats are used by virtually all hardware and programming languages.

Binary64 (double-precision) structure:

  • 1 sign bit (s)
  • 11 exponent bits (e), biased by 1023
  • 52 mantissa bits (f), with implicit leading 1 for normalised values

Normalised value: (−1)s × 2e − 1023 × 1.f

Range: approximately ±5 × 10−324 to ±1.8 × 10308. Precision: ~15–17 significant decimal digits.

Special encodings in binary64:

IEEE 754 special values
Exponent bitsMantissa bitsValue
all 0all 0±0 (signed zero)
all 0nonzerosubnormal (denormalised) number
all 1all 0±∞
all 1nonzeroNaN (quiet or signalling)
otheranynormalised number

Subnormal (denormalised) numbers extend the range near zero at the cost of reduced precision. They allow gradual underflow — the result of a very small computation does not abruptly become zero, but loses mantissa bits gradually.

Two's complement: formal arithmetic properties

For an n-bit two's complement integer, the most negative value has no positive counterpart — it is its own two's complement. For 8-bit integers: −128 represented as 10000000. Negating −128 in 8 bits produces 10000000 again (overflow). This asymmetry is a formal consequence of the range −2n−1 to 2n−1 − 1.

Two's complement addition is modular arithmetic modulo 2n. Overflow occurs when the true mathematical result falls outside the representable range. In C, signed integer overflow is undefined behaviour (ISO C17, §6.5). In Java, signed integer overflow wraps predictably (Java Language Specification §15.18.2). In Python, integers have arbitrary precision — overflow cannot occur.

Number representation in the context of data types

The connection between number systems and data types in programming languages:

Integer data types across languages with binary representation
TypeLanguageBitsRangeOverflow behaviour
intC (typical)32−2³¹ to 2³¹−1Undefined behaviour (ISO C17)
intJava32−2³¹ to 2³¹−1Wraps (modular)
intPythonArbitraryUnlimitedNo overflow
i32Rust32−2³¹ to 2³¹−1Panic in debug; wraps in release
uint8Go80 to 255Wraps (modular)
float64Go / Java / C64~±1.8×10³⁰⁸IEEE 754 (±∞, NaN)

Unicode and UTF-8: text as binary

The Unicode standard (The Unicode Consortium, The Unicode Standard, current version at unicode.org/standard/standard.html) assigns a unique integer — a code point — to every character in every writing system: Latin, Devanagari, Chinese, Arabic, mathematical symbols, emoji. Code points are written in hexadecimal: U+0041 is Latin capital letter A (decimal 65). The full Unicode code space covers 1,114,112 code points (U+0000 to U+10FFFF).

UTF-8 (RFC 3629) encodes Unicode code points as variable-length sequences of 1 to 4 bytes. ASCII characters (U+0000 to U+007F) are encoded as single bytes identical to their ASCII values — making UTF-8 backward-compatible with ASCII. Code points above U+007F use multi-byte sequences with specific bit patterns identifying the sequence length.

Specification reference

IEEE Std 754-2019 — IEEE Standard for Floating-Point Arithmetic. IEEE Computer Society, July 2019. Available at ieeexplore.ieee.org. This is the authoritative specification for floating-point arithmetic on all modern hardware and in all conforming programming languages.

How this connects
Requires first
Enables
Formal sources
Source confidence: High Last verified: Primary source: IEEE Std 754-2019

Sources

1
IEEE Std 754-2019. IEEE Standard for Floating-Point Arithmetic. IEEE Computer Society, July 2019. ieeexplore.ieee.org. — Authoritative specification for all floating-point arithmetic, including binary32 and binary64 formats, special values, and rounding modes.
2
Patterson, D. A. & Hennessy, J. L. (2020). Computer Organization and Design. 6th ed. Morgan Kaufmann. — Reference for binary representation, two's complement arithmetic, and integer data types.
3
Tanenbaum, A. S. & Austin, T. (2012). Structured Computer Organization. 6th ed. Pearson Education. — Reference for number system conversion, hexadecimal notation, and memory representation.
4
The Unicode Consortium. The Unicode Standard. Current version. unicode.org/standard/standard.html. — Authoritative specification for Unicode code points and UTF-8 encoding.
5
ISO/IEC 9899:2018. Programming languages — C (ISO C17). International Organization for Standardization. — Authoritative source for C integer type ranges and signed integer overflow undefined behaviour (§6.5).