AI EngineeringZero to ProductionHome·About·Contact
Developer Foundations · Chapter DF6

Number systems & representation

How every value a computer holds is really a pattern of bits — bases and conversions, two's complement, IEEE-754 floats (and why 0.1 + 0.2 != 0.3), text encodings, and the bitwise tricks interviewers love.

⏱️ ~3 hours🧪 8 runnable labs🎯 Beginner→Industry

Learning objectives

  • Convert fluently between binary, octal, decimal and hex — by hand and in code.
  • Explain unsigned vs signed integers and why two's complement is the universal choice.
  • Predict integer overflow and wrap-around from the bit width.
  • Decode a real IEEE-754 float bit-by-bit and explain why 0.1 + 0.2 != 0.3.
  • Move between bytes and text through ASCII, Unicode code points and UTF-8.
  • Use bitwise operators and classic bit tricks (masks, shifts, XOR swap, popcount).
🌱 Start here — from zeroEverything a computer stores is a number, and every number is a pattern of bits. This lesson is the bedrock under integers, floats, text and bit-flags. We convert between bases, build two's complement from scratch, decode a float with the standard library, and end with the bit tricks interviewers love. Every runnable block uses only the Python standard library and executes with zero errors — the outputs shown are the real ones.
▶ Runnable companionSnippets are plain Python 3. Paste any block into a file or the REPL — bin(), hex(), struct, and int.from_bytes are all stdlib.

1 · Positional number systems & conversions essential

A number system has a base (radix): the number of distinct digits. Each position is a power of the base. Decimal uses 10 digits (0–9); binary uses 2 (0–1); octal 8; hexadecimal 16 (0–9 then a–f). Hardware is binary because a wire is either high or low — one bit. We group bits into hex because each hex digit is exactly 4 bits (a nibble), so hex is a compact human-readable view of raw bits.

BaseNameDigitsPrefix in Python1 digit = ? bits
2binary0 10b1 bit
8octal0–70o3 bits
10decimal0–9(none)~3.32 bits
16hex0–9 a–f0x4 bits

Worked conversion — 156 to binary (repeated division by 2, read remainders bottom-up):

DivideQuotientRemainder
156 / 2780
78 / 2390
39 / 2191
19 / 291
9 / 241
4 / 220
2 / 210
1 / 201

Reading remainders from bottom to top gives 10011100. Group into nibbles: 1001 1100 = 0x9C. And 9*16 + 12 = 156. Everything checks out.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · convert between bases (runs)
bases.pyn = 156
print("bin:", bin(n))          # 0b prefix
print("oct:", oct(n))
print("hex:", hex(n))
# parse a string in any base with int(str, base)
print("0x9C ->", int("9C", 16))
print("0b10011100 ->", int("10011100", 2))
# format without prefix, zero-padded to 8 bits
print("padded:", format(n, "08b"), format(n, "02x"))
bin: 0b10011100
oct: 0o234
hex: 0x9c
0x9C -> 156
0b10011100 -> 156
padded: 10011100 9c
Why hex, not octalOne hex digit maps to exactly 4 bits, so a byte is always 2 hex digits (00ff). Octal packs 3 bits per digit, which doesn't align to a byte — that's why memory dumps, colours (#ff8800) and addresses are hex.

2 · Signed vs unsigned & two's complement essential

A fixed-width integer is just N bits. Unsigned reads all N bits as a magnitude: range 0 … 2^N − 1. Signed must also encode negatives. The winning scheme is two's complement: the top bit has weight −2^(N−1) instead of +2^(N−1). For 8 bits the range becomes −128 … +127.

Why two's complement and not a simple sign bit? Because addition and subtraction use the same adder circuit for signed and unsigned — there is exactly one representation of zero, and a − b is just a + (two's-complement of b). To negate: invert all bits, then add 1.

value 5 decimal bits 00000101 8-bit invert → 11111010 one's comp +1 → 11111011 add one = −5 reads as −5
Python · two's complement by hand, then check (runs)
twos.pyBITS = 8
MOD = 1 << BITS            # 256

def to_twos(x):
    "encode a signed int into its 8-bit two's-complement pattern"
    return x % MOD          # Python's % handles negatives for us

def from_twos(bits):
    "decode an 8-bit pattern back to a signed int"
    return bits - MOD if bits >= (1 << (BITS - 1)) else bits

