How to Fix ValueError: invalid literal for int() with base 10 in Python

3D illustration banner for fixing ValueError invalid literal for int base 10 in Python.

Built-in int() is far stricter with strings than it is with numbers or booleans. Pass the float 42.50 or boolean True into int() and Python happily returns 42 or 1. Pass the string "42.50", "True", an empty string "" from input(), or a scraped price containing an HTML non-breaking space ("1\xa0250") into int(), and your script immediately crashes with ValueError: invalid literal for int() with base 10.

When debugging int(), Python makes a strict distinction between two exception types:

  • TypeError: You passed the wrong type of object into int() (such as None, a list, or a dict).
  • ValueError: You passed an allowed type (str, bytes, or bytearray), but the value (characters) inside it is not a valid base-10 integer literal. In base 10, int() only accepts optional leading/trailing whitespace, an optional ASCII + or - sign immediately before the digits, and digits 0–9 (optionally separated by single underscores between digits like "1_000").

Reading the invalid literal for int() Traceback

Read the traceback from the bottom up. The exact string or bytes literal that broke int() is printed right after the colon at the very end of the ValueError line:

Traceback (most recent call last):
  File "/var/www/app/main.py", line 2, in <module>
    price = int(price_str)
ValueError: invalid literal for int() with base 10: '42.50'
  • If the end of the line reads : '' (two single quotes with nothing between them): You passed a completely empty string "" into int(), not a missing argument.
  • If the value starts with b'...' (: b'42.0'): You passed a bytes object containing non-digit ASCII characters or raw binary bytes.
  • If the string contains \xa0 (: '1\xa0250') or looks like a valid negative number (: '−42'): You scraped an HTML &nbsp; (non-breaking space) or a Unicode minus sign (\u2212) instead of a standard ASCII hyphen-minus (-).
  • Python 3.11+ Carets: Starting in Python 3.11+ (not run by me; noted from documentation), the traceback draws ^^^^^^^^^^^^^^ carets under the int(...) call on the failing line.

Tested Environment: Python 3.10.16, NumPy 1.26.3 & Pandas 2.0.0 (Linux) | Python 3.11+ carets, Polars, and type checker behaviors not run by me; noted from documentation.

Part of: The Ultimate Python Debugging Guide

⚡ The Quick Fix: Parse Float Strings, Fall Back, or Validate

Choose your fix based on whether the string contains decimals and whether the input is optional or required in your application:

1. Converting a Float String ("42.50" → 42): Convert to float First If the string represents a standard US/UK decimal number (or scientific notation like "1e5"), parse it with float() before passing it to int():

price_str = "42.50"

# Fails: int() rejects the '.' character inside a string
# price = int(price_str)

# Fix: Parse as float first, then truncate to int (or use round(float(price_str)))
price = int(float(price_str))
print(price)  # Prints: 42

2. Optional Values: Catch ValueError and Return a Sensible Default When reading optional user inputs, query parameters (request.args.get("page")), or messy CSV cells where blank or malformed strings should fall back to a default, wrap the conversion in try / except (ValueError, TypeError):

def parse_int_optional(val, default=0):
    try:
        return int(val)
    except (ValueError, TypeError):
        return default

print(parse_int_optional(""))      # Prints: 0
print(parse_int_optional("42"))    # Prints: 42

3. Required Values: Validate the Input and Raise a Meaningful Error When an integer is strictly required (such as an order quantity or database ID), do not silently substitute 0—that masks corrupted data and writes bad records downstream. Validate and raise a clear domain error that names the offending field and value:

def parse_int_required(val, field_name="value"):
    try:
        return int(val)
    except (ValueError, TypeError) as err:
        raise ValueError(f"Expected a valid integer for {field_name!r}, got {val!r}") from err

Inspect the Exact String and Hidden Unicode Characters

When int(raw_val) fails on a string that looks like a normal number in your browser or terminal (such as '−42' or '1 250'), print(raw_val) hides the bug.

Drop these two lines right above the crash using ascii(raw_val) to expose hidden escape codes like \u2212 (Unicode minus) and \xa0 (non-breaking space) alongside every non-digit character:

raw_val = "−42.50\n"

