Frontend Labs
⋯

Part 1 — Beginner: The Foundations Everyone Needs

Nine chapters, each following the same shape described above, needing nothing beyond arithmetic to start. This is the foundation everything later — linear algebra, calculus, cryptography, ML math — quietly assumes you already have, whether or not anyone ever said so out loud.

1. Number Systems & Bit-Level Math

Level: Beginner You'll use this for: many bugs involving memory, money, overflow, or "why is this number wrong."

The idea

Computers store everything as binary (base 2). Humans think in decimal (base 10). Engineers read/write hex (base 16) because it maps cleanly to binary (1 hex digit = 4 bits) and is far more compact than binary for humans.

Conversions

Decimal Binary Hex
10 1010 0xA
255 11111111 0xFF
256 100000000 0x100
4096 1000000000000 0x1000
>>> bin(255)          # '0b11111111'
>>> hex(255)           # '0xff'
>>> int('ff', 16)      # 255
>>> int('11111111', 2) # 255

The table above shows the answers. Here's the actual mechanism behind each arrow — the by-hand process, so a conversion is never a black box.

Positional notation: the one idea every conversion rests on

Every number system here is positional — each digit's value depends on where it sits, not just what digit it is. In decimal, 243 means 2×10² + 4×10¹ + 3×10⁰. The base (10) tells you what each position is a power of. Binary and hex work identically, just with base 2 and base 16 instead of base 10:

binary  1010  =  1×2³ + 0×2² + 1×2¹ + 0×2⁰  =  8 + 0 + 2 + 0  =  10
hex       FF  =  15×16¹ + 15×16⁰            =  240 + 15       =  255

Every conversion method below is just a different way of applying (decimal → other base) or undoing (other base → decimal) this same place-value idea.

Decimal → Binary: repeated division by 2

Divide by 2 repeatedly, writing down the remainder (always 0 or 1) each time, until the quotient hits 0. Then read the remainders bottom to top — that's your binary number.

Converting 10 to binary:
  10 ÷ 2 = 5   remainder 0     <- read bottom-up: 1 0 1 0
   5 ÷ 2 = 2   remainder 1
   2 ÷ 2 = 1   remainder 0
   1 ÷ 2 = 0   remainder 1     <- stop, quotient is 0

Reading remainders bottom-to-top: 1010  ✓ matches the table

Why bottom-to-top? Each division peels off the least significant bit first (the 2⁰ place), the same way repeatedly dividing by 10 peels off a decimal number's last digit first. The last remainder you compute is the most significant bit, so it goes on the left.

def decimal_to_binary(n):
    if n == 0:
        return "0"
    bits = []
    while n > 0:
        bits.append(str(n % 2))   # the remainder
        n //= 2                    # the quotient, fed into the next round
    return "".join(reversed(bits))  # remainders came out least-significant-first

print(decimal_to_binary(10))    # '1010'
print(decimal_to_binary(255))   # '11111111'

Binary → Decimal: multiply each digit by its place value, then add

Reverse of the above: number the bit positions from the right starting at 0, raise 2 to each position, multiply by the bit (0 or 1), and sum.

Converting 1010 to decimal:
  position:   3  2  1  0
  bit:        1  0  1  0
  value:   1×2³ + 0×2² + 1×2¹ + 0×2⁰
         =    8 +    0 +    2 +    0
         =   10  ✓
def binary_to_decimal(bits):
    total = 0
    for position, bit in enumerate(reversed(bits)):
        total += int(bit) * (2 ** position)
    return total

print(binary_to_decimal("1010"))      # 10
print(binary_to_decimal("11111111"))  # 255

Decimal → Hex: repeated division by 16

Identical process to decimal → binary, just dividing by 16 instead of 2. The catch: a remainder can now be 0–15, and hex only has single-character digits 0-9, so remainders 10–15 are written as letters A-F.

Converting 255 to hex:
  255 ÷ 16 = 15   remainder 15 -> 'F'    <- read bottom-up: F F
   15 ÷ 16 =  0   remainder 15 -> 'F'    <- stop, quotient is 0

Reading remainders bottom-to-top: FF  ✓ matches the table
HEX_DIGITS = "0123456789ABCDEF"

def decimal_to_hex(n):
    if n == 0:
        return "0"
    digits = []
    while n > 0:
        digits.append(HEX_DIGITS[n % 16])   # remainder, mapped to a hex character
        n //= 16
    return "".join(reversed(digits))

print(decimal_to_hex(255))    # 'FF'
print(decimal_to_hex(4096))   # '1000'

Hex → Decimal: multiply each digit by its place value, then add

Same idea as binary → decimal, but each position is a power of 16 instead of 2, and letter digits convert back to their 10–15 value first.

Converting FF to decimal:
  position:      1    0
  digit:         F    F   ->  15   15
  value:   15×16¹ + 15×16⁰
         =    240 +    15
         =    255  ✓
def hex_to_decimal(hex_str):
    total = 0
    for position, char in enumerate(reversed(hex_str.upper())):
        total += HEX_DIGITS.index(char) * (16 ** position)
    return total