for v in (5, -5, -1, -128, 127):
    b = to_twos(v)
    print(f"{v:>5} -> {b:08b} -> back {from_twos(b)}")
    5 -> 00000101 -> back 5
   -5 -> 11111011 -> back -5
   -1 -> 11111111 -> back -1
 -128 -> 10000000 -> back -128
  127 -> 01111111 -> back 127
The mental shortcut−1 is all ones in every width (11111111). −128 is the one lopsided value: it has no positive twin because the range is asymmetric (−128…127).

3 · Fixed width & integer overflow intermediate

Real hardware integers are a fixed width (8/16/32/64 bits). Add past the top and the value wraps around modulo 2^N — this is the source of countless bugs (the Gangnam-style view counter, the Ariane 5 crash lineage, timestamp rollovers). Python's own int is arbitrary precision so it never overflows — which is exactly why we must simulate a fixed width to see the effect.

Python · simulate 8-bit wrap-around (runs)
overflow.pyMASK = 0xFF                 # keep only the low 8 bits

def add8_unsigned(a, b):
    return (a + b) & MASK

def add8_signed(a, b):
    r = (a + b) & MASK
    return r - 256 if r >= 128 else r      # reinterpret as two's complement

print("255 + 1 unsigned ->", add8_unsigned(255, 1))   # wraps to 0
print("200 + 100 unsigned ->", add8_unsigned(200, 100))
print("127 + 1 signed   ->", add8_signed(127, 1))     # overflow to -128
print("Python has no limit:", 2 ** 100)
255 + 1 unsigned -> 0
200 + 100 unsigned -> 44
127 + 1 signed   -> -128
Python has no limit: 1267650600228229401496703205376
Overflow is silent in C/JavaIn C or Java that 127 + 1 quietly becomes −128 with no exception. Signed overflow is even undefined behaviour in C. Know your width, or mask deliberately.

4 · Fixed-point, floating-point & IEEE-754 advanced

Integers can't hold fractions. Fixed-point puts an imaginary binary point at a set position (money as integer cents). Floating-point stores a number like scientific notation: sign × 1.mantissa × 2^exponent, so the point 'floats'. The universal format is IEEE-754. A 64-bit double is 1 sign bit + 11 exponent bits + 52 fraction bits (exponent bias 1023).

TypeTotal bitsSignExponentFractionBias
float32321823127
float64 (double)64111521023

Let's decode a real double with struct — pack the float to 8 bytes, read the raw integer, and pull the fields apart:

Python · decode a float64 bit-by-bit (runs)
float_decode.pyimport struct

x = 0.15625              # a value that is exact in binary
bits = struct.unpack(">Q", struct.pack(">d", x))[0]   # 64-bit pattern
print("raw bits:", format(bits, "064b"))

sign =  bits >> 63
exp  = (bits >> 52) & 0x7FF          # 11 exponent bits
frac =  bits & ((1 << 52) - 1)       # 52 fraction bits
print("sign =", sign, " exp =", exp, " (unbiased", exp - 1023, ")")
value = (-1) ** sign * (1 + frac / 2 ** 52) * 2 ** (exp - 1023)
print("reconstructed:", value)
raw bits: 0011111111000100000000000000000000000000000000000000000000000000
sign = 0  exp = 1020  (unbiased -3 )
reconstructed: 0.15625

0.15625 = 1.25 × 2^−3, so the unbiased exponent is −3 and the mantissa is 1.25 — exactly reconstructed. But most decimals are not exact in base 2, which is the famous gotcha:

Python · why 0.1 + 0.2 != 0.3 (runs)
point_one.pya = 0.1 + 0.2
print("0.1 + 0.2 =", a)
print("== 0.3 ?  ", a == 0.3)
print("full digits:", format(a, ".17f"))
from decimal import Decimal
print("what 0.1 really is:", Decimal(0.1))
0.1 + 0.2 = 0.30000000000000004
== 0.3 ?   False
full digits: 0.30000000000000004
what 0.1 really is: 0.1000000000000000055511151231257827021181583404541015625
Never test floats with ==0.1 in binary is a repeating fraction (like 1/3 in decimal), so it's rounded to the nearest 52-bit value — a hair above 0.1. The tiny errors accumulate. Compare with a tolerance: math.isclose(a, 0.3), or use decimal.Decimal / integer cents for money.

