Type conversion is where good Python programs go to die. Not because the conversions are hard, but because the obvious ones have sharp edges: int() truncates towards zero instead of flooring, int('3.9') raises an error while int(3.9) quietly discards the decimal, bool('False') is True, and float('nan') is a perfectly valid float that is not equal to itself. This lesson gives you the rules and a conversion helper you can trust.
What does Python convert automatically?
Very little, and that is deliberate. Python converts numbers between themselves in one specific situation: when an operation mixes an integer and a float, the integer is promoted to a float first, because there is no other way to hold the result. Nothing else is converted implicitly. In particular, a string is never turned into a number for you, because Python cannot know whether '3' meant the number 3 or the price of three tickets.
print('1 + 2.0 ->', 1 + 2.0, '| type:', type(1 + 2.0).__name__)
print('3 / 1 ->', 3 / 1, '| type:', type(3 / 1).__name__)
print('3 + True ->', 3 + True)
print('print() casts ->', 123, 1.5, True, None)
try:
print('5' + 5)
except TypeError as error:
print('"5" + 5 ->', error)
The one automatic conversion that catches people out is the promotion inside mixed arithmetic. 3 / 1 is 3.0, not 3, because / always produces a float. If you divide counts and expect whole numbers, use // instead.
What are the traps in int() and float()?
There are four, and each one produces a wrong answer rather than an error.
int()truncates towards zero, soint(-3.9)is-3. Floor division gives-4. For negative numbers the two rules disagree.int()on a string must contain a whole number.int('3.9')raisesValueError, so you cannot "just convert a decimal string".float()accepts'nan'and'inf', because those are legitimate float values written as words. A single stray letter in your data becomes a number that behaves strangely forever.bool()treats every non-empty string asTrue, so the text'False'converts toTrue.
try:
print(int('3.9'))
except ValueError as error:
print("int('3.9') ->", error)
print('int(3.9) ->', int(3.9))
print('int(-3.9) ->', int(-3.9), '| floor would be', -3.9 // 1)
print('int(" 42 ") ->', int(' 42 '))
print("float('nan') ->", float('nan'))
print("float('inf') ->", float('inf'))
print("bool('False') ->", bool('False'))
print('round(3.14159, 2)->', round(3.14159, 2), '| type:', type(round(3.14159, 2)).__name__)
Note the last line. round() with no digits argument returns an int; round() with a digits argument returns a float. That is not a problem in itself, but it means the type of a rounded value depends on how you wrote the call, which is worth knowing when the result feeds into formatting.
How do you convert between text, numbers and bytes?
Numbers convert to text with str(), and text converts back with int() or float(). int() accepts a second argument that says what base the text is written in, which is how you read a hex or binary identifier.
print("int('0x1f', 16) ->", int('0x1f', 16))
print("int('1010', 2) ->", int('1010', 2))
print("int('ff', 16) ->", int('ff', 16))
print("int(b'42', 10) ->", int(b'42', 10))
print("int('42') ->", int('42'))
text = 'héllo'
encoded = text.encode('utf-8')
print('encoded bytes ->', encoded)
print('decoded back ->', encoded.decode('utf-8'))
print('length in chars ->', len(text))
print('length in bytes ->', len(encoded))
That last comparison is the one to remember: a Python string counts characters, while bytes count octets. An accented letter or an emoji takes several bytes but is one character, so the two lengths differ. The same idea explains why len() of a string containing a family emoji reports one character, not the seven bytes it occupies.
How do you convert safely?
The pattern that survives contact with real data is simple: strip the text, try the conversion, and return a sensible fallback when it fails. Keep that logic in one helper so the rest of your program never has to think about it.
def to_int(text):
"""Return an int, or None when the text is not a whole number."""
cleaned = text.strip()
try:
return int(cleaned)
except ValueError:
return None
samples = ['42', ' 7 ', '3.5', '', 'abc', '-9']
for sample in samples:
print(f'{sample!r:<8} -> {to_int(sample)!r}')
Notice what the helper does not do: it does not guess. '3.5' is rejected rather than truncated, because a value like that usually means the caller passed the wrong thing and would rather know. If your data legitimately contains decimals, write a separate to_float() that accepts them, so the intent stays visible in the code.
Two more conversions deserve special care. For money, build a Decimal from a string, because Decimal(19.9) inherits the float error you were trying to escape. And for fractions, Fraction accepts a string directly.
import json
from decimal import Decimal
from fractions import Fraction
price = 19.9
print('float price ->', price)
print('Decimal(price) ->', Decimal(price))
print("Decimal(str) ->", Decimal(str(price)))
print('Fraction string ->', Fraction('1/3') + Fraction('1/6'))
payload = json.loads('{"id": 7, "price": "19.90", "tags": ["a", "b"]}')
for key, value in payload.items():
print(f'{key:<6} {type(value).__name__:<6} {value!r}')
Look at the JSON output: the parser gives you a real int for the number, a str for the quoted price, and a list for the array. Structured formats carry their types with them, which is why you should prefer them to hand-rolled text splitting. And if you ever feel the urge to turn a string back into a Python object with eval(), reach for ast.literal_eval() instead, which can only build literals and refuses anything that could execute code.
There is a family of conversions you will meet once you start moving data between structures. list() turns any iterable into a list, tuple() freezes one, and set() removes duplicates. Converting between them is cheap and often useful, particularly when a function returns a list but a caller wants to work with a set. The same idea applies to bytes: list(b'abc') gives you the three character codes, and bytes([104, 105]) turns them back into b'hi'.
letters = list('abc')
numbers = list(b'abc')
print('list of chars ->', letters, type(letters).__name__)
print('list of bytes ->', numbers)
print('back to bytes ->', bytes(numbers))
print('unique letters ->', sorted(set('banana')))
print('frozen tuple ->', tuple(letters))
One last trap to keep in mind: converting a number to text and back is lossy for floats. str(0.1) gives the shortest string that round-trips, which is usually what you want, but str(float('nan')) gives 'nan' and int() refuses it entirely. When a value has to survive a trip through text, check it with math.isfinite() first.
Your turn: write a converter that never crashes
- Write
to_float(text)using the same shape as the helper above, but reject the strings'nan'and'inf'explicitly, because they pass a naive conversion and then poison every comparison downstream. - Write
to_bool(text)that accepts'yes','no','true'and'false'in any case, and returnsNonefor anything else. Do not usebool()directly. - Add
to_int_base(text, base)that converts a string written in a given base, and use it to read the identifier'0x1F'as 31. - Now build
to_price(text)that returns aDecimal, and check thatto_price('0.10') + to_price('0.20')is exactly0.30while0.10 + 0.20is not. - Finally, write a small script that reads a list of raw strings, converts each one with your helpers, and prints a table showing the original text, the converted value, and the type name. Run it on a list that includes one good value and one deliberately broken one.
Casting cheat sheet
- Python promotes an int to a float in mixed arithmetic, and does nothing else implicitly.
int()truncates towards zero, rejects decimal strings, and accepts a base argument for hex or binary text.float()accepts'nan'and'inf', so validate before you trust a converted number.bool('False')isTrue; write your own text-to-bool helper if the text comes from a user.- Build
Decimalfrom a string, keep fractions exact withFraction, and never useeval()to revive text.