Skip to main content

Python Type Casting and Coercion Practice Problems & Exercises

Practice: Type Casting and Coercion

10 problems3 Easy4 Medium3 Hard35–50 min
← Back to lesson

Easy

#1Conversion Output PredictorEasy
intfloatstrtype-casting

Predict the output of each conversion, then run to verify. Pay close attention to the types and representations.

Python
print(int("123"))
print(float(3))
print(float("3.14"))
print(bool(1))
print(int(42.999))
print(float(False))
Solution
123
3.0
3.14
True
42
0.0

Breakdown:

  • int("123") — parses the string "123" to integer 123.
  • float(3) — converts integer 3 to 3.0. No data loss.
  • float("3.14") — parses the string to floating-point 3.14.
  • bool(1) — any non-zero integer is truthy, so True.
  • int(42.999)truncates toward zero, not rounds. The .999 is discarded, yielding 42.
  • float(False)False is 0 in numeric context, so 0.0.

Key insight: int() on a float always truncates toward zero. It does not round. This is the single most common source of bugs when converting between float and int.

Expected Output
123\n3.0\n3.14\nTrue\n42\n0.0
Hints

Hint 1: int() on a string requires the string to represent a valid integer — no decimals.

Hint 2: float() on an int simply adds .0 — no data loss.

Hint 3: str() on any type returns its string representation.

#2Bool Truthiness TesterEasy
booltruthinesstype-casting

Predict the output of bool() on each value. Think about Python's falsy values before running.

Python
print(bool(0))
print(bool(42))
print(bool(""))
print(bool("0"))
print(bool([]))
print(bool([0]))
print(bool(None))
print(bool(0.001))
Solution
False
True
False
True
False
True
False
True

Breakdown:

  • bool(0) — zero is falsy → False.
  • bool(42) — any non-zero number is truthy → True.
  • bool("") — empty string is falsy → False.
  • bool("0") — the string "0" is non-empty (length 1), so truthy → True. This trips up many beginners.
  • bool([]) — empty list is falsy → False.
  • bool([0]) — the list contains one element (even though it is 0), so it is non-empty → True.
  • bool(None)None is always falsy → False.
  • bool(0.001) — any non-zero float is truthy → True.

Key insight: Truthiness depends on whether a container is empty or a number is zero — not on the truthiness of the contents. [0] is truthy because the list is non-empty, even though 0 itself is falsy.

Expected Output
False\nTrue\nFalse\nTrue\nFalse\nTrue\nFalse\nTrue
Hints

Hint 1: Empty containers (list, dict, string, set) are falsy.

Hint 2: Zero values (0, 0.0, 0j) are falsy. Everything else is truthy.

Hint 3: None is always falsy.

#3Float-to-Int Data Loss DemonstratorEasy
truncationintfloatdata-loss

Write show_truncation(value) that displays all four ways Python converts a float to an integer, revealing the subtle differences.

show_truncation(7.9)
# Value: 7.9 → int: 7, floor: 7, ceil: 8, round: 8

show_truncation(-7.9)
# Value: -7.9 → int: -7, floor: -8, ceil: -7, round: -8
Solution
import math

def show_truncation(value):
print(f"Value: {value} → int: {int(value)}, floor: {math.floor(value)}, ceil: {math.ceil(value)}, round: {round(value)}")

show_truncation(7.9)
show_truncation(-7.9)
show_truncation(3.5)

Critical distinction for negative numbers:

  • int(-7.9)-7 (truncates toward zero — chops off the decimal)
  • math.floor(-7.9)-8 (rounds toward negative infinity)

For positive numbers, int() and math.floor() happen to agree. For negative numbers, they diverge. This is one of the most common sources of off-by-one bugs in financial and scientific code.

Banker's rounding: round(3.5) returns 4 in Python 3. But round(4.5) returns 4 (rounds to even). This is IEEE 754 "round half to even" — designed to reduce cumulative bias.