print(f"Value repr: {raw_val!r} | ASCII: {ascii(raw_val)} | Length: {len(raw_val)}")
print("Bad chars: ", [ch for ch in raw_val.strip() if not ch.isdecimal()])

Verified Output (Python 3.10.16):

Value repr: '−42.50\n' | ASCII: '\u221242.50\n' | Length: 7
Bad chars:  ['−', '.']

Seeing ASCII: '\u221242.50\n' immediately reveals that the leading dash is a Unicode minus sign (\u2212) rather than an ASCII hyphen (-), and that the string also contains a decimal point '.'.

Common Causes of ValueError: invalid literal for int() with base 10

Here are the eight most frequent ways non-integer strings or bytes reach int().

1. Passing a Float or Scientific Notation String ("42.5", "10.0", "1e5")

Database exports, JSON APIs, and Excel CSVs frequently serialize whole numbers with a trailing .0 (such as "10.0") or in scientific notation ("1e5"). Because . and e are not base-10 digits, int("10.0") fails even though the value is mathematically a whole number.

  • The Trigger (Verified 3.10.16):
views = int("10.0")
# ValueError: invalid literal for int() with base 10: '10.0'
  • The Fix: Use int(float("10.0")) for standard numbers.
    ⚠️ Precision Caveat for 64-Bit IDs: Standard IEEE 754 float only has 53 bits of precision (up to 9,007,199,254,740,992). If you pass a large 64-bit integer ID string with .0 through float(), it silently loses precision (int(float("9007199254740993.0")) becomes 9007199254740992!). For large IDs or financial data, use from decimal import Decimal and call int(Decimal(val)), which preserves exact precision.
  • How to tell it’s this one: The string in the traceback contains a decimal point . or e (like '42.50' or '1e5').

2. Empty Strings '', Internal Spaces ("1 250"), and the Non-Breaking Space (\xa0) Trap

While int(" 42\n") and int("\xa042\xa0") automatically strip leading and trailing whitespace, int() strictly rejects:

  1. Empty or whitespace-only strings ("" or " "): Common when a user presses Enter at input(), submits a blank web form field, or reads a trailing blank line in a file.
  2. Internal spaces ("1 250" or "- 42"): Spaces inside the number—such as thousand separators or a space after the negative sign—cause an immediate ValueError.
  3. The HTML Non-Breaking Space Trap: When scraping prices or tables from websites, HTML frequently uses &nbsp; (\xa0) as a thousands separator so the number doesn’t wrap across lines ("1\xa0250"). Calling val.replace(" ", "") will NOT remove \xa0 because \xa0 is a distinct character from a normal ASCII space (" ")!
  • The Trigger (Verified 3.10.16):
# Fails on empty string or internal ASCII space:
age = int("")          # ValueError: invalid literal for int() with base 10: ''
temp = int("- 42")     # ValueError: invalid literal for int() with base 10: '- 42'

# Fails on scraped HTML &nbsp; even after .replace(" ", "")!
scraped = "1\xa0250".replace(" ", "")
qty = int(scraped)     # ValueError: invalid literal for int() with base 10: '1\xa0250'
  • The Fix: To strip all internal ASCII and Unicode whitespace (including " ", \t, and \xa0) in one step, split on whitespace and rejoin with "".join(val.split()):
scraped = "1\xa0250"
clean_qty = int("".join(scraped.split()))
print(clean_qty)  # Prints: 1250
  • How to tell it’s this one: The traceback ends with : '', : '1 250', : '- 42', or : '1\xa0250'.

3. US vs. European Locale Formats ("1,250" vs. "1.250,50"), Underscore Rules, and Unicode Minus ("−42")

