Python Binary, Bits, and Bytes Practice Problems & Exercises
Practice: Binary, Bits, and Bytes
← Back to lessonEasy
Write a function decimal_to_binary(n) that converts a non-negative integer to its binary string representation without using bin(), format(), or f-string formatting. Implement the repeated-division algorithm from scratch.
print(f"0 -> {decimal_to_binary(0)}")
print(f"1 -> {decimal_to_binary(1)}")
print(f"10 -> {decimal_to_binary(10)}")
print(f"42 -> {decimal_to_binary(42)}")
print(f"255 -> {decimal_to_binary(255)}")
Solution
def decimal_to_binary(n):
if n == 0:
return "0"
bits = []
while n > 0:
bits.append(str(n % 2))
n //= 2
return "".join(reversed(bits))
How it works: The repeated-division algorithm extracts one bit at a time from the least significant position. n % 2 gives the current rightmost bit (0 or 1), and n //= 2 shifts the number right by one position. The remainders come out in reverse order (LSB first), so we reverse them at the end.
Verification: 42 // 2 = 21 r0, 21 // 2 = 10 r1, 10 // 2 = 5 r0, 5 // 2 = 2 r1, 2 // 2 = 1 r0, 1 // 2 = 0 r1 → reversed: 101010.
def decimal_to_binary(n):
"""Convert a non-negative integer to its binary string representation.
Do NOT use bin(), format(), or f-strings. Implement the algorithm manually.
Return '0' for input 0.
"""
passExpected Output
0 -> 0\n1 -> 1\n10 -> 1010\n42 -> 101010\n255 -> 11111111Hints
Hint 1: Repeatedly divide by 2 and collect the remainders. The binary representation is the remainders read in reverse order.
Hint 2: Use a while loop: remainder = n % 2, n = n // 2. Prepend each remainder to your result string, or collect them in a list and reverse at the end.
Write a function count_set_bits(n) that returns the number of 1 bits (also called "popcount" or "Hamming weight") in the binary representation of a non-negative integer. Do not use bin(n).count('1').
for val in [0, 1, 7, 42, 255, 1023]:
print(f"{val} -> {count_set_bits(val)}")
Solution
def count_set_bits(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
Alternative — Brian Kernighan's algorithm (faster):
def count_set_bits(n):
count = 0
while n:
n &= n - 1 # Clears the lowest set bit
count += 1
return count
Why Kernighan's is faster: The basic approach loops once per bit (up to 32 or 64 times). Kernighan's only loops once per set bit. For n = 1024 (binary 10000000000), the basic approach loops 11 times, but Kernighan's loops just once.
How n & (n-1) works: Subtracting 1 flips the lowest set bit and all bits below it. ANDing with the original clears exactly that lowest set bit. Example: 1010 & 1001 = 1000.
def count_set_bits(n):
"""Return the number of 1-bits in the binary representation of n.
Do NOT use bin().count('1'). Implement it using bitwise operations.
"""
passExpected Output
0 -> 0\n1 -> 1\n7 -> 3\n42 -> 3\n255 -> 8\n1023 -> 10Hints
Hint 1: Check the last bit with `n & 1` — it is 1 if the bit is set, 0 otherwise. Then right-shift with `n >>= 1`.
Hint 2: Brian Kernighan trick: `n & (n - 1)` clears the lowest set bit. Count how many times you can do this before n becomes 0.
Predict the output of each bitwise operation before running. Write out the binary to verify.
a = 0b1010 # 10
b = 0b1111 # 15
print(f"AND: {a & b}")
print(f"OR: {a | b}")
print(f"XOR: {a ^ b}")
print(f"NOT: {~a}")
print(f"Left shift: {a << 1}")
print(f"Right shift: {a >> 1}")Solution
AND: 8
OR: 15
XOR: 7
NOT: -11
Left shift: 20
Right shift: 5
Bit-by-bit breakdown:
a = 1010
b = 1111
a & b = 1010 (both bits must be 1) = 8
a | b = 1111 (either bit can be 1) = 15
a ^ b = 0111 (bits must differ) = 7
~a = ...10101 → -(10 + 1) = -11 (two's complement)
a << 1 = 10100 (shift left = multiply by 2) = 20
a >> 1 = 0101 (shift right = floor divide by 2) = 5
Key insight: ~n = -(n+1) in Python because Python integers have unlimited precision, so the bitwise NOT flips infinitely many leading zeros into ones, giving a negative two's complement result. This catches many beginners off guard.
Expected Output
AND: 8\nOR: 15\nXOR: 7\nNOT: -11\nLeft shift: 20\nRight shift: 5Hints
Hint 1: Write out the binary representations: 10 = 1010, 15 = 1111, 8 = 1000. Apply each operation bit by bit.
Hint 2: `~n` in Python gives `-(n+1)` because Python uses two's complement with unlimited precision. `~10` = `-11`.
Run the utf8_byte_breakdown function on several strings and predict the total byte count before running.
def utf8_byte_breakdown(text):
for char in text:
byte_count = len(char.encode('utf-8'))
code_point = ord(char)
print(f"'{char}' U+{code_point:04X} -> {byte_count} byte(s)")
total = len(text.encode('utf-8'))
print(f"Total: {len(text)} characters, {total} bytes")
# Test 1: Pure ASCII
utf8_byte_breakdown("Hi!")
print()
# Test 2: Try these and predict before running
# utf8_byte_breakdown("café")
# utf8_byte_breakdown("hello 🌍")
# utf8_byte_breakdown("漢字")Challenge: Without running the code, predict the byte counts for UTF-8 byte ranges: Key insight: "café", "hello 🌍", and "漢字".Solution
Unicode range UTF-8 bytes Examples U+0000 - U+007F 1 byte ASCII: A, z, 0, ! U+0080 - U+07FF 2 bytes e with accent, Greek, Cyrillic U+0800 - U+FFFF 3 bytes CJK, Japanese, Korean, most symbols U+10000 - U+10FFFF 4 bytes Emoji, rare scripts, math symbols len("café") returns 4 (characters), but len("café".encode("utf-8")) returns 5 (bytes). In networking, file I/O, and databases, it is the byte count that matters for storage and transmission.
def utf8_byte_breakdown(text):
"""Print each character, its Unicode code point, and its UTF-8 byte count."""
for char in text:
byte_count = len(char.encode('utf-8'))
code_point = ord(char)
print(f"'{char}' U+{code_point:04X} -> {byte_count} byte(s)")
total = len(text.encode('utf-8'))
print(f"Total: {len(text)} characters, {total} bytes")Expected Output
'H' U+0048 -> 1 byte(s)\n'i' U+0069 -> 1 byte(s)\n'!' U+0021 -> 1 byte(s)\nTotal: 3 characters, 3 bytesHints
Hint 1: ASCII characters (U+0000 to U+007F) take 1 byte in UTF-8. Many accented characters take 2 bytes. CJK characters and emoji typically take 3-4 bytes.
Hint 2: Use `len(char.encode("utf-8"))` to get the byte count for a single character, or `ord(char)` to get its Unicode code point.
Medium
Write a function binary_to_decimal(binary_str) that converts a binary string to its decimal integer value without using int(binary_str, 2). Implement the positional notation algorithm from scratch.
for b in ["0", "1", "1010", "101010", "11111111", "10000000000"]:
print(f"{b} -> {binary_to_decimal(b)}")
Solution
Approach 1 — Positional notation (sum of powers):
def binary_to_decimal(binary_str):
result = 0
for i, bit in enumerate(reversed(binary_str)):
if bit == '1':
result += 2 ** i
return result
Approach 2 — Horner's method (more efficient, no exponentiation):
def binary_to_decimal(binary_str):
result = 0
for bit in binary_str:
result = result * 2 + int(bit)
return result
Why Horner's method is better: It processes bits left-to-right in a single pass with no exponentiation. For 101010: 0*2+1=1, 1*2+0=2, 2*2+1=5, 5*2+0=10, 10*2+1=21, 21*2+0=42. Each step doubles the accumulated value (shifting it left by one position) and adds the new bit.
Approach 3 — Using bit shifting (most "binary-native"):
def binary_to_decimal(binary_str):
result = 0
for bit in binary_str:
result = (result << 1) | int(bit)
return result
This is identical to Horner's method but uses << 1 (left shift = multiply by 2) and | int(bit) (OR to set the lowest bit).
def binary_to_decimal(binary_str):
"""Convert a binary string (e.g., '101010') to its decimal integer value.
Do NOT use int(binary_str, 2). Implement the positional notation algorithm.
"""
passExpected Output
0 -> 0\n1 -> 1\n1010 -> 10\n101010 -> 42\n11111111 -> 255\n10000000000 -> 1024Hints
Hint 1: Each position i (from right, starting at 0) contributes bit_value * 2^i to the total. Sum them all up.
Hint 2: Alternative approach: Horner's method — start from the left, and for each bit do `result = result * 2 + bit`. This avoids computing powers of 2.
Write a function hex_to_rgb(hex_color) that extracts the red, green, and blue components from a 24-bit hex color integer using bitwise operations only (no string conversion).
colors = [0xFF8800, 0x00FF00, 0x336699, 0x000000, 0xFFFFFF]
for c in colors:
print(f"0x{c:06X} -> {hex_to_rgb(c)}")
Solution
def hex_to_rgb(hex_color):
r = (hex_color >> 16) & 0xFF
g = (hex_color >> 8) & 0xFF
b = hex_color & 0xFF
return (r, g, b)
How the bit layout works:
Hex color: 0xFF8800
Binary: 11111111 10001000 00000000
^^^^^^^^ ^^^^^^^^ ^^^^^^^^
Red=255 Green=136 Blue=0
Extraction:
Red: >> 16 moves RRRRRRRR to lowest 8 bits, & 0xFF keeps only those 8
Green: >> 8 moves GGGGGGGG to lowest 8 bits, & 0xFF keeps only those 8
Blue: no shift needed, & 0xFF keeps lowest 8 bits
Bonus — the reverse operation (packing RGB into a hex integer):
def rgb_to_hex(r, g, b):
return (r << 16) | (g << 8) | b
Real-world use: This exact technique is used in image processing, game engines, CSS color manipulation, and LED control. The mask 0xFF (255 in decimal, 11111111 in binary) is one of the most common constants in systems programming.
def hex_to_rgb(hex_color):
"""Extract R, G, B values from a hex color integer.
Example: 0xFF8800 -> (255, 136, 0)
Use bitwise operations, not string parsing.
"""
passExpected Output
0xFF8800 -> (255, 136, 0)\n0x00FF00 -> (0, 255, 0)\n0x336699 -> (51, 102, 153)\n0x000000 -> (0, 0, 0)\n0xFFFFFF -> (255, 255, 255)Hints
Hint 1: A 24-bit color is laid out as RRRRRRRRGGGGGGGGBBBBBBBB. Right-shift to move the component you want into the lowest 8 bits, then mask with 0xFF.
Hint 2: Red = (color >> 16) & 0xFF, Green = (color >> 8) & 0xFF, Blue = color & 0xFF. The mask 0xFF = 11111111 in binary isolates the bottom 8 bits.
Write a function is_power_of_two(n) that returns True if n is a power of 2. Use bitwise operations only — no loops, no math.log, no string conversion.
for val in [0, 1, 2, 3, 4, 16, 17, 1024, -8]:
print(f"{val} -> {is_power_of_two(val)}")
Solution
def is_power_of_two(n):
return n > 0 and (n & (n - 1)) == 0
Why it works:
Powers of 2 have exactly one set bit:
1 = 00000001
2 = 00000010
4 = 00000100
8 = 00001000
16 = 00010000
n - 1 flips the set bit and sets all lower bits:
16 = 00010000
15 = 00001111
n & (n-1) = 00000000 → zero! Only possible for powers of 2.
Non-power: 6 = 00000110, 5 = 00000101, 6 & 5 = 00000100 ≠ 0
Edge cases:
n = 0: Not a power of 2 (0 has no set bits). Then > 0check handles this.n < 0: Negative numbers are never powers of 2. Then > 0check handles this.n = 1: Is 2^0, which is a valid power of 2.1 & 0 = 0, so it correctly returnsTrue.
Performance: This is O(1) — a single comparison and a single bitwise operation. This is the standard trick used in operating systems (checking memory alignment), hash table implementations (checking capacity), and graphics engines.
def is_power_of_two(n):
"""Return True if n is a power of 2 (1, 2, 4, 8, 16, ...).
Use a single bitwise operation — no loops, no log.
"""
passExpected Output
0 -> False\n1 -> True\n2 -> True\n3 -> False\n4 -> True\n16 -> True\n17 -> False\n1024 -> True\n-8 -> FalseHints
Hint 1: A power of 2 in binary has exactly one set bit: 1, 10, 100, 1000, etc. What happens when you subtract 1 from such a number?
Hint 2: If n is a power of 2, then n-1 has all lower bits set (e.g., 1000 - 1 = 0111). So `n & (n-1)` is 0. But you also need to handle n <= 0.
The expression 0.1 + 0.2 does not equal 0.3 in Python (or any IEEE 754 language). Demonstrate the problem and then fix it using the decimal module.
from decimal import Decimal
# The problem
naive = 0.1 + 0.2
print(f"Naive: {naive}")
print(f"Fixed: {0.3}")
print(f"Equal? {naive == 0.3}")
# The fix — fill in the correct Decimal usage
d1 = Decimal('0.1')
d2 = Decimal('0.2')
d3 = Decimal('0.3')
print(f"Decimal: {d1 + d2 == d3}")Solution
from decimal import Decimal
# The problem: IEEE 754 binary floating point
naive = 0.1 + 0.2
print(f"Naive: {naive}") # 0.30000000000000004
print(f"Fixed: {0.3}") # 0.3 (display rounds, but value is still imprecise)
print(f"Equal? {naive == 0.3}") # False
# The fix: decimal module uses base-10 arithmetic
d1 = Decimal('0.1') # MUST use strings, not Decimal(0.1)
d2 = Decimal('0.2')
d3 = Decimal('0.3')
print(f"Decimal: {d1 + d2 == d3}") # True
Why this happens: 0.1 in binary is 0.0001100110011... (repeating forever), just like 1/3 in decimal is 0.333.... IEEE 754 truncates this to 53 significant bits, introducing a tiny error of about 5.5 x 10^-17. When you add two such approximations, the error accumulates.
Critical mistake: Decimal(0.1) imports the float error into the Decimal. Always use Decimal('0.1') (string) to get exact base-10 representation.
Other approaches for comparing floats:
import math
math.isclose(0.1 + 0.2, 0.3) # True (default rel_tol=1e-9)
abs(0.1 + 0.2 - 0.3) < 1e-9 # True (manual epsilon)
round(0.1 + 0.2, 10) == round(0.3, 10) # True (round then compare)
When to use decimal: Financial calculations, currency, tax, anywhere exact base-10 arithmetic is required. For scientific computing, math.isclose() is usually sufficient.
Expected Output
Naive: 0.30000000000000004\nFixed: 0.3\nEqual? False\nDecimal: TrueHints
Hint 1: 0.1 and 0.2 cannot be represented exactly in binary (IEEE 754). They are stored as infinitely repeating fractions truncated to 53 bits of precision.
Hint 2: The `decimal` module uses base-10 arithmetic internally, so 0.1 + 0.2 = 0.3 exactly. Initialize Decimal from strings, not floats, to avoid importing the float error.
Implement xor_swap(a, b) that swaps two integers using only XOR operations — no temporary variable, no tuple unpacking, no arithmetic.
pairs = [(42, 17), (0, 255), (-5, 100)]
for a, b in pairs:
print(f"Before: a={a}, b={b}")
a, b = xor_swap(a, b)
print(f"After: a={a}, b={b}")
Solution
def xor_swap(a, b):
a = a ^ b # a now holds a^b
b = a ^ b # b = (a^b)^b = a (original a)
a = a ^ b # a = (a^b)^a = b (original b)
return a, b
Step-by-step trace with a=42, b=17:
Start: a = 42 (101010), b = 17 (010001)
Step 1: a = a ^ b = 101010 ^ 010001 = 111011 (59)
Step 2: b = a ^ b = 111011 ^ 010001 = 101010 (42) ← original a!
Step 3: a = a ^ b = 111011 ^ 101010 = 010001 (17) ← original b!
Why it works: XOR is self-inverse: x ^ x = 0 and x ^ 0 = x. After step 1, a encodes both values. Step 2 extracts the original a into b. Step 3 extracts the original b into a.
Practical note: In Python, just use a, b = b, a — it is clearer, faster (the compiler optimizes it), and works for any type. XOR swap is a classic interview question and useful in embedded systems where memory is extremely constrained, but it should never be used in production Python code.
def xor_swap(a, b):
"""Swap two integers using only XOR — no temp variable, no tuple unpacking.
Return the swapped values as a tuple.
"""
# Your code: three XOR operations
pass
return a, bExpected Output
Before: a=42, b=17\nAfter: a=17, b=42\nBefore: a=0, b=255\nAfter: a=255, b=0\nBefore: a=-5, b=100\nAfter: a=100, b=-5Hints
Hint 1: The three steps are: a = a ^ b, then b = a ^ b, then a = a ^ b. After step 1, `a` holds the combined XOR of both original values.
Hint 2: XOR is its own inverse: if you XOR something twice with the same value, you get the original back. (x ^ y) ^ y = x.
Hard
Implement twos_complement(value, num_bits) that returns the two's complement binary string representation of a signed integer in the given bit width.
The valid range for num_bits bits is -2^(num_bits-1) to 2^(num_bits-1) - 1. Raise ValueError if the value is out of range.
tests = [
(5, 8), (-5, 8), (0, 8), (-1, 8),
(127, 8), (-128, 8), (1, 4), (-8, 4),
]
for value, bits in tests:
print(f"{value:>4} in {bits} bits: {twos_complement(value, bits)}")
Solution
def twos_complement(value, num_bits):
min_val = -(1 << (num_bits - 1)) # -2^(n-1)
max_val = (1 << (num_bits - 1)) - 1 # 2^(n-1) - 1
if value < min_val or value > max_val:
raise ValueError(
f"{value} out of range for {num_bits}-bit two's complement "
f"[{min_val}, {max_val}]"
)
if value >= 0:
# Positive: just convert to binary and zero-pad
return format(value, f'0{num_bits}b')
else:
# Negative: compute 2^num_bits + value
unsigned = (1 << num_bits) + value
return format(unsigned, f'0{num_bits}b')
How two's complement works for -5 in 8 bits:
Method 1 — Invert and add 1:
+5 = 00000101
flip = 11111010
+1 = 11111011 ← this is -5
Method 2 — 2^n + value (used in our solution):
2^8 + (-5) = 256 - 5 = 251
251 = 11111011 ← same result
Why two's complement is universal:
- Addition and subtraction use the same hardware circuit for both signed and unsigned numbers.
- There is only one representation of zero (unlike sign-magnitude, which has +0 and -0).
- The MSB (most significant bit) naturally indicates the sign: 0 = positive, 1 = negative.
Range for N bits: The leading bit is the sign bit, leaving N-1 bits for magnitude. Positive max = 2^(N-1) - 1, negative min = -2^(N-1). For 8 bits: -128 to 127.
def twos_complement(value, num_bits):
"""Return the two's complement binary string for a signed integer.
Args:
value: signed integer (can be negative)
num_bits: bit width (e.g., 8 for a byte)
Returns:
Binary string of length num_bits
Raises:
ValueError if value is out of range for num_bits
"""
passExpected Output
5 in 8 bits: 00000101\n-5 in 8 bits: 11111011\n 0 in 8 bits: 00000000\n-1 in 8 bits: 11111111\n 127 in 8 bits: 01111111\n-128 in 8 bits: 10000000\n 1 in 4 bits: 0001\n-8 in 4 bits: 1000Hints
Hint 1: For positive numbers, just convert to binary and zero-pad. For negative numbers, the two's complement is: flip all bits of the absolute value, then add 1.
Hint 2: Alternative shortcut: for negative values, compute 2^num_bits + value (which gives the unsigned representation of the negative number), then convert that to binary.
Build a BitFlags class that packs multiple boolean flags into a single integer. This is how operating systems store file permissions, how network protocols pack header flags, and how game engines store entity states.
flags = BitFlags(['read', 'write', 'execute', 'admin'])
flags.set('read')
flags.set('execute')
print(f"After set read, execute: {flags.packed()} (0b{flags.packed():b})")
print(f"Active: {flags.active_flags()}")
print(f"read={flags.get('read')}, write={flags.get('write')}, "
f"execute={flags.get('execute')}, admin={flags.get('admin')}")
flags.toggle('execute')
print(f"After toggle execute: {flags.packed()} (0b{flags.packed():b})")
flags.clear('read')
print(f"After clear read: {flags.packed()} (0b{flags.packed():b})")
flags.unpack(0b1010)
print(f"Unpacked 0b1010: {flags.active_flags()}")
Solution
class BitFlags:
def __init__(self, flag_names):
self._positions = {name: i for i, name in enumerate(flag_names)}
self._names = flag_names
self._bits = 0
def set(self, name):
self._bits |= (1 << self._positions[name])
def clear(self, name):
self._bits &= ~(1 << self._positions[name])
def get(self, name):
return bool((self._bits >> self._positions[name]) & 1)
def toggle(self, name):
self._bits ^= (1 << self._positions[name])
def packed(self):
return self._bits
def unpack(self, value):
self._bits = value
def active_flags(self):
return [name for name in self._names if self.get(name)]
Bitwise operations used:
SET bit N: bits |= (1 << N) OR with mask → forces bit to 1
CLEAR bit N: bits &= ~(1 << N) AND with inverted mask → forces bit to 0
TOGGLE bit N: bits ^= (1 << N) XOR with mask → flips the bit
CHECK bit N: (bits >> N) & 1 shift right, then mask → 0 or 1
Real-world examples:
- Unix file permissions:
chmod 755= owner rwx (111), group r-x (101), other r-x (101) =0b111101101 - TCP flags: SYN, ACK, FIN, RST, PSH, URG packed into a 6-bit field in the TCP header
- CPU status registers: carry, zero, overflow, negative flags packed into a single register word
Why pack flags? A single 64-bit integer can hold 64 boolean flags in 8 bytes. Storing them as separate Python booleans would use 64 x 28 = 1,792 bytes (each Python bool object is 28 bytes). That is a 224x memory difference.
class BitFlags:
"""Pack multiple boolean flags into a single integer using bit fields.
Example:
flags = BitFlags(['read', 'write', 'execute', 'admin'])
flags.set('read')
flags.set('execute')
flags.get('read') # True
flags.get('write') # False
flags.packed() # 0b0101 = 5
"""
def __init__(self, flag_names):
"""Each flag gets a bit position: first flag = bit 0, second = bit 1, etc."""
pass
def set(self, name):
"""Set a flag to True."""
pass
def clear(self, name):
"""Set a flag to False."""
pass
def get(self, name):
"""Return True if the flag is set."""
pass
def toggle(self, name):
"""Flip a flag."""
pass
def packed(self):
"""Return the integer holding all flags."""
pass
def unpack(self, value):
"""Load flags from a packed integer."""
pass
def active_flags(self):
"""Return list of flag names that are currently set."""
passExpected Output
After set read, execute: 5 (0b101)\nActive: ['read', 'execute']\nread=True, write=False, execute=True, admin=False\nAfter toggle execute: 4 (0b100)\nAfter clear read: 0 (0b0)\nUnpacked 0b1010: ['write', 'admin']Hints
Hint 1: Store flag names with their bit positions in a dict: {"read": 0, "write": 1, ...}. Use a single integer `self._bits` to hold all flags. Set bit N with `self._bits |= (1 << N)`.
Hint 2: Clear bit N with `self._bits &= ~(1 << N)`. Toggle with `self._bits ^= (1 << N)`. Check if set with `(self._bits >> N) & 1`.
Build a function decompose_float32(value) that takes a Python float, converts it to IEEE 754 single-precision (32-bit), and extracts the sign, exponent, and mantissa components.
import struct
def decompose_float32(value):
# Pack as float32, unpack as uint32 to get the raw bits
packed = struct.pack('>f', value)
bits = struct.unpack('>I', packed)[0]
# Extract components
binary_str = format(bits, '032b')
sign = (bits >> 31) & 1
exponent_bits = (bits >> 23) & 0xFF
mantissa_bits = bits & 0x7FFFFF
# Calculate actual values
exponent_actual = exponent_bits - 127
mantissa_value = 1.0
for i in range(23):
if mantissa_bits & (1 << (22 - i)):
mantissa_value += 2 ** -(i + 1)
result = (-1) ** sign * mantissa_value * (2 ** exponent_actual)
print(f"Binary: {binary_str}")
print(f"Sign: {sign} ({'+' if sign == 0 else '-'})")
print(f"Exponent bits: {binary_str[1:9]} "
f"(stored: {exponent_bits}, actual: {exponent_actual})")
print(f"Mantissa bits: {binary_str[9:]} ({mantissa_value})")
print(f"Formula: {'+' if sign == 0 else '-'}{mantissa_value} "
f"* 2^{exponent_actual} = {result}")
print("=== 1.0 ===")
decompose_float32(1.0)
print()
print("=== -6.5 ===")
decompose_float32(-6.5)
print()
print("=== 0.1 ===")
decompose_float32(0.1)Solution
import struct
def decompose_float32(value):
packed = struct.pack('>f', value)
bits = struct.unpack('>I', packed)[0]
binary_str = format(bits, '032b')
sign = (bits >> 31) & 1
exponent_bits = (bits >> 23) & 0xFF
mantissa_bits = bits & 0x7FFFFF
exponent_actual = exponent_bits - 127
# Reconstruct mantissa: 1.xxxxx (implicit leading 1)
mantissa_value = 1.0
for i in range(23):
if mantissa_bits & (1 << (22 - i)):
mantissa_value += 2 ** -(i + 1)
result = (-1) ** sign * mantissa_value * (2 ** exponent_actual)
print(f"Binary: {binary_str}")
print(f"Sign: {sign} ({'+' if sign == 0 else '-'})")
print(f"Exponent bits: {binary_str[1:9]} "
f"(stored: {exponent_bits}, actual: {exponent_actual})")
print(f"Mantissa bits: {binary_str[9:]} ({mantissa_value})")
print(f"Formula: {'+' if sign == 0 else '-'}{mantissa_value} "
f"* 2^{exponent_actual} = {result}")
IEEE 754 single-precision layout (32 bits):
| 1 bit | 8 bits | 23 bits |
| sign | exponent | mantissa |
| S | EEEEEEEE | MMMMMMMMMMMMMMMMMMMMMMM |
Worked example: -6.5
Step 1: Sign = 1 (negative)
Step 2: |6.5| = 110.1 in binary = 1.101 * 2^2
Step 3: Exponent = 2 + 127 (bias) = 129 = 10000001
Step 4: Mantissa = 101 (drop the leading 1, pad with zeros to 23 bits)
Result: 1 10000001 10100000000000000000000
S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
Why 0.1 is interesting: Its mantissa bits are 10011001100110011001101 — a repeating pattern that gets truncated, which is exactly why 0.1 + 0.2 != 0.3. The last bit is rounded up, introducing the tiny error.
The struct trick: struct.pack('>f', value) gives us the raw IEEE 754 bytes. struct.unpack('>I', ...) reinterprets those same bytes as an unsigned integer so we can do bit manipulation. This "type punning" is a systems programming technique used in network protocols, file formats, and hardware drivers.
import struct
def decompose_float32(value):
"""Decompose a Python float into its IEEE 754 single-precision components.
Returns a dict with:
'binary': full 32-bit binary string
'sign': 0 or 1
'exponent_bits': 8-bit string
'exponent_value': actual exponent (after subtracting bias of 127)
'mantissa_bits': 23-bit string
'mantissa_value': actual mantissa as float (1.xxxxx in binary)
'formula': string showing sign * mantissa * 2^exponent
"""
passExpected Output
=== 1.0 ===\nBinary: 00111111100000000000000000000000\nSign: 0 (+)\nExponent bits: 01111111 (stored: 127, actual: 0)\nMantissa bits: 00000000000000000000000 (1.0)\nFormula: +1.0 * 2^0 = 1.0Hints
Hint 1: Use `struct.pack(">f", value)` to get the raw 4 bytes of a float32, then `struct.unpack(">I", bytes)` to read those same bytes as an unsigned 32-bit integer. Now you can extract bits.
Hint 2: Sign = bit 31. Exponent = bits 30-23 (subtract bias 127). Mantissa = bits 22-0 (add implicit leading 1 for normal numbers). The formula is (-1)^sign * (1 + mantissa_fraction) * 2^(exponent - 127).