import math

def show_truncation(value):
    """Print the original float, int() result, math.floor(),
    math.ceil(), and round() — showing how each differs."""
    pass

# Test with positive and negative floats
show_truncation(7.9)
show_truncation(-7.9)
show_truncation(3.5)
Expected Output
Value: 7.9 → int: 7, floor: 7, ceil: 8, round: 8\nValue: -7.9 → int: -7, floor: -8, ceil: -7, round: -8\nValue: 3.5 → int: 3, floor: 3, ceil: 4, round: 4
Hints

Hint 1: int() truncates TOWARD ZERO — not toward negative infinity.

Hint 2: math.floor() always rounds DOWN (toward negative infinity).

Hint 3: For negative numbers, int() and math.floor() give DIFFERENT results.


Medium

#4Custom Class Casting ProtocolMedium
__int____float____bool__dunder-methodsprotocol

Implement the __int__, __float__, and __bool__ dunder methods on the Temperature class so that Python's built-in int(), float(), and bool() work correctly.

Rules:

  • int(t) — truncate toward zero (same as built-in int-from-float behavior)
  • float(t) — return the exact Celsius value
  • bool(t)False only if at or below absolute zero (-273.15)
Solution
class Temperature:
def __init__(self, celsius):
self.celsius = celsius

def __int__(self):
return int(self.celsius)

def __float__(self):
return float(self.celsius)

def __bool__(self):
return self.celsius > -273.15

def __repr__(self):
return f"Temperature({self.celsius}°C)"

t1 = Temperature(36.6)
print(int(t1)) # 36
print(float(t1)) # 36.6
print(bool(t1)) # True

t2 = Temperature(-300.0)
print(int(t2)) # -300
print(float(t2)) # -300.0
print(bool(t2)) # False

t3 = Temperature(0.0)
print(bool(t3)) # True

How the protocol works:

  • When you call int(obj), Python looks for obj.__int__(). If it exists, that return value is used.
  • Same for float(obj)__float__() and bool(obj)__bool__().
  • If __bool__ is not defined, Python falls back to __len__ (truthy if non-zero length). If neither exists, all instances are truthy.
  • __int__ must return an actual int, __float__ must return an actual float, __bool__ must return an actual bool. Returning the wrong type raises TypeError.
class Temperature:
    """Represents a temperature in Celsius.
    Supports int(), float(), and bool() conversions.
    
    - int() returns the rounded-down whole degrees
    - float() returns the exact Celsius value
    - bool() returns False if at or below absolute zero (-273.15°C)
    """

    def __init__(self, celsius):
        self.celsius = celsius

    def __int__(self):
        pass

    def __float__(self):
        pass

    def __bool__(self):
        pass

    def __repr__(self):
        return f"Temperature({self.celsius}°C)"

# Tests
t1 = Temperature(36.6)
print(int(t1))
print(float(t1))
print(bool(t1))

t2 = Temperature(-300.0)
print(int(t2))
print(float(t2))
print(bool(t2))

t3 = Temperature(0.0)
print(bool(t3))
Expected Output
36\n36.6\nTrue\n-300\n-300.0\nFalse\nTrue
Hints

Hint 1: import math and use math.floor() for __int__ — or simply use int() on self.celsius (which truncates toward zero).

Hint 2: __float__ should return the raw celsius value as a float.

Hint 3: __bool__ returns True if temperature is above absolute zero (-273.15°C).

#5Safe Type ConverterMedium
ValueErrorerror-handlingtype-castingtry-except

Write a safe_convert function that attempts type conversion and returns a default value if it fails. Handle both ValueError and TypeError.