Numbers scraped from websites, PDFs, or spreadsheets carry locale, separator, and typography formatting that int() cannot parse:

  1. US/UK Format ("1,250.50"): Uses commas for thousands and . for decimals. Strip commas first: int(float(val.replace(",", ""))).
  2. European Format ("1.250,50" or "1.250"): In Germany, France, Spain, Italy, and many other countries, the separators are reversed—. separates thousands and , separates decimals.
    ⚠️ Silent Bug Warning: If your dataset uses European formatting where "1.250" means one thousand two hundred fifty, never blindly run int(float("1.250"))! Python will interpret . as a decimal point (1.25) and silently truncate 1,250 down to 1!
  3. PEP 515 Underscore Rules ("1_000" vs. "_1000"): While Python 3.6+ allows single underscores as visual separators in "1_000" (evaluating to 1000), underscores are only valid directly between digits. Leading underscores ("_1000" or "-_1000"), trailing underscores ("1000_"), or consecutive double underscores ("1__000") all raise ValueError.
  4. The Unicode Minus Sign Trap (\u2212): Microsoft Word, Excel, and LaTeX PDFs frequently replace the standard ASCII hyphen-minus (-, U+002D) with a Unicode minus sign (−, U+2212) or en-dash (–, U+2013). Visually, '−42' looks identical to '-42', but int("−42") crashes every time.
  • The Trigger (Verified 3.10.16):
# European price string:
euro_price = int("1.250,50")  # ValueError: invalid literal for int() with base 10: '1.250,50'

# Invalid underscore placement (PEP 515):
bad_sep = int("1__000")       # ValueError: invalid literal for int() with base 10: '1__000'
trailing = int("1000_")       # ValueError: invalid literal for int() with base 10: '1000_'

# Unicode minus sign (\u2212) from PDF/Excel:
balance = int("−42")          # ValueError: invalid literal for int() with base 10: '−42'
  • The Fix: Normalize the separators according to the source locale before calling float() and int():
# Fix for European format ("1.250,50" -> "1250.50" -> 1250):
euro_str = "1.250,50"
clean_euro = int(float(euro_str.replace(".", "").replace(",", ".")))
print(clean_euro)  # Prints: 1250

# Fix for US format with Unicode minus ("−$1,250" -> -1250):
us_str = "−$1,250"
clean_us = int(us_str.replace("\u2212", "-").replace("\u2013", "-").replace("$", "").replace(",", ""))
print(clean_us)    # Prints: -1250
  • How to tell it’s this one: The string in the traceback contains ,, $, €, leading/trailing/double _, multiple dots, or a negative sign '−' whose ascii(val) starts with '\u2212'.

4. Casting Boolean Strings ("True" / "False") to Integers

In Python, the bool type is a subclass of int, so int(True) evaluates to 1 and int(False) evaluates to 0. However, if you read a boolean flag from an environment variable (os.getenv("ENABLE_CACHE")), a config file, or a CSV row, you have the string "True" or "False"—and int("True") crashes immediately.

  • The Trigger (Verified 3.10.16):
print(int(True))    # Works: prints 1
print(int("True"))  # Fails: ValueError: invalid literal for int() with base 10: 'True'
  • The Fix: Compare the normalized string against known truthy values first, then cast the resulting boolean:
raw_flag = "True"
flag_int = int(raw_flag.strip().lower() in ("true", "1", "yes"))
print(flag_int)  # Prints: 1
  • How to tell it’s this one: The traceback ends with : 'True' or : 'False'.

5. Hexadecimal ("0x1a") or Binary ("0b101") Strings Without a base Argument

By default, int(x) assumes base=10. In base 10, the letters a–f and prefixes 0x / 0b / 0o are treated as invalid characters.

  • The Trigger (Verified 3.10.16):
val = int("0x1a")
# ValueError: invalid literal for int() with base 10: '0x1a'
  • The Fix: Pass the second base argument to int(): use int("0x1a", 16) for hex, int("0b101", 2) for binary, or pass base=0 (int("0x1a", 0)), which tells Python to auto-detect the base from the 0x, 0b, or 0o prefix:
print(int("0x1a", 16))  # Prints: 26
print(int("0x1a", 0))   # Prints: 26 (auto-detects 0x prefix)
  • How to tell it’s this one: The string in the traceback starts with '0x', '0b', '0o', or contains hex characters 'a'–'f'.

6. Passing bytes Objects from Sockets or Binary Files (b"42.0" vs. b"\x00\x2a")