print(hex_to_decimal("FF"))     # 255
print(hex_to_decimal("1000"))   # 4096

Binary ↔ Hex: group into nibbles (no arithmetic needed at all)

Why hex maps to binary so cleanly: 16 = 2⁴, so every hex digit is exactly 4 bits (a "nibble"), and every possible 4-bit pattern (0000 through 1111) has exactly one hex digit (0 through F). That means converting between binary and hex is pure regrouping — no division, no multiplication, just splitting into chunks of 4 and looking each chunk up:

Binary -> Hex: split into groups of 4 bits, starting from the right,
                then convert each group independently.

  11111111  ->  1111 1111  ->  F    F   ->  FF

Hex -> Binary: expand each hex digit into its 4-bit pattern, and
                concatenate.

  FF        ->  F     F    ->  1111 1111  ->  11111111
nibble lookup table (memorize this once, use it forever):
0000=0   0100=4   1000=8   1100=C
0001=1   0101=5   1001=9   1101=D
0010=2   0110=6   1010=A   1110=E
0011=3   0111=7   1011=B   1111=F
def binary_to_hex_by_grouping(bits):
    bits = bits.zfill((len(bits) + 3) // 4 * 4)   # pad to a multiple of 4 on the left
    nibbles = [bits[i:i+4] for i in range(0, len(bits), 4)]
    return "".join(HEX_DIGITS[int(nibble, 2)] for nibble in nibbles)

print(binary_to_hex_by_grouping("11111111"))   # 'FF'

This is why MAC addresses (00:1A:2B:3C:4D:5E), IPv6 addresses, and memory dumps are always shown in hex — it's binary in disguise, without 32 characters of 1s and 0s to read, and you can convert either direction in your head by regrouping 4 bits at a time instead of doing any real math.

Two's complement (how negative numbers actually work)

An 8-bit signed integer doesn't store a "minus sign" — it flips all bits of the positive value and adds 1.

 5  = 00000101
-5  = 11111011   (flip bits: 11111010, add 1: 11111011)

Why it matters: signed integer overflow. An 8-bit signed int ranges from -128 to 127. 127 + 1 wraps to -128. This is a real, recurring bug class:

  • The 1996 Ariane 5 rocket exploded 37 seconds after launch because a 64-bit float was cast into a 16-bit signed integer and overflowed.
  • Classic game bugs ("civilization gandhi nuke" isn't real, but integer wraparound score/health bugs are extremely common).
  • Security angle: integer overflow is a documented vulnerability class (CWE-190). An attacker who can push a size/length calculation past INT_MAX can cause a buffer to be allocated smaller than expected, leading to a buffer overflow.
# Simulate 8-bit signed overflow
def to_int8(n):
    n = n & 0xFF                 # keep lowest 8 bits
    return n - 256 if n >= 128 else n

print(to_int8(127 + 1))   # -128  <- wraps around

IEEE-754 floating point (why 0.1 + 0.2 != 0.3)

Floats store numbers as sign × mantissa × 2^exponent. Just like 1/3 cannot be written exactly in decimal, 0.1 cannot be written exactly in binary — it's a repeating fraction. The stored value is the closest representable approximation.

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

Real-life engineering ties:

  • SWE: never store money as float/double. Use integer cents, or a Decimal type (from decimal import Decimal). Comparing floats for equality is a classic bug — use abs(a - b) < epsilon instead.
  • AI: float16/bfloat16 training (mixed precision) trades mantissa precision for speed/memory — understanding why explains why some models see NaN losses (exponent overflow) at low precision.
  • Network: checksums and packet length fields are fixed-width integers; overflow/wraparound is a real parsing bug source (e.g., TCP sequence number wraparound at 2³², handled by "serial number arithmetic," RFC 1982).
  • Security: integer overflow (CWE-190) and floating-point rounding errors in financial systems are exploitable classes — "salami slicing" attacks historically exploited rounding remainders.

More real-life examples:

  • The Boeing 787 Dreamliner's generator control units had a documented 32-bit counter overflow (FAA Airworthiness Directive 2015-19-07) that forced a shutdown after 248 days of continuous power — the same overflow class as the toy 8-bit example above, at safety-critical scale.
  • The "Year 2038 problem": Unix time stored as a signed 32-bit integer overflows on January 19, 2038 — every system still storing timestamps this way inherits this chapter's wraparound bug on a fixed calendar date.
  • In 2010, a Bitcoin integer-overflow bug let someone mint 184.4 billion BTC in a single transaction before the network was patched — a direct real-money consequence of fixed-width arithmetic.
  • Payment APIs (e.g., Stripe) represent amounts as integer minor units (cents), not decimals, specifically to avoid the floating-point rounding drift shown in the 0.1 + 0.2 example above.

Common mistake

Assuming int in your language is unbounded. Python's int is arbitrary precision (auto-grows), but C, Java, Go's fixed-width ints are not — this difference alone explains many "works in Python, crashes in C" ports.


Generated from real-life-maths-guide.md · 8 Oct 2026 16:36