Python Primitive Data Types Practice Problems & Exercises
Practice: Primitive Data Types
← Back to lessonEasy
Predict the output of every type() call before running. At least two of these will surprise you.
print(type(42))
print(type(3.14))
print(type(True))
print(type(None))
print(type(0xFF))
print(type(1.0))
print(type(0b1010))
print(type(10**100))
print(type(3 + 0j))
print(type(float('inf')))Solution
<class 'int'>
<class 'float'>
<class 'bool'>
<class 'NoneType'>
<class 'int'>
<class 'float'>
<class 'int'>
<class 'int'>
<class 'complex'>
<class 'float'>
Key insights:
0xFF(hex literal) and0b1010(binary literal) are bothint— the base notation is just syntax; the resulting object is alwaysint.10**100isint— Python integers have arbitrary precision, so even a 100-digit number is stillint.1.0isfloateven though it represents a whole number. The decimal point forces float type.float('inf')is afloat— infinity is a valid IEEE 754 floating-point value, not a special type.3 + 0jiscomplex— thejsuffix creates a complex number even if the imaginary part is zero.
Interview note: Knowing that hex/binary/octal literals produce int and that type(True) returns bool (not int) are common gotcha questions.
Expected Output
See solution for exact outputHints
Hint 1: Python has no separate `long` type — all integers are `int`, regardless of size. `42`, `0xFF`, and `2**100` are all `int`.
Hint 2: `True` and `False` are `bool`, not `int` — even though `bool` is a subclass of `int`. `type()` returns the most specific type.
Predict every line of output. These expressions reveal how Python treats booleans as integers in arithmetic.
print(True + True + False) print(False * 100) print(sum([True, False, True, True])) print(True == 1) print(True ** 100) print(True * -1) print(bool(42) + bool(0) == 1)
Solution
2
0
3
True
1
-1
True
Why this works: bool is a subclass of int (specified in PEP 285). When booleans appear in arithmetic expressions, they behave exactly like 1 and 0:
True + True + False=1 + 1 + 0=2False * 100=0 * 100=0sum([True, False, True, True])counts theTruevalues:1 + 0 + 1 + 1=3True == 1isTrue— they are equal by valueTrue ** 100=1 ** 100=1True * -1=1 * -1=-1bool(42)isTrue(any nonzero number is truthy),bool(0)isFalse(zero is falsy), soTrue + False == 1isTrue
Practical use: sum(condition for item in iterable) is an idiomatic Python pattern for counting how many items satisfy a condition. It works precisely because True is 1.
Expected Output
2\n0\n3\nTrue\n1\n-1\nTrueHints
Hint 1: `bool` is a subclass of `int`. `True` is `1` and `False` is `0` in all arithmetic contexts.
Hint 2: `True * -1` works because Python treats `True` as `1` — so it evaluates to `1 * -1 = -1`.
Predict each output. This is the most famous floating-point gotcha — understanding why it happens separates engineers from script kiddies.
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(f"0.1 = {0.1:.20f}")
print(f"0.2 = {0.2:.20f}")
print(f"0.3 = {0.3:.20f}")
print(f"0.1 + 0.2 = {0.1 + 0.2:.20f}")
# The right way to compare floats
import math
print(math.isclose(0.1 + 0.2, 0.3))
print(abs(0.1 + 0.2 - 0.3) < 1e-9)Solution
0.30000000000000004
False
0.1 = 0.10000000000000000555
0.2 = 0.20000000000000001110
0.3 = 0.29999999999999998890
0.1 + 0.2 = 0.30000000000000004441
True
True
Why this happens: Python floats are IEEE 754 double-precision (64-bit). They store numbers in binary, and 0.1 in binary is a repeating fraction — just like 1/3 = 0.333... in decimal. The closest representable value to 0.1 is 0.1000000000000000055511151231257827021181583404541015625.
When you add two such approximations, the tiny errors accumulate:
- Stored
0.1is slightly above 0.1 - Stored
0.2is slightly above 0.2 - Their sum is slightly above 0.3
- But stored
0.3is slightly below 0.3 - So
0.1 + 0.2 != 0.3
The fix: Never compare floats with ==. Use:
math.isclose(a, b)— relative tolerance (default1e-9)abs(a - b) < epsilon— absolute tolerance for values near zero
Interview note: If asked "Is 0.1 + 0.2 == 0.3 in Python?", the answer is no, and you should explain IEEE 754 representation, not just say "floating point is imprecise."
Expected Output
See solution for exact outputHints
Hint 1: `0.1` cannot be represented exactly in binary floating point — it becomes a repeating fraction like 1/3 in decimal.
Hint 2: Use `f"{0.1:.20f}"` to see the actual stored value. The difference from 0.1 is in the 17th decimal place.
Predict every output. This problem tests your understanding of identity vs equality — the most common source of subtle None-related bugs.
x = None # Identity checks print(x is None) print(x is not False) print(x is not 0) # Equality checks print(x == False) print(x == None) print(None is None) # The trap print(x) print(x is None)
Solution
True
True
True
False
True
True
None
True
Key concepts:
-
Noneis a singleton. Python guarantees exactly oneNoneobject exists per interpreter. Every variable assignedNonepoints to the same object. That is whyis Noneworks — it is an identity check against a known unique object. -
isvs==with None:x is None— checks ifxis the None object (identity). This is the correct way.x == None— checks ifxequals None (value). This works for None itself, but a custom class can override__eq__to returnTruewhen compared with None, creating a false positive.
-
Noneis notFalseand not0:None is not FalseisTrue— they are different objectsNone == FalseisFalse— they are not equalNoneis falsy (evaluates toFalsein a boolean context), but it is NOT equal toFalse
-
print(None)outputs the textNone, not an empty line.
PEP 8 rule: Always use is None or is not None — never == None or != None. This is not just style; it prevents bugs with objects that override __eq__.
Expected Output
True\nTrue\nTrue\nFalse\nTrue\nTrue\nNone\nTrueHints
Hint 1: `None` is a singleton — there is exactly ONE None object in a Python process. Every reference to None points to the same object.
Hint 2: `is` checks identity (same object in memory). `==` checks equality (same value). For None, always use `is` because it is both faster and semantically correct.
Medium
Predict every output. Negative floor division is where most engineers get tripped up — Python rounds toward negative infinity, not toward zero like C.
# Positive cases — straightforward
print(7 // 2)
print(7 % 2)
# Negative cases — HERE is the trap
print(-7 // 2)
print(-7 % 2)
print(7 // -2)
print(7 % -2)
# Verify the invariant: a == (a // b) * b + (a % b)
a, b = -7, 2
print(f"{a} == ({a // b}) * {b} + ({a % b}) -> {(a // b) * b + (a % b)}")
# Float division
print(7 / 2)
print(-7 / 2)
print(7 // 2.0)
print(type(7 // 2.0))Solution
3
1
-4
1
-4
-1
-7 == (-4) * 2 + (1) -> -7
3.5
-3.5
3.0
<class 'float'>
Why -7 // 2 is -4, not -3:
Python's // performs floor division — it always rounds toward negative infinity. In C, integer division truncates toward zero, so -7 / 2 == -3. But Python guarantees:
Floor toward -inf: -7 // 2 = -4 (Python)
Truncate toward 0: -7 / 2 = -3 (C, Java)
This design choice ensures the modulo result always has the same sign as the divisor:
-7 % 2 = 1(positive, because divisor2is positive)7 % -2 = -1(negative, because divisor-2is negative)
The invariant a == (a // b) * b + (a % b) always holds:
-7 == (-4) * 2 + 1— correct- If it were
-3(C-style), you would need-7 == (-3) * 2 + (-1), which gives a negative remainder — less useful in practice.
Float floor division: 7 // 2.0 returns 3.0 (a float), not 3 (an int). When either operand is a float, the result type is float, even for //.
Interview note: "What is -7 % 2 in Python vs C?" is a classic question. Python: 1. C: -1. Different languages, different rules.
Expected Output
See solution for exact outputHints
Hint 1: `//` is floor division — it rounds toward negative infinity, not toward zero. This matters for negative numbers: `-7 // 2` is `-4`, not `-3`.
Hint 2: Python guarantees `a == (a // b) * b + (a % b)` for any integers a, b (b != 0). This is the division-modulo invariant.
Predict every output. Infinity and NaN follow IEEE 754 rules that violate normal Python intuition — especially NaN, which breaks the law of identity.
import math
inf = float('inf')
nan = float('nan')
# Infinity arithmetic
print(inf + 1)
print(inf + inf)
print(inf * -1)
print(1 / inf)
print(inf > 10**308)
# NaN — the identity breaker
print(nan == nan)
print(nan is nan)
print(nan != nan)
print(nan < 0)
print(nan > 0)
# How to detect NaN
print(math.isnan(nan))
print(math.isinf(inf))
# The surprise
print(inf - inf)
print(inf / inf)
print(type(inf - inf))Solution
inf
inf
-inf
0.0
True
False
True
True
False
False
True
True
nan
nan
<class 'float'>
Infinity (float('inf')):
- Absorbs additions:
inf + 1 = inf,inf + inf = inf - Negation works:
inf * -1 = -inf - Division by inf:
1 / inf = 0.0(approaches zero) - Greater than any finite number, including
10**308(nearsys.float_info.max)
NaN (float('nan')):
- NaN is not equal to itself.
nan == nanisFalse. This is the IEEE 754 specification, not a Python bug. nan is nanisTrue— they are the same Python object (identity), but not equal (value).nan != nanisTrue— the only value in Python wherex != x.- NaN is not less than zero, not greater than zero, not equal to zero — all comparisons return
False(except!=).
Indeterminate forms:
inf - inf = nan— mathematically indeterminateinf / inf = nan— mathematically indeterminate- These are not errors — they produce NaN, which is IEEE 754's way of saying "this computation has no meaningful result."
Detection: Always use math.isnan() and math.isinf() — never x == float('nan') (which always returns False).
Interview note: "How do you check if a value is NaN in Python?" The wrong answer is x == float('nan'). The right answer is math.isnan(x).
Expected Output
See solution for exact outputHints
Hint 1: `float("inf")` is a valid IEEE 754 value representing positive infinity. Arithmetic with infinity follows mathematical rules: inf + 1 = inf, inf * -1 = -inf.
Hint 2: `float("nan")` (Not a Number) is unique: it is NOT equal to itself. `nan == nan` is `False`. This is by IEEE 754 specification.
Predict every output. This explores the subtle difference between isinstance(), type(), and issubclass() — and how bool's inheritance from int creates surprising results.
# isinstance vs type
print(isinstance(True, int))
print(isinstance(True, bool))
print(type(True) is int)
print(type(True) is bool)
# Subclass relationship
print(issubclass(bool, int))
print(issubclass(int, bool))
# Method Resolution Order
print(bool.__mro__)
# The dictionary key trap
d = {True: "bool", 1: "int", 1.0: "float"}
print(d)
print(len(d))
print(d[True])
print(d[1])
print(d[1.0])
# Hash equality
print(hash(True) == hash(1) == hash(1.0))Solution
True
True
False
True
True
False
(<class 'bool'>, <class 'int'>, <class 'object'>)
{True: 'float'}
1
float
float
float
True
The isinstance vs type distinction:
isinstance(True, int)isTruebecauseboolis a subclass ofint. isinstance walks the inheritance chain.type(True) is intisFalsebecausetype()returns the exact class —bool, notint.
The MRO reveals the chain: bool -> int -> object. This means every bool IS-A int, but not every int IS-A bool.
The dictionary key trap — this one surprises everyone:
Python dictionaries use hash() and == to determine key identity. Since True == 1 == 1.0 and hash(True) == hash(1) == hash(1.0), the dictionary treats them as the same key. Each subsequent assignment to 1 or 1.0 overwrites the previous value, but the original key (True) is kept.
The final dict is {True: 'float'} — a single entry where:
- The key is
True(first key inserted) - The value is
'float'(last value assigned, from1.0)
This is not a bug — it is a consequence of bool subclassing int and Python's hash-equality contract.
Interview note: "What happens when you use True and 1 as dictionary keys?" is an advanced Python gotcha question. The answer: they collide because True == 1 and hash(True) == hash(1).
Expected Output
See solution for exact outputHints
Hint 1: `isinstance(True, int)` is `True` because `bool` inherits from `int`. But `type(True) is int` is `False` — `type()` returns the exact class.
Hint 2: Check the Method Resolution Order (MRO) of `bool` to see the full inheritance chain: `bool.__mro__`.
Build a function that performs safe None checking and distinguishes between actual None, falsy values, and truthy values. Then test it against a class that tries to fake being None.
def is_none_safe(value):
"""Return (is_actual_none, is_falsy, type_name)."""
is_actual_none = value is None
is_falsy = not value
type_name = type(value).__name__
return (is_actual_none, is_falsy, type_name)
# Test cases
test_values = [None, 0, False, "", [], 0.0, 42, "hello", [1, 2]]
for v in test_values:
actual_none, falsy, tname = is_none_safe(v)
print(f"{str(v):10s} -> None: {str(actual_none):5s} Falsy: {str(falsy):5s} Type: {tname}")
# The adversary — a class that pretends to be None
class Sneaky:
def __eq__(self, other):
return other is None # Claims to be equal to None!
def __bool__(self):
return False # Also pretends to be falsy
print("\n--- Adversary test ---")
s = Sneaky()
print(f"s == None: {s == None}")
print(f"s is None: {s is None}")
actual_none, falsy, tname = is_none_safe(s)
print(f"is_none_safe: None: {actual_none}, Falsy: {falsy}, Type: {tname}")Solution
None -> None: True Falsy: True Type: NoneType
0 -> None: False Falsy: True Type: int
False -> None: False Falsy: True Type: bool
-> None: False Falsy: True Type: str
[] -> None: False Falsy: True Type: list
0.0 -> None: False Falsy: True Type: float
42 -> None: False Falsy: False Type: int
hello -> None: False Falsy: False Type: str
[1, 2] -> None: False Falsy: False Type: list
--- Adversary test ---
s == None: True
s is None: False
is_none_safe: None: False, Falsy: True, Type: Sneaky
Why is cannot be fooled:
is checks object identity — whether two names point to the exact same object in memory (same id()). There is no dunder method to override is. You cannot make x is None return True for any object that is not actually None.
== checks value equality via __eq__, which any class can override. The Sneaky class above overrides __eq__ to return True when compared with None — but is sees through it immediately.
The falsy trap: Many values are falsy in Python — None, 0, False, "", [], {}, set(), 0.0, 0j. Code like if not value: does NOT check for None — it triggers on all of these. Always use if value is None: when you specifically mean None.
Real-world impact: This matters in function arguments:
def process(data=None):
if data is None: # Correct — only triggers on None
data = []
if not data: # Bug — also triggers on empty list []
data = []
def is_none_safe(value):
"""Return True if value is None.
Must handle objects with custom __eq__ that could
return True when compared to None.
Also detect 'None-like' values: empty string, 0, False, empty list.
Return a tuple: (is_actual_none, is_falsy, type_name)
"""
passExpected Output
See solution for exact outputHints
Hint 1: Use `value is None` for the true None check — never `value == None`. An object could override `__eq__` to fool the equality check.
Hint 2: For the falsy check, use `not value` — but remember that `not None`, `not 0`, `not False`, `not ""`, and `not []` are all `True`.
Demonstrate why financial calculations must use decimal.Decimal instead of float. Show the precision difference and the critical mistake of constructing Decimal from a float.
from decimal import Decimal, getcontext
# Float arithmetic — the problem
print("=== Float ===")
print(f"0.1 + 0.2 = {0.1 + 0.2}")
total = sum(0.1 for _ in range(10))
print(f"0.1 * 10 = {total}")
print(f"== 1.0? {total == 1.0}")
# Decimal arithmetic — the solution
print("\n=== Decimal (from string) ===")
d1 = Decimal("0.1")
d2 = Decimal("0.2")
print(f"0.1 + 0.2 = {d1 + d2}")
total_d = sum(Decimal("0.1") for _ in range(10))
print(f"0.1 * 10 = {total_d}")
print(f"== 1.0? {total_d == Decimal('1.0')}")
# The critical mistake — constructing from float
print("\n=== Decimal (from float) — THE TRAP ===")
bad = Decimal(0.1)
good = Decimal("0.1")
print(f"Decimal(0.1) = {bad}")
print(f"Decimal('0.1') = {good}")
print(f"Equal? {bad == good}")
# Precision control
print("\n=== Precision control ===")
getcontext().prec = 50
print(f"1/7 as Decimal: {Decimal(1) / Decimal(7)}")
getcontext().prec = 6
print(f"1/7 at prec=6: {Decimal(1) / Decimal(7)}")Solution
=== Float ===
0.1 + 0.2 = 0.30000000000000004
0.1 * 10 = 0.9999999999999999
== 1.0? False
=== Decimal (from string) ===
0.1 + 0.2 = 0.3
0.1 * 10 = 1.0
== 1.0? True
=== Decimal (from float) — THE TRAP ===
Decimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1') = 0.1
Equal? False
=== Precision control ===
1/7 as Decimal: 0.14285714285714285714285714285714285714285714285714
1/7 at prec=6: 0.142857
Why Decimal solves the problem: decimal.Decimal uses base-10 arithmetic. Since 0.1 is exact in base 10, Decimal("0.1") + Decimal("0.2") is exactly 0.3.
The trap: Decimal(0.1) takes a float as input. The float 0.1 is already imprecise (it is 0.100000000000000005551... in IEEE 754). Decimal faithfully captures that imprecision. Always construct Decimal from strings: Decimal("0.1").
When to use Decimal:
- Financial calculations (money, taxes, billing)
- Any domain where rounding errors are unacceptable
- When you need exact decimal representation
When NOT to use Decimal:
- Scientific computing (use NumPy/float — speed matters more than decimal precision)
- Machine learning (model weights are inherently approximate)
- Performance-critical loops (Decimal is ~100x slower than float)
Interview note: "How would you handle currency calculations in Python?" The answer is decimal.Decimal with string construction, never float.
Expected Output
See solution for exact outputHints
Hint 1: `decimal.Decimal("0.1")` (from a string) is exact. `decimal.Decimal(0.1)` (from a float) captures the float imprecision — it stores the exact float value, which is not 0.1.
Hint 2: Set precision with `decimal.getcontext().prec`. This controls significant digits for all Decimal operations.
Hard
Build a function that generates a complete type coercion table. For every pair of types (int, float, bool, None) and every operator (+, *, /), show: the result value, the result type, and whether a TypeError occurs. This reveals Python's full coercion hierarchy.
def build_coercion_table():
"""Generate a type coercion table for primitive operations."""
types = {
'int': 7,
'float': 3.14,
'bool': True,
'None': None,
}
operators = {
'+': lambda a, b: a + b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
'==': lambda a, b: a == b,
}
results = []
for op_name, op_func in operators.items():
print(f"\n{'='*60}")
print(f" Operator: {op_name}")
print(f"{'='*60}")
print(f" {'Left':>6s} {op_name:^3s} {'Right':<6s} -> {'Result':>20s} {'Type':<15s}")
print(f" {'-'*55}")
for left_name, left_val in types.items():
for right_name, right_val in types.items():
try:
result = op_func(left_val, right_val)
result_type = type(result).__name__
print(f" {left_name:>6s} {op_name:^3s} {right_name:<6s} -> {str(result):>20s} {result_type:<15s}")
except TypeError as e:
print(f" {left_name:>6s} {op_name:^3s} {right_name:<6s} -> {'TypeError':>20s} {'---':<15s}")
except ZeroDivisionError:
print(f" {left_name:>6s} {op_name:^3s} {right_name:<6s} -> {'ZeroDivError':>20s} {'---':<15s}")
build_coercion_table()
# Summarize the coercion rules
print("\n" + "="*60)
print(" COERCION HIERARCHY (for arithmetic operators)")
print("="*60)
print(" bool -> int -> float -> complex")
print()
print(" Rules:")
print(" 1. bool is coerced to int (True->1, False->0)")
print(" 2. int is coerced to float when mixed with float")
print(" 3. None cannot participate in arithmetic (TypeError)")
print(" 4. == works across all types (no coercion needed)")
print(" 5. bool/int/float: result type is the 'wider' type")Solution
============================================================
Operator: +
============================================================
Left + Right -> Result Type
-------------------------------------------------------
int + int -> 14 int
int + float -> 10.14 float
int + bool -> 8 int
int + None -> TypeError ---
float + int -> 10.14 float
float + float -> 6.28 float
float + bool -> 4.14 float
float + None -> TypeError ---
bool + int -> 8 int
bool + float -> 4.14 float
bool + bool -> 2 int
bool + None -> TypeError ---
None + int -> TypeError ---
None + float -> TypeError ---
None + bool -> TypeError ---
None + None -> TypeError ---
============================================================
Operator: *
============================================================
Left * Right -> Result Type
-------------------------------------------------------
int * int -> 49 int
int * float -> 21.98 float
int * bool -> 7 int
int * None -> TypeError ---
float * int -> 21.98 float
float * float -> 9.8596 float
float * bool -> 3.14 float
float * None -> TypeError ---
bool * int -> 7 int
bool * float -> 3.14 float
bool * bool -> 1 int
bool * None -> TypeError ---
None * int -> TypeError ---
None * float -> TypeError ---
None * bool -> TypeError ---
None * None -> TypeError ---
============================================================
Operator: /
============================================================
Left / Right -> Result Type
-------------------------------------------------------
int / int -> 1.0 float
int / float -> 2.2292993630573248 float
int / bool -> 7.0 float
int / None -> TypeError ---
float / int -> 0.44857142857142857 float
float / float -> 1.0 float
float / bool -> 3.14 float
float / None -> TypeError ---
bool / int -> 0.14285714285714285 float
bool / float -> 0.3184713375796178 float
bool / bool -> 1.0 float
bool / None -> TypeError ---
None / int -> TypeError ---
None / float -> TypeError ---
None / bool -> TypeError ---
None / None -> TypeError ---
============================================================
Operator: ==
============================================================
Left == Right -> Result Type
-------------------------------------------------------
int == int -> True bool
int == float -> False bool
int == bool -> False bool
int == None -> False bool
float == int -> False bool
float == float -> True bool
float == bool -> False bool
float == None -> False bool
bool == int -> False bool
bool == float -> False bool
bool == bool -> True bool
bool == None -> False bool
None == int -> False bool
None == float -> False bool
None == bool -> False bool
None == None -> True bool
============================================================
COERCION HIERARCHY (for arithmetic operators)
============================================================
bool -> int -> float -> complex
Rules:
1. bool is coerced to int (True->1, False->0)
2. int is coerced to float when mixed with float
3. None cannot participate in arithmetic (TypeError)
4. == works across all types (no coercion needed)
5. bool/int/float: result type is the 'wider' type
Key patterns revealed by the table:
int op int -> int(except/which always returnsfloat)int op float -> float— int is promoted to float before the operationbool op int -> int— bool is promoted to int (True=1, False=0)bool op float -> float— bool is promoted all the way to floatbool op bool -> int— yes,int, notbool! Arithmetic on bools produces ints.None op anything -> TypeError— None supports no arithmetic operators/ always returns float— evenint / intreturnsfloatin Python 3== works everywhere— comparison never raises TypeError between primitive types
The coercion rule in one sentence: When operand types differ, the "narrower" type is promoted to the "wider" type: bool -> int -> float -> complex. The result type is always the widest operand type (except / which forces float).
Why bool + bool = int: When Python coerces True + True, both are promoted to int (1 + 1), and the result is int(2). Python does NOT re-check if the result could be a bool. Arithmetic always exits the bool type.
def build_coercion_table():
"""Build a table showing the result type when combining
int, float, bool, and None with +, *, /, and ==.
Handle TypeErrors gracefully.
"""
passExpected Output
See solution for full tableHints
Hint 1: Python coerces types in a hierarchy: bool -> int -> float -> complex. When you add an int and a float, the int is promoted to float first.
Hint 2: Not all combinations work — `None + 1` raises TypeError. Use try/except to handle unsupported operations and record the error.
Build a TrackedFloat class that wraps a float but internally maintains a Decimal ground truth. After every arithmetic operation, it reports the cumulative precision error. This reveals exactly how much precision you lose over a sequence of operations.
from decimal import Decimal, getcontext
getcontext().prec = 50 # High precision for ground truth
class TrackedFloat:
"""Float wrapper that tracks precision loss against Decimal ground truth."""
def __init__(self, value, _decimal=None, _ops=0):
if isinstance(value, str):
self.float_val = float(value)
self.decimal_val = Decimal(value)
elif isinstance(value, TrackedFloat):
self.float_val = value.float_val
self.decimal_val = value.decimal_val
else:
self.float_val = float(value)
self.decimal_val = Decimal(str(value)) if _decimal is None else _decimal
self.ops = _ops
def _make(self, float_result, decimal_result, ops):
t = TrackedFloat.__new__(TrackedFloat)
t.float_val = float_result
t.decimal_val = decimal_result
t.ops = ops
return t
def __add__(self, other):
other = self._coerce(other)
return self._make(
self.float_val + other.float_val,
self.decimal_val + other.decimal_val,
self.ops + other.ops + 1,
)
def __sub__(self, other):
other = self._coerce(other)
return self._make(
self.float_val - other.float_val,
self.decimal_val - other.decimal_val,
self.ops + other.ops + 1,
)
def __mul__(self, other):
other = self._coerce(other)
return self._make(
self.float_val * other.float_val,
self.decimal_val * other.decimal_val,
self.ops + other.ops + 1,
)
def __truediv__(self, other):
other = self._coerce(other)
return self._make(
self.float_val / other.float_val,
self.decimal_val / other.decimal_val,
self.ops + other.ops + 1,
)
def _coerce(self, other):
if isinstance(other, TrackedFloat):
return other
return TrackedFloat(other)
@property
def error(self):
"""Absolute error: |float_result - decimal_truth|"""
return abs(self.decimal_val - Decimal(str(self.float_val)))
@property
def relative_error(self):
"""Relative error as a ratio."""
if self.decimal_val == 0:
return Decimal('0') if self.float_val == 0.0 else Decimal('inf')
return abs(self.error / self.decimal_val)
def report(self):
print(f" Float value: {self.float_val}")
print(f" Exact value: {self.decimal_val}")
print(f" Abs error: {self.error:.2e}")
print(f" Rel error: {self.relative_error:.2e}")
print(f" Operations: {self.ops}")
def __repr__(self):
return f"TrackedFloat({self.float_val}, error={self.error:.2e}, ops={self.ops})"
# Experiment 1: Sum 0.1 ten times
print("=== Sum 0.1 ten times ===")
total = TrackedFloat("0.0")
for _ in range(10):
total = total + TrackedFloat("0.1")
total.report()
# Experiment 2: Sum 0.1 one thousand times
print("\n=== Sum 0.1 one thousand times ===")
total_1k = TrackedFloat("0.0")
for _ in range(1000):
total_1k = total_1k + TrackedFloat("0.1")
total_1k.report()
# Experiment 3: Catastrophic cancellation
print("\n=== Catastrophic cancellation ===")
a = TrackedFloat("1.0") + TrackedFloat("1e-16")
b = a - TrackedFloat("1.0")
print("(1.0 + 1e-16) - 1.0:")
b.report()
print(f" Expected: 1e-16 = {Decimal('1e-16')}")
# Experiment 4: Multiplication chain
print("\n=== Multiply 1.1 twenty times ===")
product = TrackedFloat("1.0")
for _ in range(20):
product = product * TrackedFloat("1.1")
product.report()
print(f" Expected: 1.1^20 = {Decimal('1.1') ** 20}")Solution
=== Sum 0.1 ten times ===
Float value: 0.9999999999999999
Exact value: 1.0
Abs error: 1.11E-16
Rel error: 1.11E-16
Operations: 10
=== Sum 0.1 one thousand times ===
Float value: 99.99999999999857
Exact value: 100.0
Abs error: 1.43E-12
Rel error: 1.43E-14
Operations: 1000
=== Catastrophic cancellation ===
(1.0 + 1e-16) - 1.0:
Float value: 0.0
Exact value: 1E-16
Abs error: 1E-16
Rel error: 1
Operations: 2
Expected: 1e-16 = 1E-16
=== Multiply 1.1 twenty times ===
Float value: 6.727499949325611
Exact value: 6.72749994932560009201...
Abs error: ...E-16
Rel error: ...E-17
Operations: 20
Expected: 1.1^20 = 6.72749994932560009201...
(Exact values vary by platform — the structure and magnitude of errors are consistent.)
What this reveals:
-
Error accumulates linearly with additions: 10 additions of 0.1 gives ~1e-16 error. 1000 additions gives ~1e-12 error. Each addition can introduce up to 1 ULP (unit in the last place) of error.
-
Catastrophic cancellation is real:
(1.0 + 1e-16) - 1.0should be1e-16but produces0.0— 100% relative error! The addition1.0 + 1e-16cannot be represented (1.0 absorbs the tiny value), so when you subtract 1.0, all information is lost. This is NOT a rounding error — it is total information destruction. -
Multiplication errors grow multiplicatively: Each multiplication can introduce a relative error of up to
epsilon/2. Afternmultiplications, the relative error is approximatelyn * epsilon/2.
Real-world applications:
- Financial systems: use
Decimal— errors in money are unacceptable - Scientific computing: use compensated summation (Kahan algorithm) for long sums
- Machine learning: use float32 for speed, but monitor gradient magnitudes for vanishing/exploding precision
Engineering principle: TrackedFloat is a debugging tool. In production, you would not use it (too slow). But running your numerical pipeline through TrackedFloat during development reveals exactly where precision loss occurs and how fast it accumulates.
class TrackedFloat:
"""A float wrapper that tracks cumulative precision loss
by comparing against a Decimal ground truth.
"""
def __init__(self, value):
pass
def __add__(self, other):
pass
def __repr__(self):
passExpected Output
See solution for full outputHints
Hint 1: Store both a `float` value and a `Decimal` value (from string). After each arithmetic operation, compute the difference between the float result and the Decimal result.
Hint 2: Override `__add__`, `__sub__`, `__mul__`, `__truediv__` to return new TrackedFloat instances that accumulate the error. Track the number of operations performed.
Build a statistics calculator that handles the full zoo of numeric edge cases: NaN, None, inf, -inf, bools mixed with ints and floats. Real-world data is messy — your function must survive it all.
import math
from collections import Counter
def nan_safe_stats(data):
"""Compute statistics on dirty numeric data.
Handles: NaN, None, inf, -inf, bool, int, float, mixed types.
Returns dict with: count, valid, skipped, mean, median, std, min, max,
type_distribution, skip_reasons.
"""
skip_reasons = Counter()
valid = []
for i, val in enumerate(data):
# Skip None
if val is None:
skip_reasons['None'] += 1
continue
# Convert bool to int (before numeric check, since bool is int)
if isinstance(val, bool):
val = int(val)
# Must be numeric
if not isinstance(val, (int, float)):
skip_reasons[f'non-numeric ({type(val).__name__})'] += 1
continue
# Convert to float for uniform handling
val = float(val)
# Skip NaN
if math.isnan(val):
skip_reasons['NaN'] += 1
continue
# Skip infinities
if math.isinf(val):
skip_reasons[f'inf ({val})'] += 1
continue
valid.append(val)
result = {
'total_count': len(data),
'valid_count': len(valid),
'skipped_count': len(data) - len(valid),
'skip_reasons': dict(skip_reasons),
}
if not valid:
result.update({
'mean': float('nan'),
'median': float('nan'),
'std': float('nan'),
'min': float('nan'),
'max': float('nan'),
})
return result
# Mean
n = len(valid)
mean = sum(valid) / n
# Median
sorted_vals = sorted(valid)
if n % 2 == 1:
median = sorted_vals[n // 2]
else:
median = (sorted_vals[n // 2 - 1] + sorted_vals[n // 2]) / 2
# Standard deviation (population)
variance = sum((x - mean) ** 2 for x in valid) / n
std = math.sqrt(variance)
result.update({
'mean': round(mean, 6),
'median': round(median, 6),
'std': round(std, 6),
'min': min(valid),
'max': max(valid),
})
return result
def print_stats(label, data):
print(f"\n{'='*50}")
print(f" {label}")
print(f"{'='*50}")
print(f" Input: {data}")
stats = nan_safe_stats(data)
for key, val in stats.items():
print(f" {key:>15s}: {val}")
# Test 1: Clean data
print_stats("Clean data", [1, 2, 3, 4, 5])
# Test 2: Data with NaN and None
print_stats("NaN and None mixed in",
[1.0, float('nan'), 3.0, None, 5.0, float('nan'), 7.0])
# Test 3: Infinity mixed in
print_stats("Infinity contamination",
[10, 20, float('inf'), 30, float('-inf'), 40])
# Test 4: Booleans mixed with numbers
print_stats("Bools mixed with numbers",
[True, False, 1, 2, 3, True, False, 0])
# Test 5: All invalid
print_stats("All invalid data",
[None, float('nan'), float('nan'), None])
# Test 6: The kitchen sink
print_stats("Kitchen sink",
[42, 3.14, True, False, None, float('nan'), float('inf'),
float('-inf'), 0, -7, 2.718, None, float('nan'), 100])
# Bonus: Show why naive approaches fail
print(f"\n{'='*50}")
print(" WHY NAIVE APPROACHES FAIL")
print(f"{'='*50}")
dirty = [1, 2, float('nan'), 4, 5]
print(f" sum({dirty}) = {sum(dirty)}")
print(f" min({dirty}) = {min(dirty)}")
print(f" max({dirty}) = {max(dirty)}")
print(" NaN poisons everything — one NaN destroys your entire calculation.")Solution
==================================================
Clean data
==================================================
Input: [1, 2, 3, 4, 5]
total_count: 5
valid_count: 5
skipped_count: 0
skip_reasons: {}
mean: 3.0
median: 3.0
std: 1.414214
min: 1.0
max: 5.0
==================================================
NaN and None mixed in
==================================================
Input: [1.0, nan, 3.0, None, 5.0, nan, 7.0]
total_count: 7
valid_count: 4
skipped_count: 3
skip_reasons: {'NaN': 2, 'None': 1}
mean: 4.0
median: 4.0
std: 2.236068
min: 1.0
max: 7.0
==================================================
Infinity contamination
==================================================
Input: [10, 20, inf, 30, -inf, 40]
total_count: 6
valid_count: 4
skipped_count: 2
skip_reasons: {'inf (inf)': 1, 'inf (-inf)': 1}
mean: 25.0
median: 25.0
std: 11.18034
min: 10.0
max: 40.0
==================================================
Booleans mixed with numbers
==================================================
Input: [True, False, 1, 2, 3, True, False, 0]
total_count: 8
valid_count: 8
skipped_count: 0
skip_reasons: {}
mean: 1.0
median: 1.0
std: 1.0
min: 0.0
max: 3.0
==================================================
All invalid data
==================================================
Input: [None, nan, nan, None]
total_count: 4
valid_count: 0
skipped_count: 4
skip_reasons: {'None': 2, 'NaN': 2}
mean: nan
median: nan
std: nan
min: nan
max: nan
==================================================
Kitchen sink
==================================================
Input: [42, 3.14, True, False, None, nan, inf, -inf, 0, -7, 2.718, None, nan, 100]
total_count: 14
valid_count: 8
skipped_count: 6
skip_reasons: {'None': 2, 'NaN': 2, 'inf (inf)': 1, 'inf (-inf)': 1}
mean: 17.6073
median: 1.929
std: 33.031...
min: -7.0
max: 100.0
==================================================
WHY NAIVE APPROACHES FAIL
==================================================
sum([1, 2, nan, 4, 5]) = nan
min([1, 2, nan, 4, 5]) = 1
max([1, 2, nan, 4, 5]) = 5
NaN poisons everything — one NaN destroys your entire calculation.
(Exact decimal places may vary slightly by platform.)
Why this problem matters in production:
-
NaN is contagious.
sum([1, 2, nan, 4, 5])isnan. One bad value destroys an entire aggregation. This is the single most common data pipeline bug in production ML systems. -
min()andmax()are inconsistent with NaN.min([1, nan])returns1butmin([nan, 1])returnsnan— the result depends on order! This is because Python'smin/maxuse comparisons, andnan < 1isFalse. -
None vs NaN are different failure modes.
Nonemeans "no value was provided" (missing data).NaNmeans "a computation failed" (e.g.,0/0in NumPy). Your cleaning logic should distinguish them — they may require different imputation strategies. -
Bool contamination is silent. If someone passes
[True, True, False]to a stats function, you getmean = 0.667. Is that what they wanted? The function convertsTrue -> 1andFalse -> 0, which is correct per Python semantics but may surprise users expecting only numeric input. -
Infinity must be filtered separately.
mean([1, 2, float('inf')])isinf— technically correct but useless. In practice, infinities usually indicate overflow or bad data and should be excluded from statistics.
In production, use pandas with df.describe() and df.dropna(), or NumPy's np.nanmean(), np.nanstd(), etc. But understanding what these functions do internally — filtering NaN, handling mixed types, guarding against infinity — is essential for debugging data pipelines when the built-in functions give unexpected results.
def nan_safe_stats(data):
"""Compute mean, median, std, min, max on a list that
may contain NaN, None, inf, and mixed numeric types.
Skip invalid values. Return a stats dict.
"""
passExpected Output
See solution for full outputHints
Hint 1: Use `math.isnan()` and `math.isinf()` to filter values. Remember that `float("nan") == float("nan")` is False, so you cannot filter NaN with `!=`.
Hint 2: Handle edge cases: empty list after filtering, all-NaN input, mixed int/float/bool values (remember bool is int). Convert everything to float for consistent statistics.