Edge cases to handle:

  • int("3.14") raises ValueError (can't parse float string as int directly)
  • int(None) raises TypeError
  • str(None) returns the string "None" — but the user probably wants the default
Solution
def safe_convert(value, target_type, default=None):
if value is None:
return default
try:
return target_type(value)
except (ValueError, TypeError):
return default

print(safe_convert("42", int)) # 42
print(safe_convert("3.14", float)) # 3.14
print(safe_convert("hello", int, default=-1)) # -1
print(safe_convert("", float, default=0.0)) # 0.0
print(safe_convert(None, str, default="N/A")) # None (our default, not "None")
print(safe_convert("3.14", int, default=0)) # 0
print(safe_convert([1, 2], int, default=-1)) # -1

Why the None check matters: Without it, str(None) would return the string "None" instead of the default. This is a common bug in data pipelines — you expect None to mean "missing data" but str() happily converts it to a string.

Why int("3.14") fails: Python's int() can parse integer strings ("42") but not float strings ("3.14"). You would need int(float("3.14")) for a two-step conversion. Our function correctly returns the default instead of silently doing something surprising.

Production pattern: This function is the foundation of every data cleaning pipeline. Pandas' pd.to_numeric(errors='coerce') does essentially the same thing internally.

def safe_convert(value, target_type, default=None):
    """Attempt to convert value to target_type.
    Return default if conversion fails.
    
    Args:
        value: The value to convert
        target_type: The type to convert to (int, float, str, bool)
        default: Value to return on failure
    
    Returns:
        Converted value, or default if conversion fails
    """
    pass

# Tests
print(safe_convert("42", int))
print(safe_convert("3.14", float))
print(safe_convert("hello", int, default=-1))
print(safe_convert("", float, default=0.0))
print(safe_convert(None, str, default="N/A"))
print(safe_convert("3.14", int, default=0))
print(safe_convert([1, 2], int, default=-1))
Expected Output
42\n3.14\n-1\n0.0\nNone\n0\n-1
Hints

Hint 1: Use try/except to catch ValueError and TypeError — both can occur during conversion.

Hint 2: Note that int("3.14") raises ValueError — you cannot parse a float string directly to int.

Hint 3: str(None) returns "None" — is that what the user wants? Think about the edge case.

#6Implicit Coercion TrackerMedium
implicit-coercionmixed-arithmetictype-promotion

Predict the output and type for each expression. Python silently coerces types in mixed arithmetic — trace exactly what happens.

Python
# int + float → ?
result1 = 2 + 3.0
print(result1, type(result1))

# bool + float → ?
result2 = True + 2.0
print(result2, type(result2))

# int + complex → ?
result3 = 1 + (2+2j)
print(result3, type(result3))

# bool + bool → ?
result4 = True + True
print(result4, type(result4))

# bool * int → ?
result5 = True * 1
print(result5, type(result5))

# str * int → ?
result6 = "Foo" * 3
print(result6, type(result6))

# list * int → ?
result7 = [1, 2] * 3
print(result7, type(result7))
Solution
5.0 <class 'float'>
3.0 <class 'float'>
(3+2j) <class 'complex'>
2 <class 'int'>
1 <class 'int'>
FooFooFoo <class 'str'>
[1, 2, 1, 2, 1, 2] <class 'list'>

Python's numeric coercion hierarchy: bool → int → float → complex

When two different numeric types appear in an arithmetic operation, Python promotes the "narrower" type to the "wider" type:

  • 2 + 3.0: int 2 is promoted to float 2.0, result is 5.0 (float)
  • True + 2.0: bool True → int 1 → float 1.0, result is 3.0 (float)
  • 1 + (2+2j): int 1 → complex (1+0j), result is (3+2j) (complex)
  • True + True: both bools → both ints (1 + 1), result is 2 (int, not bool!)
  • True * 1: bool → int, result is 1 (int)

Non-numeric coercion:

  • "Foo" * 3: string repetition — not arithmetic coercion. Returns a new string.
  • [1, 2] * 3: list repetition — same idea.

Key insight: bool is a subclass of int in Python. True == 1 and False == 0 in all arithmetic contexts. This is by design (PEP 285), not a quirk.

Expected Output
5.0 <class 'float'>\n3.0 <class 'float'>\n(3+2j) <class 'complex'>\n2 <class 'int'>\n1 <class 'int'>\nFooFooFoo <class 'str'>\n[1, 2, 1, 2, 1, 2] <class 'list'>
Hints

Hint 1: Python promotes int → float when mixed in arithmetic.

Hint 2: float → complex when mixed with complex numbers.

Hint 3: bool is a subclass of int: True is 1, False is 0 in arithmetic.

#7Type Conversion Chain TrackerMedium
type-castingchaindata-loss

Build a conversion_chain function that applies a sequence of type conversions and records each step — showing exactly how data transforms (and potentially loses information) through a chain of casts.

# Watch data loss happen step by step:
conversion_chain(3.7, int, str, float)
# 3.7 (float) → 3 (int) → '3' (str) → 3.0 (float)
# Started at 3.7, ended at 3.0 — the .7 is gone forever
Solution
def conversion_chain(value, *types):
chain = [(value, type(value).__name__)]
for t in types:
value = t(value)
chain.append((value, type(value).__name__))
return chain

chain1 = conversion_chain(3.7, int, str, float)
for val, tname in chain1:
print(f" {val!r:>10} ({tname})")

print()

chain2 = conversion_chain(True, int, float, str, bool)
for val, tname in chain2:
print(f" {val!r:>10} ({tname})")

print()

chain3 = conversion_chain(0, bool, int, str, list)
for val, tname in chain3:
print(f" {val!r:>10} ({tname})")

Data loss in chain 1: 3.7 → 3 → '3' → 3.0. The fractional part .7 is permanently lost at the int() step. Converting back to float gives 3.0, not 3.7. Type conversion chains are not reversible.

Surprising chain 2: True → 1 → 1.0 → '1.0' → True. The value starts as True, becomes 1, then 1.0, then the string '1.0'. Converting '1.0' back to bool gives True because it is a non-empty string — not because of the original boolean value. If it had been False → 0 → 0.0 → '0.0' → True — the final bool would be True (non-empty string), the opposite of where it started.

Chain 3: 0 → False → 0 → '0' → ['0']. list('0') creates a list from the iterable string, giving ['0'] — a list with one character element.

def conversion_chain(value, *types):
    """Apply a chain of type conversions, tracking each step.
    
    Args:
        value: Starting value
        *types: Sequence of types to convert through
    
    Returns:
        List of (value, type_name) tuples for each step
    
    Example:
        conversion_chain(3.7, int, str, float)
        → [(3.7, 'float'), (3, 'int'), ('3', 'str'), (3.0, 'float')]
    """
    pass

# Tests
chain1 = conversion_chain(3.7, int, str, float)
for val, tname in chain1:
    print(f"  {val!r:>10} ({tname})")

print()

chain2 = conversion_chain(True, int, float, str, bool)
for val, tname in chain2:
    print(f"  {val!r:>10} ({tname})")

print()

chain3 = conversion_chain(0, bool, int, str, list)
for val, tname in chain3:
    print(f"  {val!r:>10} ({tname})")
Expected Output
         3.7 (float)\n           3 (int)\n         '3' (str)\n         3.0 (float)\n\n        True (bool)\n           1 (int)\n         1.0 (float)\n       '1.0' (str)\n        True (bool)\n\n           0 (int)\n       False (bool)\n           0 (int)\n         '0' (str)\n       ['0'] (list)
Hints

Hint 1: Start with the initial value and build up the chain list step by step.

Hint 2: Use type(value).__name__ to get a clean type name string.

Hint 3: Apply each conversion in order: value = next_type(value).


Hard

#8Smart Numeric Type PickerHard
type-inferencenumericparsingdesign

Build a smart_numeric function that parses a string into the narrowest appropriate numeric type. This is the kind of function you would find inside a CSV parser or configuration loader.

Requirements:

  • "true" / "false" (case-insensitive) → bool
  • Whole numbers → int (including hex like "0xFF")
  • Decimal or scientific notation → float
  • Complex numbers → complex
  • Anything else → return original string unchanged
Solution
def smart_numeric(value):
stripped = value.strip()
if not stripped:
return value

# Check bool first (case-insensitive)
if stripped.lower() == "true":
return True
if stripped.lower() == "false":
return False

# Try int (including hex, octal, binary)
try:
# Handle 0x, 0o, 0b prefixes
if stripped.lower().startswith(("0x", "0o", "0b")):
return int(stripped, 0)
result = int(stripped)
return result
except ValueError:
pass

# Try float (handles scientific notation like 1e10)
try:
return float(stripped)
except ValueError:
pass

# Try complex (handles '3+2j')
try:
return complex(stripped)
except ValueError:
pass

# Nothing worked — return original string
return value

test_cases = [
"42",
" -17 ",
"3.14",
"true",
"FALSE",
"1e10",
"3+2j",
"0",
"0.0",
"hello",
" ",
"0xFF",
]

for tc in test_cases:
result = smart_numeric(tc)
print(f" {tc!r:>12}{result!r:>20} ({type(result).__name__})")

Design decisions:

  1. Bool before intTrue and False are technically int subclasses (int("true") raises ValueError anyway, but this makes intent clear).
  2. Int before float"42" should be int, not float. If int() succeeds, we use it. Scientific notation like "1e10" fails int() and falls through to float().
  3. Hex support via int(s, 0) — passing base 0 tells Python to detect the base from the prefix (0x = hex, 0o = octal, 0b = binary).
  4. Whitespace preserved on failure — if nothing parses, return the original value with its whitespace intact.

Real-world usage: Libraries like pandas, pyyaml, and configparser all implement variants of this logic internally. YAML's type resolution is almost exactly this hierarchy.

def smart_numeric(value):
    """Convert a string to the most appropriate numeric type.
    
    Priority (use the narrowest type that fits):
    1. bool — if value is 'true'/'false' (case-insensitive)
    2. int — if value represents a whole number (no decimal point)
    3. float — if value represents a decimal number
    4. complex — if value represents a complex number (e.g., '3+2j')
    5. Return the original string if none apply
    
    Handle leading/trailing whitespace. Handle negative numbers.
    Handle scientific notation (e.g., '1e10' → float).
    """
    pass

# Tests
test_cases = [
    "42",
    "  -17  ",
    "3.14",
    "true",
    "FALSE",
    "1e10",
    "3+2j",
    "0",
    "0.0",
    "hello",
    "  ",
    "0xFF",
]

for tc in test_cases:
    result = smart_numeric(tc)
    print(f"  {tc!r:>12} → {result!r:>20} ({type(result).__name__})")
Expected Output
  '42' →                   42 (int)
  '  -17  ' →                  -17 (int)
  '3.14' →                 3.14 (float)
  'true' →                 True (bool)
  'FALSE' →                False (bool)
  '1e10' →          10000000000.0 (float)
  '3+2j' →              (3+2j) (complex)
  '0' →                    0 (int)
  '0.0' →                  0.0 (float)
  'hello' →              'hello' (str)
  '  ' →                  '  ' (str)
  '0xFF' →                  255 (int)
Hints

Hint 1: Strip whitespace first. Check for bool strings before numeric parsing.

Hint 2: Try int() first (including base-16 for 0x prefixes), then float(), then complex().

Hint 3: Use try/except chains — attempt the narrowest type first and fall through on ValueError.

Hint 4: Scientific notation like "1e10" should be parsed as float, not int.

#9Safe Cast with Fallback ChainHard
error-handlingfallbacktype-castingdesign-pattern

Build safe_cast — a production-grade type conversion function with a fallback chain. It tries each type in order and returns the first successful conversion.

This pattern appears in:

  • API request parsing (try int, then float, then keep as string)
  • Database migration scripts (convert old schema types to new ones)
  • Configuration file loaders
Solution
def safe_cast(value, *type_chain, default=None):
if value is None:
return default

# Pre-process: strip whitespace from strings
cleaned = value.strip() if isinstance(value, str) else value

# Handle bool specially — bool("anything non-empty") is always True
# so we need to check for actual boolean string values
for target_type in type_chain:
try:
if target_type is bool:
if isinstance(cleaned, str):
if cleaned.lower() in ("true", "1", "yes"):
return True
elif cleaned.lower() in ("false", "0", "no"):
return False
else:
continue
else:
return bool(cleaned)
return target_type(cleaned)
except (ValueError, TypeError):
continue

return default

print(safe_cast("42", int, float)) # 42
print(safe_cast("3.14", int, float)) # 3.14
print(safe_cast("hello", int, float, default="N/A")) # N/A
print(safe_cast("true", bool, int, float)) # True
print(safe_cast("3+2j", int, float, complex)) # (3+2j)
print(safe_cast("", int, float, default=0)) # 0
print(safe_cast(" 99 ", int)) # 99
print(safe_cast(None, int, str, default="missing")) # missing

Critical design decision — why bool needs special handling:

bool("false") returns True in Python because "false" is a non-empty string. If we naively called bool() on a string, every non-empty string would convert to True. Instead, we explicitly check for known boolean string representations.

Why strip whitespace: User input almost always has trailing whitespace. int(" 99 ") works in Python (it strips internally), but this makes the intent explicit and handles types that do not auto-strip.

The fallback chain pattern: This is essentially a simplified version of the "Chain of Responsibility" design pattern. Each type converter is a handler — if it cannot process the value, it passes to the next one.

def safe_cast(value, *type_chain, default=None):
    """Try each type in the chain until one succeeds.
    
    Unlike simple try/except, this tries MULTIPLE types in order
    and returns the first successful conversion.
    
    Args:
        value: The value to convert
        *type_chain: Types to try, in order of preference
        default: Value to return if ALL conversions fail
    
    Returns:
        First successful conversion result, or default
    
    Example:
        safe_cast("3.14", int, float)  → 3.14  (int fails, float succeeds)
        safe_cast("hello", int, float) → None  (both fail)
    """
    pass

# Tests
print(safe_cast("42", int, float))
print(safe_cast("3.14", int, float))
print(safe_cast("hello", int, float, default="N/A"))
print(safe_cast("true", bool, int, float))
print(safe_cast("3+2j", int, float, complex))
print(safe_cast("", int, float, default=0))
print(safe_cast("  99  ", int))
print(safe_cast(None, int, str, default="missing"))
Expected Output
42\n3.14\nN/A\nTrue\n(3+2j)\n0\n99\nmissing
Hints

Hint 1: Loop through type_chain, try each conversion in a try/except block.

Hint 2: Return immediately on the first success — do not try remaining types.

Hint 3: Handle None as a special case — most types cannot convert None.

Hint 4: Consider stripping whitespace from strings before conversion.

#10Mixed-Type List NormalizerHard
type-normalizationdata-cleaningschemainference

Build a data normalizer that takes a messy, mixed-type list and converts it to a clean, uniform type. This is a real-world data engineering task — every pandas DataFrame column does this internally.

Auto-inference logic: Try the narrowest type first. If every element can be that type, use it. Otherwise, widen.

Forced mode: When target_type is given, convert what you can and mark failures as None.

Solution
def normalize_list(data, target_type=None):
def try_convert(value, t):
if value is None:
return False, None
try:
if isinstance(value, str):
value = value.strip()
return True, t(value)
except (ValueError, TypeError):
return False, None

def try_all_as(data, t):
results = []
success = 0
failed = 0
for item in data:
ok, val = try_convert(item, t)
if ok:
results.append(val)
success += 1
else:
results.append(None)
failed += 1
return results, success, failed

if target_type is not None:
results, success, failed = try_all_as(data, target_type)
return results, target_type.__name__, {"success": success, "failed": failed}

# Auto-infer: try int → float → str
for t in (int, float, str):
results, success, failed = try_all_as(data, t)
if failed == 0:
return results, t.__name__, {"success": success, "failed": 0}

# Nothing worked universally — return originals
return list(data), "mixed", {"success": 0, "failed": len(data)}

# Test 1: Auto-infer int
result, tname, stats = normalize_list([1, "2", 3.0, True, " 4 "])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 2: Auto-infer float (some values aren't clean ints)
result, tname, stats = normalize_list([1, "2.5", 3, "4.0"])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 3: Forced type with failures
result, tname, stats = normalize_list(["10", "hello", "30", None, "50"], target_type=int)
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 4: Mixed types that can only be strings
result, tname, stats = normalize_list([1, "hello", 3.14, True])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")

How auto-inference works:

  1. Try int first: [1, "2", 3.0, True, " 4 "]int(1)=1, int("2")=2, int(3.0)=3, int(True)=1, int(" 4 ")=4. All succeed → use int.
  2. Try int on test 2: int("2.5") fails → fall through to float. float(1)=1.0, float("2.5")=2.5, etc. All succeed → use float.
  3. Try int on test 4: int("hello") fails. Try float: float("hello") fails. Try str: str(1)="1", str("hello")="hello", etc. All succeed → use str.

Production parallel: This is exactly what pandas.to_numeric(errors='coerce') does, and what pd.read_csv() does when inferring column dtypes. The "try narrow, widen on failure" strategy minimizes information loss while ensuring type consistency.

Edge case — True as int: int(True) returns 1, which is correct for data normalization. In a statistics pipeline, you want boolean flags as 0/1 integers. This is intentional behavior, not a bug.

def normalize_list(data, target_type=None):
    """Normalize a mixed-type list to a consistent type.
    
    If target_type is given, convert all elements to that type.
    If target_type is None, infer the best common type:
        1. If ALL values can be int → int
        2. Else if ALL can be float → float
        3. Else if ALL can be str → str
        4. Otherwise keep original types
    
    Elements that fail conversion are replaced with None.
    
    Args:
        data: list of mixed-type values
        target_type: optional target type to force
    
    Returns:
        tuple of (normalized_list, type_name, conversion_stats)
        where conversion_stats = {'success': N, 'failed': N}
    """
    pass

# Test 1: Auto-infer int
result, tname, stats = normalize_list([1, "2", 3.0, True, "  4  "])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 2: Auto-infer float (some values aren't clean ints)
result, tname, stats = normalize_list([1, "2.5", 3, "4.0"])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 3: Forced type with failures
result, tname, stats = normalize_list(["10", "hello", "30", None, "50"], target_type=int)
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
print()

# Test 4: Mixed types that can only be strings
result, tname, stats = normalize_list([1, "hello", 3.14, True])
print(f"Result: {result}")
print(f"Type: {tname}, Stats: {stats}")
Expected Output
Result: [1, 2, 3, 1, 4]\nType: int, Stats: {'success': 5, 'failed': 0}\n\nResult: [1.0, 2.5, 3.0, 4.0]\nType: float, Stats: {'success': 4, 'failed': 0}\n\nResult: [10, None, 30, None, 50]\nType: int, Stats: {'success': 3, 'failed': 2}\n\nResult: ['1', 'hello', '3.14', 'True']\nType: str, Stats: {'success': 4, 'failed': 0}
Hints

Hint 1: For auto-inference, try converting ALL elements to int first. If any fail, try float. If any fail, try str.

Hint 2: Use a helper function that attempts conversion and returns (success, value).

Hint 3: True/False as int gives 1/0 — this is correct behavior for data normalization.

Hint 4: None values should be counted as failed conversions when a target type is specified.

© 2026 EngineersOfAI. All rights reserved.