When reading data from a network socket, serial port, Redis, or a binary file (open(..., "rb")), Python gives you a bytes object rather than a str. Built-in int() accepts bytes and bytearray objects directly and applies the exact same base-10 ASCII character rules (int(b"42") returns 42). However, this causes two distinct ValueError crashes:

  1. ASCII Float or Malformed Bytes (int(b"42.0")): Raises ValueError: invalid literal for int() with base 10: b'42.0'.
  2. Raw Packed Binary Bytes (int(b"\x00\x2a")): If the bytes represent raw binary integers (such as a 2-byte big-endian integer \x00\x2a for 42) rather than ASCII digit text, int() tries to read \x00* as ASCII digits and crashes!
  • The Trigger (Verified 3.10.16):
print(int(b"42"))       # Works: prints 42

# Fails on ASCII decimal bytes:
val1 = int(b"42.0")     # ValueError: invalid literal for int() with base 10: b'42.0'

# Fails on raw binary bytes:
val2 = int(b"\x00\x2a") # ValueError: invalid literal for int() with base 10: b'\x00*'
  • The Fix: For ASCII decimal bytes, use int(float(b"42.0")). For raw binary byte sequences from a socket or binary file, use int.from_bytes() and specify the byte order ("big" or "little"):
print(int(float(b"42.0")))                  # Prints: 42
print(int.from_bytes(b"\x00\x2a", "big"))   # Prints: 42
  • How to tell it’s this one: The offending literal at the end of the traceback is prefixed with a b (: b'42.0' or : b'\x00*').

7. Forgetting to Skip the Header Row in a CSV File

When iterating over a CSV file with csv.reader(), the very first row yielded is the header list of column names (such as ['order_id', 'quantity']). Calling int(row[1]) on the first iteration tries to convert the column title "quantity" into an integer.

  • The Trigger:
import csv, io

reader = csv.reader(io.StringIO("id,qty\n1,5\n"))
for row in reader:
    qty = int(row[1])  # Fails on row 1: invalid literal for int() with base 10: 'qty'
  • The Fix: Advance the iterator past the header row with next(reader, None) before starting the for loop, or use csv.DictReader().
  • How to tell it’s this one: The string inside the traceback quotes is the exact name of one of your CSV columns ('qty', 'price', 'id').

8. Converting Dirty Columns in Pandas (.astype(int)) and Polars (.cast(pl.Int64))

In Pandas 2.0.0, calling df["col"].astype(int) on a column containing float strings ("20.5"), text placeholders ("N/A"), or empty strings raises a long chained traceback ending in ValueError: invalid literal for int() with base 10 (always scroll to the very last line of a Pandas traceback to see the offending string). Even worse, if the column contains None or np.nan, .astype(int) crashes with pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.

  • The Trigger (Verified Pandas 2.0.0):
import pandas as pd

s = pd.Series(["10", "20.5", "N/A", None])
print(s.astype(int))
# Fails with: ValueError: invalid literal for int() with base 10: '20.5'
  • The Fix (Pandas & Polars):
  • In Pandas: Parse the column with pd.to_numeric(..., errors="coerce") (which turns "N/A" and "" into NaN and parses "20.5" as 20.5), round or truncate if decimals exist, and cast to Pandas’ nullable "Int64" (capital I) dtype:
import pandas as pd

s = pd.Series(["10", "20.5", "N/A", None])
clean_s = pd.to_numeric(s, errors="coerce").round().astype("Int64")
print(clean_s)
# 0      10
# 1      20
# 2    <NA>
# 3    <NA>
# dtype: Int64
  • In Polars (not run by me; noted from documentation): Calling df.with_columns(pl.col("x").cast(pl.Int64)) on invalid strings raises polars.exceptions.InvalidOperationError (or ComputeError). Pass strict=False (pl.col("x").cast(pl.Int64, strict=False)) to convert unparseable strings into null safely.

Why .isdigit() Is a Trap (And Why try/except Wins)

Many tutorials tell beginners to guard int(s) with if s.isdigit():. In production Python, relying on .isdigit() (or .isnumeric()) causes two bugs:

  1. It Rejects Negative Numbers and Leading Signs: "-42".isdigit() and "+42".isdigit() both return False because - and + are not digit characters.
  2. It Accepts Superscripts That Crash int(): Unicode superscripts like "²" (\u00b2) return True for .isdigit(), yet crash int("²") with ValueError!