5 · Encodings: ASCII, Unicode & UTF-8 intermediate

Text is numbers too. ASCII maps 128 characters to 7-bit codes (A=65). Unicode assigns every character a code point (e.g. U+00E9 for é, U+1F600 for 😀) — that's an abstract number, not bytes. UTF-8 is the dominant encoding: it stores a code point in 1–4 bytes, is backwards-compatible with ASCII, and is what the web runs on.

'é' character abstract code point U+00E9 233 decimal UTF-8 encode rule bytes C3 A9 2 bytes
Python · text ↔ code points ↔ bytes (runs)
encodings.pys = "café 😀"
print("chars:", list(s))
print("code points:", [hex(ord(c)) for c in s])
raw = s.encode("utf-8")
print("utf-8 bytes:", raw, "len", len(raw))
# decode raw bytes straight from an integer list
print("back:", raw.decode("utf-8"))
# note: 6 characters but more bytes (é=2, 😀=4)
chars: ['c', 'a', 'f', 'é', ' ', '😀']
code points: ['0x63', '0x61', '0x66', '0xe9', '0x20', '0x1f600']
utf-8 bytes: b'caf\xc3\xa9 \xf0\x9f\x98\x80' len 10
back: café 😀
Bytes vs intint.from_bytes(b'\x01\x00', 'big') is 256; 'little' gives 1. Endianness is just which byte comes first — the same concept behind network byte order and file formats.

6 · Bitwise operators & bit tricks advanced

OperatorNameExample (4-bit)Use
&AND1100 & 1010 = 1000mask / test a bit
|OR1100 | 1010 = 1110set a bit
^XOR1100 ^ 1010 = 0110toggle / find difference
~NOT~1100 = …0011invert all bits
<<left shift0011 << 1 = 0110×2 per shift
>>right shift0110 >> 1 = 0011÷2 per shift
Python · masks, shifts, XOR swap, popcount (runs)
bittricks.py# 1. test / set / clear / toggle bit i with a mask
flags = 0b0000
flags |= (1 << 2)                 # set bit 2
print("after set bit2:", format(flags, "04b"))
print("bit2 set?", bool(flags & (1 << 2)))
flags &= ~(1 << 2)                # clear bit 2
print("after clear:", format(flags, "04b"))

# 2. XOR swap two ints with no temp variable
a, b = 12, 25
a ^= b; b ^= a; a ^= b
print("swapped:", a, b)

# 3. popcount: how many 1-bits (Python 3.10+: int.bit_count)
n = 0b10110101
print("popcount:", bin(n).count("1"))

# 4. classic: is n a power of two?  (only one bit set)
def is_pow2(n): return n > 0 and (n & (n - 1)) == 0
print("64 pow2?", is_pow2(64), " 65 pow2?", is_pow2(65))
after set bit2: 0100
bit2 set? True
after clear: 0000
swapped: 25 12
popcount: 5
64 pow2? True  65 pow2? False
Why n & (n-1) worksSubtracting 1 flips the lowest set bit and everything below it. ANDing clears that lowest set bit. A power of two has exactly one set bit, so the result is 0. Repeatedly applying n &= n-1 also counts bits in O(number-of-set-bits).

✓ Checkpoint — you can move on when you can…

  • Convert 156 between decimal, binary, octal and hex without a calculator.
  • Negate a number in two's complement (invert + 1) and explain why −1 is all ones.
  • Predict the result of 8-bit unsigned and signed overflow.
  • Explain, from the bit layout, why 0.1 + 0.2 != 0.3 and what to do instead.
  • Encode/decode text through UTF-8 and read a byte string's code points.
  • Use masks/shifts/XOR and know the n & (n-1) power-of-two trick.

Knowledge check check yourself

✓ Knowledge check

In 8-bit two's complement, what is the bit pattern for −1, and why is the range −128…127 rather than −127…128?

Show answer
−1 is 11111111 (all ones). The range is asymmetric because one bit pattern (10000000) is spent on −128 and there is a single zero (00000000); the positive side tops out at 127. So you get 128 negatives, one zero, 127 positives = 256 values.
✓ Knowledge check

Explain in one sentence why 0.1 + 0.2 == 0.3 is False in virtually every language.

