Python Type Casting
Convert values safely and predictably
A complete, runnable reference for converting between Python's built-in types. Every code block includes a numbered line-by-line breakdown, so you understand exactly what happens at each step — plus practice exercises, pitfalls to avoid, and a full quick-reference table.
# A first look at Python type casting user_input = "42" age = int(user_input) price = "19.99" cost = float(price) total = 3 * cost label = "Total: $" + str(round(total, 2)) is_adult = bool(age >= 18) items = list("abc") print(f"age={age} ({type(age).__name__})") print(f"cost={cost} ({type(cost).__name__})") print(label) print(f"is_adult={is_adult} items={items}")
📚 What This Lesson Covers
Twelve sections, each with runnable code and a numbered explanation of every line
🧭 Why Type Casting Matters
Real programs constantly convert between types — from user input, files, APIs, and databases
Python is a strongly typed language. It will not silently coerce a string "42" into an integer when you try to do arithmetic on it. This is a feature — it prevents whole classes of bugs — but it also means that the moment your data crosses a boundary (from a text file, a web form, a JSON response), you need to convert it explicitly. Type casting is how you do that safely.
Three places you cast constantly
User input: input() always returns a string — you must cast before doing math.
Files & APIs: JSON and CSV parsers give you strings and numbers mixed — cast as needed.
Formatting output: str() and f-strings turn any value into text for display.
⚖️ Implicit vs Explicit Conversion
Python automatically promotes some types; you must convert others yourself
🤖 Implicit Conversion — Python Does It
When two numeric types meet in an operation, Python promotes the narrower one to the wider one. int + float → float. There is no data loss, so Python does it silently.
a = 5 b = 2.5 result = a + b print(f"result = {result}") print(f"type = {type(result).__name__}") whole = 5 + 3 print(f"whole = {whole} type = {type(whole).__name__}") flag = True + 1 print(f"flag = {flag} type = {type(flag).__name__}")
- 1–2Two values:
ais anint(5),bis afloat(2.5). - 3
a + b— Python detects the type mismatch and implicitly promotesato5.0before the addition. The result is afloat. - 5–6The first pair of prints confirms
resultis7.5and its type is"float". - 8–9
5 + 3— both operands areint, so no promotion happens. Result is theint8. - 11–12
True + 1— becauseboolis a subclass ofint, Python treatsTrueas1. Result is theint2.
str to a number — that's why "5" + 3 raises TypeError.🎯 Explicit Conversion — You Do It
When implicit rules don't apply (or you want a specific type), you call a conversion function explicitly. This is casting — and it puts you in control of what happens on failure.
text = "42" number = int(text) print(f"text={text!r} type={type(text).__name__}") print(f"number={number!r} type={type(number).__name__}") back_to_text = str(number) print(f"back_to_text={back_to_text!r}") try: bad = int("hello") except ValueError as e: print(f"Failed: {e}")
- 1
text = "42"— a string. Arithmetic on it directly would fail. - 2
number = int(text)— explicit cast. Python parses the string as an integer literal and returns theint42. - 4–5Confirms the original
textis still a string andnumberis now anint. - 7
str(number)— explicit cast back to string, producing"42". - 10–12Attempting to cast
"hello"raisesValueError. Thetry/exceptcatches it so the program keeps running.
ValueError or TypeError). Wrap user-supplied input in try/except — never assume it will succeed.🔢 Converting to int — int()
Truncates floats, parses strings, honours base prefixes
📥 int() — Float, String, and Base Conversions
int() behaves differently depending on its input. Floats are truncated toward zero (not rounded). Strings are parsed as integer literals. An optional second argument sets the base for string parsing.
print(int(3.99)) print(int(-3.99)) print(int("42")) print(int(" -17 ")) print(int("0b101010", base=0)) print(int("0x2A", base=0)) print(int("101010", base=2)) print(int("52", base=8)) print(int("2A", base=16))
- 1
int(3.99)→3. The fractional part is truncated, not rounded.int()never rounds. - 2
int(-3.99)→-3. Truncation always moves toward zero, so negative floats round up numerically. - 3
int("42")→42. Parses a decimal integer literal. - 4
int(" -17 ")→-17. Leading and trailing whitespace is stripped before parsing. - 5–6
base=0tells Python to infer the base from the prefix:0bmeans binary,0xmeans hex. Both return42. - 7–9Explicit base arguments parse without a prefix.
int("101010", 2)= 42,int("52", 8)= 42,int("2A", 16)= 42.
round() first: int(round(3.99)) gives 4. But note that round() uses banker's rounding — round(2.5) is 2, not 3.⚠️ What Raises an Error
Some strings look numeric but aren't valid integer literals. Knowing which cases fail is half the battle.
for s in ["3.14", "abc", "", "1,000"]: try: print(int(s)) except ValueError as e: print(f"int({s!r}) failed: {e}")
- 1Iterates four problematic strings.
- 2–3Attempts the cast. If it succeeds, prints the result.
- 4–5If it fails, catches
ValueErrorand prints the error message. The!rin the f-string shows the string's repr, so""appears as''(distinguishing empty from space).
"3.14" to int, chain casts: int(float("3.14")) → 3. For thousands separators, strip commas first: int("1,000".replace(",", "")).🔣 Converting to float — float()
Handles decimal strings, scientific notation, and special values
📥 float() — Integers, Strings, and Scientific Notation
float() converts any numeric type or numeric string to a float. It also recognises inf, -inf, and nan (case-insensitive).
print(float(7)) print(float("3.14")) print(float("-0.5")) print(float("1e3")) print(float("1.5e-2")) print(float("inf")) print(float("nan")) import math print(math.isnan(float("nan"))) print(math.isinf(float("inf")))
- 1
float(7)→7.0. An integer becomes a float with no data loss. - 2
float("3.14")→3.14. Parses the decimal string. - 3
float("-0.5")→-0.5. Negative decimals are fine. - 4
float("1e3")→1000.0. Scientific notation is supported. - 5
float("1.5e-2")→0.015. Small numbers via negative exponent. - 6–7
infandnanare recognised as special float values — not errors. - 9–10Use
math.isnan()andmath.isinf()to test for them. Never compare with==—nan != nanby definition.
nan with ==. Because NaN is "not a number", nan == nan is always False. Use math.isnan(x).📝 Converting to str — str()
Every Python value has a string representation — but str() and repr() differ
📤 str() vs repr()
str() gives the human-readable form; repr() gives the developer-readable form — usually a valid Python expression that recreates the value. Use str() for display, repr() for debugging.
print(str(42)) print(str(3.14)) print(str(True)) print(str(None)) print(str([1, 2, 3])) print(str({"a": 1})) print(repr("hello")) print(str("hello")) print(repr("a\nb")) print(str("a\nb"))
- 1–6Each
str()call returns the human-readable form. Lists and dicts keep their Python syntax — useful for logging but not for building other strings. - 8
repr("hello")→'hello'— with quotes. This is exactly how you'd type the string in source code. - 9
str("hello")→hello— no quotes. Just the content. - 10
repr("a\nb")→'a\nb'— the escape sequence is shown literally, so you can see the newline exists. - 11
str("a\nb")→ two actual lines. The newline is rendered, not escaped.
+, use str() for numbers, but prefer f-strings — they handle conversion automatically and are more readable: f"Score: {score}".🔗 Concatenation and f-strings
The most common conversion in everyday code is turning non-string values into text for output. F-strings do this invisibly and are almost always the right choice.
count = 5 price = 9.99 name = "Ana" old_style = "Name: " + name + ", total: " + str(count * price) f_style = f"Name: {name}, total: {count * price}" print(old_style) print(f_style) print(f_style == old_style)
- 1–3Three variables of three different types.
- 5Manual concatenation:
"Name: " + name + ", total: " + str(...). Note the explicitstr()— without it, Python raisesTypeErrorbecause it refuses to concatenate a number onto a string. - 6f-string version: any expression inside
{}is converted automatically withstr()internally. - 8–10Both produce the same string. The final comparison prints
True, confirming the two approaches are equivalent — but the f-string is far easier to read.
✅ Converting to bool — bool()
Truthiness rules apply — every value becomes either True or False
🧠 bool() and Truthiness
There's no way to go from "True" (the string) to True (the boolean) via bool() — because any non-empty string is truthy. Parsing user input like "false" requires explicit comparison.
print(bool(0)) print(bool(-1)) print(bool(0.0)) print(bool("")) print(bool("0")) print(bool("False")) print(bool([])) print(bool([0])) print(bool(None)) user_input = "false" parsed = user_input.strip().lower() == "true" print(f"parsed = {parsed}")
- 1
bool(0)→False. Zero is falsy for any numeric type. - 2
bool(-1)→True. Any non-zero number is truthy. - 3–4
bool(0.0)andbool("")→False. Zero float and empty string are falsy. - 5–6
bool("0")andbool("False")→ bothTrue. Any non-empty string is truthy, regardless of content. - 7–8
bool([])→False(empty list).bool([0])→True(list contains one item, even if it's zero). - 9
bool(None)→False.Noneis always falsy. - 11–12To parse the text
"false"into the booleanFalse, compare explicitly against"true".strip()removes whitespace,lower()makes the comparison case-insensitive.
{"true": True, "false": False, "1": True, "0": False}. Explicit lookup makes intent clear and avoids accidental truthiness bugs.📦 Collection Conversions — list, tuple, set, dict
Convert between sequence types freely — but understand what is preserved and what is lost
🔁 list(), tuple(), set(), dict()
Any iterable can be converted to any collection type. The conversion processes each element once — order, uniqueness, and mutability are determined by the target type.
text = "hello" numbers = [1, 2, 2, 3, 3, 3] print(list(text)) print(tuple(text)) print(set(text)) print(list(numbers)) print(set(numbers)) print(list(set(numbers))) pairs = [("a", 1), ("b", 2)] print(dict(pairs)) print(dict(name="Ana", age=25))
- 1–2A string and a list with duplicates — both are iterables.
- 4
list("hello")→['h', 'e', 'l', 'l', 'o']. Each character becomes an element. - 5
tuple(text)→('h', 'e', 'l', 'l', 'o'). Same content, immutable container. - 6
set(text)→{'h', 'e', 'l', 'o'}. Duplicates are removed and order is lost. - 8
list(numbers)keeps duplicates intact. - 9
set(numbers)reduces to{1, 2, 3}— the classic deduplication trick. - 10
list(set(numbers))converts back to a list — but order is not guaranteed. - 12–13
dict(pairs)turns a list of 2-tuples into a dict:{'a': 1, 'b': 2}. - 14
dict(name="Ana", age=25)builds a dict from keyword arguments — a handy shortcut.
list(dict.fromkeys(seq)). That relies on dictionaries maintaining insertion order since Python 3.7 — unlike sets.⚠️ Common Pitfalls to Avoid
The four mistakes that cause most type-casting bugs in real programs
❌ Pitfall 1 — Using input() Without Casting
input() always returns a string, even if the user types a number. Arithmetic on it fails with TypeError.
user_input = "42" result = user_input + 1
- 1Simulates what
input()returns — a string, even though it looks like a number. - 3
user_input + 1raisesTypeError: can only concatenate str (not "int") to str. This is one of the most common beginner mistakes.
result = int(user_input) + 1. Always cast input() before arithmetic.❌ Pitfall 2 — Using == Instead of Explicit Comparison for bool
Because bool("False") is True, casting strings to booleans rarely does what you mean.
value = "False" print(bool(value)) parsed = value.strip().lower() == "true" print(parsed)
- 1The string
"False"— five characters, non-empty. - 3
bool("False")→True. Surprising but correct — any non-empty string is truthy. - 5Explicit comparison against the literal
"true"gives the expected result:False.
❌ Pitfall 3 — Assuming int() Rounds
int() truncates toward zero. It never rounds to nearest. If you need rounding, call round() first.
print(int(2.9)) print(int(-2.9)) print(round(2.9)) print(round(-2.9))
- 1
int(2.9)→2. Truncation, not rounding. - 2
int(-2.9)→-2. Toward zero, not toward negative infinity. - 3–4
round(2.9)→3,round(-2.9)→-3. Rounding to nearest.
int(round(2.9)) gives 3, but round(int(2.9)) gives 2.❌ Pitfall 4 — Ignoring Exceptions on Bad Input
Real user input is messy. Any cast that could fail should be guarded. Otherwise one bad character crashes the whole program.
def parse_int(text, default=0): try: return int(text) except (ValueError, TypeError): return default print(parse_int("42")) print(parse_int("abc")) print(parse_int(None, default=-1))
- 1Defines a function that takes a value and a fallback default.
- 2–3Tries the cast. On success, returns the integer.
- 4–5Catches two exceptions:
ValueErrorfor malformed strings,TypeErrorfor values that aren't strings or numbers at all (likeNone). Returns the default in either case. - 7Valid input: returns
42. - 8Invalid string: returns
0(the default). - 9Unsupported type: returns
-1(explicit default).
🛡️ Safe Conversion — Building Robust Helpers
A small set of well-designed helper functions eliminates most casting bugs
🧰 A Reusable Parsing Module
Once you find yourself writing try/except around int() three times in a project, it's time to extract helpers. Here's a minimal, production-shaped set.
def parse_int(value, default=0): try: return int(value) except (ValueError, TypeError): return default def parse_float(value, default=0.0): try: return float(value) except (ValueError, TypeError): return default def parse_bool(value, default=False): if isinstance(value, bool): return value if isinstance(value, str): v = value.strip().lower() if v in ("true", "1", "yes"): return True if v in ("false", "0", "no"): return False return default print(parse_int("42"), parse_int("bad", -1)) print(parse_float("3.14"), parse_float(None)) print(parse_bool("YES"), parse_bool("no"), parse_bool(True))
- 1–5
parse_int: tries the cast, returns the value on success. On failure (bad string or unsupported type), returnsdefault. Catching bothValueErrorandTypeErroris what makes it actually safe. - 7–11
parse_float: same pattern, default0.0. - 13–22
parse_bool: more nuanced —bool()alone is dangerous for strings. First handles actual booleans. For strings, normalises case and whitespace, then looks up in two explicit sets of recognised truthy and falsy tokens. Falls back todefaultfor unrecognised input. - 24–26Test calls that exercise the success path, the failure path, and the string-to-bool path for each helper.
🏋️ Practice — Check Your Understanding
Three exercises with solutions. Try each before expanding the answer.
Exercise 1 — Predict the Output
What does each line print? Write your answers before expanding.
print(int("3.14")) print(int(3.99)) print(int(float("3.14"))) print(bool("False")) print(str(None)) print(list("abc")) print(set([1, 1, 2, 2]))
Show solution
Line 1: ValueError — "3.14" is not a valid integer literal.
Line 2: 3 — truncation, not rounding.
Line 3: 3 — chained cast works because float("3.14") succeeds first.
Line 4: True — any non-empty string is truthy.
Line 5: "None" — string representation of the None object.
Line 6: ['a', 'b', 'c'] — a string iterates character-by-character.
Line 7: {1, 2} — sets remove duplicates.
Exercise 2 — Fix the Runtime Error
This program crashes on the second line. Explain why and fix it.
user_input = "25" next_age = user_input + 1 print(next_age)
Show solution
user_input is a string, so user_input + 1 raises TypeError. Python will not silently coerce. Fix: next_age = int(user_input) + 1. If the input might not be a valid integer, wrap it: next_age = int(user_input) + 1 inside a try/except — or use the parse_int helper from the Safe Conversion section.
Exercise 3 — Build a Temperature Converter
Write a function celsius_from_fahrenheit that accepts a value that might be a string like "98.6", a float, or an int — and returns the Celsius equivalent rounded to one decimal place. If the value can't be parsed, return None.
Show solution
def celsius_from_fahrenheit(value): try: f = float(value) except (ValueError, TypeError): return None return round((f - 32) * 5 / 9, 1) print(celsius_from_fahrenheit("98.6")) print(celsius_from_fahrenheit(212)) print(celsius_from_fahrenheit("abc"))
Key points: float() handles strings, ints, and floats uniformly, so one cast covers all three input types. The try/except catches both bad strings (ValueError) and unsupported types (TypeError). Returning None for bad input lets callers decide what to do next. Expected output: 37.0, 100.0, None.
❓ Frequently Asked Questions
Common questions about Python type casting, answered directly
Implicit conversion is done automatically by Python's interpreter — like when an int and a float are mixed in arithmetic and Python promotes the int to float. Explicit conversion (casting) requires you to call a conversion function like int(), float(), or str() to force a specific type. Implicit conversion never loses information; explicit casting can (int(3.99) truncates to 3).
int() of a string expects a valid integer literal, and "3.14" contains a decimal point, so parsing fails with ValueError. int() of a float truncates the decimal portion. To convert "3.14" to an integer, chain two calls: int(float("3.14")) returns 3.
Wrap the conversion in try/except and catch ValueError for malformed input and TypeError for unsupported types. For example: try: n = int(user_input) except ValueError: n = 0 (or prompt again). Never trust input() to produce a number — it always returns a string, and users can enter anything.
No. bool("False") returns True because any non-empty string is truthy. To parse the word "False" into the boolean False, compare explicitly: value = user_input.lower() == "true" — or use a mapping like {"true": True, "false": False}.
📋 Quick-Reference Table
Every conversion function and its common cases — bookmark this page for quick reference
| Call | Result | Notes |
|---|---|---|
| int(3.99) | 3 | Truncates toward zero — never rounds |
| int(-3.99) | -3 | Negative truncation also moves toward zero |
| int("42") | 42 | Parses a decimal integer literal |
| int(" -17 ") | -17 | Whitespace is stripped |
| int("3.14") | ValueError | Not a valid integer literal — chain with float() |
| int("0x2A", 0) | 42 | base=0 auto-detects prefix |
| int("2A", 16) | 42 | Explicit base for hex |
| float(7) | 7.0 | int → float, no data loss |
| float("3.14") | 3.14 | Parses decimal string |
| float("1e3") | 1000.0 | Scientific notation supported |
| float("nan") | nan | Test with math.isnan() — not == |
| str(42) | "42" | Human-readable form |
| repr("hello") | 'hello' | Developer-readable form with quotes |
| bool(0) | False | Zero is falsy |
| bool("0") | True | Non-empty string is truthy — surprise! |
| bool([]) | False | Empty containers are falsy |
| list("abc") | ['a', 'b', 'c'] | Iterates character-by-character |
| set([1, 2, 2, 3]) | {1, 2, 3} | Removes duplicates |
| tuple([1, 2, 3]) | (1, 2, 3) | Immutable copy |
| dict([("a", 1), ("b", 2)]) | {"a": 1, "b": 2} | Builds from key-value pairs |
Educational Use Disclaimer
This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.
📬 Python, weekly
Short lessons, real code, and ecosystem news — sent about once a week
🗣️ Learner Voices
No comments yet — your voice starts the conversation.