print("-42".isdigit())  # False! (Rejects valid negative integers)
print("²".isdigit())    # True!  (Yet int("²") raises ValueError!)
Validation ApproachHandles “-42”?Handles ” 42\n”?Rejects “²”?Verdict
s.isdigit() / s.isnumeric() (Verified 3.10.16)❌ False❌ False❌ True (Crashes int())Avoid for int() validation.
s.isdecimal() (Verified 3.10.16)❌ False❌ False✅ FalseOnly works for unsigned, stripped strings.
try: int(s) / except ValueError: (Verified 3.10.16)✅ Works✅ Works✅ Caught safelyGold Standard in Python.

Sibling Type Conversion Error Variants (__int__ vs. __index__)

How does Python convert custom objects to integers, and why does int(42.5) work while range(42.5) or my_list[42.5] raises a TypeError?

Under the hood, Python has two distinct integer protocols:

  • __int__(self) (Lossy Conversion): Called by int(obj). It is allowed to truncate decimals (like float.__int__) or parse strings. If a custom class doesn’t define __int__, int(obj) falls back to __index__() or __trunc__().
  • __index__(self) (Lossless True Integer): Called by operations that strictly require a whole number without truncation, such as range(obj), list indexing (items[obj]), and bin(obj) / hex(obj). Neither str nor float implements __index__, which is why passing them to range() raises TypeError: '...' object cannot be interpreted as an integer.

Here are the verified sibling conversion errors on Python 3.10.16 and Pandas 2.0.0:

Error Message Variant (Verified 3.10.16)What Triggered ItThe Fix
ValueError: could not convert string to float: 'abc'Passing a non-numeric, empty, or European-formatted ('1.250,50') string to float().Normalize commas/currency first, or catch ValueError when calling float().
ValueError: Exceeds the limit (4300) for integer string conversionPassing a string with more than 4,300 digits (e.g., int("9" * 5000)), a DoS security limit added in Python 3.10.7+ / 3.11+.Reject oversized untrusted inputs, or call sys.set_int_max_str_digits(10000) if parsing huge integers intentionally.
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'Passing None (or a list / dict) into int(None) instead of a string, bytes, or number.Catch (ValueError, TypeError) together, or coalesce with int(val or 0).
TypeError: 'str' object cannot be interpreted as an integerPassing a string "5" into a C-level function that requires a true integer (__index__), such as range("5"), bin("5"), or chr("65").Explicitly wrap the string in int() first: range(int("5")).
ValueError: cannot convert float NaN to integerCalling int(float("nan")), or .astype(int) on a string/object Series containing NaN.Check if not math.isnan(x): before converting, or use .astype("Int64") in Pandas.
OverflowError: cannot convert float infinity to integerCalling int(float("inf")) or int(float("-inf")).Check if math.isfinite(x): before calling int(x).
pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer (Pandas 2.0.0)Calling df["col"].astype(int) on a numeric (float64) Series containing NaN or None.Cast to Pandas’ nullable integer type: df["col"].astype("Int64") (capital I).

Catch Unsafe int() Calls with Type Checkers and Validators

While static type checkers cannot know the runtime contents of an arbitrary user string, they will immediately catch nullable or mistyped variables passed into int() (type checker behaviors noted from documentation):

  1. Pyright / Pylance (reportArgumentType) & Mypy (arg-type): If you pass a variable typed as str | None—such as the return value of os.getenv("PORT") or dict.get("port")—directly into int(raw_port), both Pylance (reportArgumentType) and Mypy (arg-type) flag the call because int() does not accept None. Narrow the type first or provide a string default:
import os

# Flagged by Pylance (reportArgumentType) and Mypy (arg-type):
# port = int(os.getenv("PORT"))  # os.getenv returns str | None!

# Fix 1 (Optional): Provide a valid default string to os.getenv()
port = int(os.getenv("PORT", "8080"))

# Fix 2 (Required): Narrow the type with an explicit None check
raw_port = os.getenv("PORT")
if raw_port is None:
    raise RuntimeError("Missing required PORT environment variable")
port = int(raw_port)