Show answer
0.1, 0.2 and 0.3 cannot be represented exactly in binary floating-point (they're repeating fractions in base 2), so each is stored rounded to the nearest 52-bit value; the rounding errors of 0.1 and 0.2 add up to a value a hair above the rounded 0.3, so the equality fails.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Base conversions three waysBeginner

Context: Every debugging session eventually shows you a value in hex or binary; converting in your head and confirming in code is table stakes.

Your task: Convert the decimal number 205 to binary, octal and hex, then verify each with Python and parse them back to decimal.

Requirements:

  • Show 205 in all three bases using bin/oct/hex
  • Parse each string form back with int(s, base) and confirm you get 205
  • Print the binary zero-padded to 8 bits with format(205, '08b')

💡 Hint: 205 = 128 + 64 + 8 + 4 + 1. Group the 8 bits into two nibbles to read the hex directly.

Show solution
n = 205
print(bin(n), oct(n), hex(n))        # 0b11001101 0o315 0xcd
print(format(n, '08b'))              # 11001101
assert int('11001101', 2) == 205
assert int('315', 8) == 205
assert int('cd', 16) == 205
print('all round-trip to', 205)

Nibbles 1100 1101 = C D = 0xCD, and 12*16 + 13 = 205. Every representation is the same number in a different base.

Exercise 2 · Encode/decode a signed byteIntermediate

Context: Protocols and binary file formats store signed values in a fixed number of bits; you must be able to encode and decode two's complement yourself.

Your task: Write to_i8(x) and from_i8(bits) for 8-bit two's complement and verify the full boundary set.

Requirements:

  • to_i8 maps a signed int in −128…127 to its 8-bit pattern
  • from_i8 maps an 8-bit pattern back to the signed int
  • Round-trip −128, −1, 0, 1 and 127 and assert equality
  • Reject (or document) inputs outside −128…127

💡 Hint: Python's modulo x % 256 already yields the correct unsigned pattern for negatives.

Show solution
def to_i8(x):
    if not -128 <= x <= 127:
        raise ValueError('out of range for int8')
    return x % 256

def from_i8(bits):
    return bits - 256 if bits & 0x80 else bits

for v in (-128, -1, 0, 1, 127):
    assert from_i8(to_i8(v)) == v, v
print('round-trip OK; -1 ->', format(to_i8(-1), '08b'))
# round-trip OK; -1 -> 11111111

The & 0x80 tests the sign bit; if set, subtract the modulus to recover the negative value.

Exercise 3 · Prove a float is inexactAdvanced

Context: Money and measurement bugs almost always trace back to float rounding; you need to demonstrate the error and pick the right fix.

Your task: Show that 0.1 + 0.2 != 0.3, print the 17-significant-digit value, and demonstrate two correct comparison strategies.

Requirements:

  • Print format(0.1 + 0.2, '.17f') to expose the error
  • Compare correctly with math.isclose
  • Show an exact result using decimal.Decimal('0.1') + Decimal('0.2')
  • Explain in a comment why integers-as-cents is the standard money fix

💡 Hint: Decimal constructed from strings is exact; constructed from a float it inherits the float's error.

Show solution
import math
from decimal import Decimal

s = 0.1 + 0.2
print(format(s, '.17f'))                 # 0.30000000000000004
print(s == 0.3)                          # False
print(math.isclose(s, 0.3))              # True  (tolerant compare)
print(Decimal('0.1') + Decimal('0.2'))   # 0.3   (exact, from strings)
# money: store cents as int -> 10 + 20 == 30, no float at all

isclose uses a relative+absolute tolerance; Decimal from strings avoids binary rounding entirely; integer cents sidesteps floats altogether.

Exercise 4 · Decode any float64 fieldExpert

Context: Reading a raw sensor stream or a binary format means pulling a double out of 8 bytes and interpreting it — the same thing a debugger does.

Your task: Write a function that takes 8 big-endian bytes of an IEEE-754 double and returns (sign, unbiased_exponent, mantissa_fraction, value).

Requirements:

  • Use struct.unpack('>Q', ...) to get the 64-bit integer
  • Slice out sign (bit 63), exponent (bits 62–52), fraction (bits 51–0)
  • Return the unbiased exponent (subtract 1023) and reconstruct the value
  • Verify on the bytes of 1.0 and -0.15625

💡 Hint: 1.0 has exponent field 1023 (unbiased 0) and a zero fraction; that's your sanity check.

Show solution
import struct

def decode_f64(b8):
    bits = struct.unpack('>Q', b8)[0]
    sign = bits >> 63
    exp  = (bits >> 52) & 0x7FF
    frac = bits & ((1 << 52) - 1)
    value = (-1)**sign * (1 + frac/2**52) * 2**(exp - 1023)
    return sign, exp - 1023, frac, value

print(decode_f64(struct.pack('>d', 1.0)))
print(decode_f64(struct.pack('>d', -0.15625)))
# (0, 0, 0, 1.0)
# (1, -3, 1125899906842624, -0.15625)

For 1.0 the fraction is 0 and the unbiased exponent is 0, so the value is 1 × 2^0 = 1.0. The reconstruction formula is the IEEE-754 definition itself.

Exercise 5 · Pack boolean flags into one intProfessional

Context: Feature flags, permission bits and hardware registers are stored as packed bitfields; a clean helper avoids error-prone hand-masking scattered through the code.

Your task: Build a tiny BitFlags helper with set, clear, toggle and is_set over named bit positions.

Requirements:

  • Store state as a single integer
  • Support named flags mapping to bit positions
  • Implement set/clear/toggle/is_set using masks and shifts
  • Provide a readable binary dump for debugging
  • Demonstrate setting two flags and clearing one

💡 Hint: Set = state |= 1<<i, clear = state &= ~(1<<i), toggle = state ^= 1<<i.

Show solution
class BitFlags:
    def __init__(self, names):
        self.pos = {n: i for i, n in enumerate(names)}
        self.state = 0
    def _m(self, name): return 1 << self.pos[name]
    def set(self, n):    self.state |=  self._m(n); return self
    def clear(self, n):  self.state &= ~self._m(n); return self
    def toggle(self, n): self.state ^=  self._m(n); return self
    def is_set(self, n): return bool(self.state & self._m(n))
    def dump(self):      return format(self.state, f'0{len(self.pos)}b')

f = BitFlags(['read', 'write', 'exec'])
f.set('read').set('exec')
print(f.dump(), f.is_set('read'), f.is_set('write'))  # 101 True False
f.clear('read')
print(f.dump())                                       # 100

All state lives in one integer; each operation is a single mask. This is exactly how OS permission bits and hardware control registers are manipulated.

Exercise 6 · A checksum from first principlesIndustry scenario

Context: Every storage and network layer detects corruption with a checksum; understanding one at the bit level demystifies CRCs, hashes and error detection.

Your task: Implement the standard Internet checksum (RFC 1071 one's-complement 16-bit sum) over a byte buffer, and show it detects a single-bit flip.

Requirements:

  • Sum the data as big-endian 16-bit words
  • Fold the carry bits back in (end-around carry) until it fits in 16 bits
  • Take the one's complement as the final checksum
  • Verify that recomputing over data+checksum yields zero
  • Flip one bit and show the verification now fails

💡 Hint: End-around carry: while s >> 16: s = (s & 0xFFFF) + (s >> 16). Padding: if the buffer has an odd length, treat the last byte as the high half of a word.

Show solution
def inet_checksum(data: bytes) -> int:
    if len(data) % 2:
        data += b'\x00'
    s = 0
    for i in range(0, len(data), 2):
        s += (data[i] << 8) | data[i+1]        # big-endian 16-bit word
    while s >> 16:
        s = (s & 0xFFFF) + (s >> 16)           # fold carries
    return (~s) & 0xFFFF                        # one's complement

msg = b'HELLO-WORLD!'
cksum = inet_checksum(msg)
packet = msg + bytes([cksum >> 8, cksum & 0xFF])
print('checksum:', hex(cksum))
print('verify good packet ->', inet_checksum(packet))     # 0 == valid

corrupt = bytearray(packet); corrupt[0] ^= 0x01           # flip one bit
print('verify corrupt   ->', inet_checksum(bytes(corrupt)))  # non-zero

Recomputing the checksum over data-plus-checksum yields 0 for an intact packet because the sum plus its own complement is all-ones, which one's-complement folds to zero. A single flipped bit changes the sum, so verification returns non-zero — corruption detected. This is the exact algorithm in IPv4, ICMP, TCP and UDP headers.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in