Skip to main content

Python Primitive Data Types Practice Problems & Exercises

Practice: Primitive Data Types

12 problems4 Easy5 Medium3 Hard45–60 min
← Back to lesson

Easy

#1Type Detective: Predict the type()Easy
type()literalsintfloatboolNoneType

Predict the output of every type() call before running. At least two of these will surprise you.

Python
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) and 0b1010 (binary literal) are both int — the base notation is just syntax; the resulting object is always int.
  • 10**100 is int — Python integers have arbitrary precision, so even a 100-digit number is still int.
  • 1.0 is float even though it represents a whole number. The decimal point forces float type.
  • float('inf') is a float — infinity is a valid IEEE 754 floating-point value, not a special type.
  • 3 + 0j is complex — the j suffix 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 output
Hints

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.

#2Bool Arithmetic: True + True + FalseEasy
boolintarithmeticsubclass

Predict every line of output. These expressions reveal how Python treats booleans as integers in arithmetic.

Python
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 = 2
  • False * 100 = 0 * 100 = 0
  • sum([True, False, True, True]) counts the True values: 1 + 0 + 1 + 1 = 3
  • True == 1 is True — they are equal by value
  • True ** 100 = 1 ** 100 = 1
  • True * -1 = 1 * -1 = -1
  • bool(42) is True (any nonzero number is truthy), bool(0) is False (zero is falsy), so True + False == 1 is True

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\nTrue
Hints

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`.

#3Float Precision: The 0.1 + 0.2 ProblemEasy
floatprecisionIEEE-754representation

Predict each output. This is the most famous floating-point gotcha — understanding why it happens separates engineers from script kiddies.

Python
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.1 is slightly above 0.1
  • Stored 0.2 is slightly above 0.2
  • Their sum is slightly above 0.3
  • But stored 0.3 is slightly below 0.3
  • So 0.1 + 0.2 != 0.3

The fix: Never compare floats with ==. Use:

  • math.isclose(a, b) — relative tolerance (default 1e-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 output
Hints

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.

#4None Identity: is vs ==Easy
NoneNoneTypeisidentitysingleton

Predict every output. This problem tests your understanding of identity vs equality — the most common source of subtle None-related bugs.

Python
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:

  1. None is a singleton. Python guarantees exactly one None object exists per interpreter. Every variable assigned None points to the same object. That is why is None works — it is an identity check against a known unique object.

  2. is vs == with None:

    • x is None — checks if x is the None object (identity). This is the correct way.
    • x == None — checks if x equals None (value). This works for None itself, but a custom class can override __eq__ to return True when compared with None, creating a false positive.
  3. None is not False and not 0:

    • None is not False is True — they are different objects
    • None == False is False — they are not equal
    • None is falsy (evaluates to False in a boolean context), but it is NOT equal to False
  4. print(None) outputs the text None, 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\nTrue
Hints

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

#5Integer Division vs True Division Edge CasesMedium
divisionfloor-divisionmodulonegative-numbers

Predict every output. Negative floor division is where most engineers get tripped up — Python rounds toward negative infinity, not toward zero like C.

Python
# 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 divisor 2 is positive)
  • 7 % -2 = -1 (negative, because divisor -2 is 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 output
Hints

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.

#6Infinity and NaN: Float's Special ValuesMedium
floatinfnanIEEE-754math

Predict every output. Infinity and NaN follow IEEE 754 rules that violate normal Python intuition — especially NaN, which breaks the law of identity.

Python
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 (near sys.float_info.max)

NaN (float('nan')):

  • NaN is not equal to itself. nan == nan is False. This is the IEEE 754 specification, not a Python bug.
  • nan is nan is True — they are the same Python object (identity), but not equal (value).
  • nan != nan is True — the only value in Python where x != 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 indeterminate
  • inf / 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 output
Hints

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.

#7Bool as Int Subclass: The isinstance ChainMedium
boolintisinstanceissubclasstype-hierarchyMRO

Predict every output. This explores the subtle difference between isinstance(), type(), and issubclass() — and how bool's inheritance from int creates surprising results.

Python
# 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) is True because bool is a subclass of int. isinstance walks the inheritance chain.
  • type(True) is int is False because type() returns the exact class — bool, not int.

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, from 1.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 output
Hints

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__`.

#8Safe None Comparison FunctionMedium
NonecomparisonNoneTypedefensive-programmingtype-checking

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.

Python
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)
    """
    pass
Expected Output
See solution for exact output
Hints

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`.

#9Decimal vs Float: Precision ShowdownMedium
decimalfloatprecisionDecimalfinancial

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.

Python
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 output
Hints

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

#10Type Coercion Table GeneratorHard
type-coercionoperator-overloadingtype-hierarchyintfloatboolNone

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.

Python
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:

  1. int op int -> int (except / which always returns float)
  2. int op float -> float — int is promoted to float before the operation
  3. bool op int -> int — bool is promoted to int (True=1, False=0)
  4. bool op float -> float — bool is promoted all the way to float
  5. bool op bool -> int — yes, int, not bool! Arithmetic on bools produces ints.
  6. None op anything -> TypeError — None supports no arithmetic operators
  7. / always returns float — even int / int returns float in Python 3
  8. == 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.
    """
    pass
Expected Output
See solution for full table
Hints

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.

#11TrackedFloat: Tracking Precision LossHard
custom-classoperator-overloadingprecisionfloatdunder-methods

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.

Python
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:

  1. 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.

  2. Catastrophic cancellation is real: (1.0 + 1e-16) - 1.0 should be 1e-16 but produces 0.0 — 100% relative error! The addition 1.0 + 1e-16 cannot 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.

  3. Multiplication errors grow multiplicatively: Each multiplication can introduce a relative error of up to epsilon/2. After n multiplications, the relative error is approximately n * 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):
        pass
Expected Output
See solution for full output
Hints

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.

#12NaN-Aware Statistics CalculatorHard
nanstatisticsfloatedge-casesdefensive-programming

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.

Python
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:

  1. NaN is contagious. sum([1, 2, nan, 4, 5]) is nan. One bad value destroys an entire aggregation. This is the single most common data pipeline bug in production ML systems.

  2. min() and max() are inconsistent with NaN. min([1, nan]) returns 1 but min([nan, 1]) returns nan — the result depends on order! This is because Python's min/max use comparisons, and nan < 1 is False.

  3. None vs NaN are different failure modes. None means "no value was provided" (missing data). NaN means "a computation failed" (e.g., 0/0 in NumPy). Your cleaning logic should distinguish them — they may require different imputation strategies.

  4. Bool contamination is silent. If someone passes [True, True, False] to a stats function, you get mean = 0.667. Is that what they wanted? The function converts True -> 1 and False -> 0, which is correct per Python semantics but may surprise users expecting only numeric input.

  5. Infinity must be filtered separately. mean([1, 2, float('inf')]) is inf — 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.
    """
    pass
Expected Output
See solution for full output
Hints

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.

© 2026 EngineersOfAI. All rights reserved.