2. Schema Validation at the Boundary (Pydantic): For API payloads, environment variables, and CSV rows, use Pydantic (BaseModel / BaseSettings), which validates integer fields at the boundary and returns structured validation errors instead of raw runtime crashes.

Worked Example: Cleaning a Messy CSV Column Without Crashing

Let’s walk through a real traceback where a script sums the quantity column from a CSV stream containing a decimal string ("3.0") and a blank cell ("").

The Code (/var/www/app/main.py):

import csv, io

raw_csv = "order_id,quantity\n101,5\n102,3.0\n103,\n"
reader = csv.DictReader(io.StringIO(raw_csv))

def total_quantity(rows):
    return sum(int(row["quantity"]) for row in rows)

print(total_quantity(reader))

Verified Traceback (Python 3.10.16):

Traceback (most recent call last):
  File "/var/www/app/main.py", line 9, in <module>
    print(total_quantity(reader))
  File "/var/www/app/main.py", line 7, in total_quantity
    return sum(int(row["quantity"]) for row in rows)
  File "/var/www/app/main.py", line 7, in <genexpr>
    return sum(int(row["quantity"]) for row in rows)
ValueError: invalid literal for int() with base 10: '3.0'

The Thought Process & Fix:

  1. Row 101 ("5") succeeded, but row 102 has "3.0" in the quantity column, which crashed int() with ValueError: invalid literal for int() with base 10: '3.0'.
  2. Even after we fix "3.0" by converting through float(), row 103 has an empty string "", which would crash next.
  3. The Result: Use a helper function that handles float strings and either returns a default 0 (when blank quantities are optional) or raises a row-specific ValueError (when every order must have a valid quantity):
import csv, io

raw_csv = "order_id,quantity\n101,5\n102,3.0\n103,\n"

# Approach A (Optional cells): Fall back to 0 on blank or invalid strings
def parse_int_optional(val, default=0):
    try:
        return int(float(val))
    except (ValueError, TypeError):
        return default

# Approach B (Required cells): Validate and name the offending order_id
def parse_int_strict(val, order_id):
    try:
        return int(float(val))
    except (ValueError, TypeError) as err:
        raise ValueError(f"Order {order_id} has invalid quantity: {val!r}") from err

reader = csv.DictReader(io.StringIO(raw_csv))
print("Optional sum:", sum(parse_int_optional(row["quantity"]) for row in reader))
# Verified Output: Optional sum: 8

Frequently Asked Questions

Why does int(42.5) work in Python, while int("42.5") raises a ValueError?

int() has two separate internal paths: when given a numeric float object (42.5), it calls 42.5.__int__() to truncate the decimal toward zero. When given a str object ("42.5"), it parses the characters strictly as a base-10 integer literal, where . is an illegal character. Convert the string to a float first with int(float("42.5")).

Does int(float("42.9")) round to 43 or truncate to 42?

int(float("42.9")) truncates toward zero, returning 42 (and -42 for "-42.9"). If you want standard rounding to the nearest integer, use round(float("42.9")), which returns an int (43) directly in Python 3.

Why does int("1_000") work, while int("_1000"), int("1000_"), or int("1__000") raises a ValueError?

Under PEP 515, Python allows single underscores as visual digit separators inside numeric literals and strings passed to int(), but only directly between two digits (or immediately after a base specifier like int("0x_1a", 16)). Leading underscores ("_1000"), trailing underscores ("1000_"), or consecutive underscores ("1__000") are rejected as invalid syntax.

Why did val.replace(" ", "") fail to fix ValueError: invalid literal for int() with base 10: '1\xa0250'?

\xa0 is the Unicode non-breaking space character (&nbsp; in HTML). Because .replace(" ", "") only removes standard ASCII spaces (U+0020), \xa0 remains inside the string. Use "".join(val.split()) or val.replace("\xa0", "").replace(" ", "") to remove both standard and non-breaking spaces.

How do I make a custom Python class work with both int(obj) and range(obj)?

Implement def __index__(self) -> int: on your class. In Python 3.8+, int(obj) automatically falls back to calling __index__() if __int__() is not defined, and defining __index__() also allows your object to be used in range(obj), list slicing items[obj], bin(obj), and hex(obj).

Similar Posts

Leave a Reply