Python

ValueError: could not convert string to float in Python (Fixes)

Python · Published 2026-10-24

An old typed invoice with handwritten sums pencilled in the margin
A 1919 wholesale invoice, with sums worked out by hand in the margin.

ValueError: could not convert string to float means float() received text that is not a plain decimal literal: an empty string, a thousands separator, a currency symbol, a placeholder such as 'N/A', or an invisible character. The fix is to clean or filter the string before converting, or to let pandas coerce bad values to NaN.

What float() actually accepts

float() parses a very narrow grammar: an optional sign, digits with one optional decimal point, an optional exponent, or the special words nan and inf. Leading and trailing whitespace (including \n) is ignored, and underscores between digits are allowed since Python 3.6. Everything else is rejected. The exact rules are in the float() documentation.

print(float(" 42.0\n"), float("1e3"), float("1_000"), float("nan"))
42.0 1000.0 1000.0 nan

The error message always quotes the offending value with repr(), which is your best clue. These are the inputs that trigger it most often:

for s in ["", "1,234.56", "$19.99", "N/A", "12.5%", "3,14", "1 000", "1.2.3"]:
    try:
        float(s)
    except ValueError as exc:
        print(exc)
could not convert string to float: ''
could not convert string to float: '1,234.56'
could not convert string to float: '$19.99'
could not convert string to float: 'N/A'
could not convert string to float: '12.5%'
could not convert string to float: '3,14'
could not convert string to float: '1 000'
could not convert string to float: '1.2.3'
Value in the messageCauseTypical fix
''Empty cell, blank line, missing form fieldSkip it or map it to a default/None
'1,234.56'Thousands separator.replace(",", "") or thousands="," in pandas
'3,14', '1.234,56'European decimal commaSwap separators, locale.atof, or decimal=","
'$19.99', '12.5%'Units or currency symbolsStrip the symbol (and divide by 100 for percentages)
'N/A', '-', 'null'Placeholder for missing dataTreat as missing (None/NaN)
'1\xa0000', '12.5'Non-breaking space, byte order mark, zero-width spaceRemove the character; fix the file encoding

Fix 1: clean the string before converting

Leading and trailing whitespace is harmless, but separators and symbols are not. Remove them explicitly, and keep the transformation obvious so nobody wonders later what was stripped.

raw = "  1,234.56\n"
print(float(raw.strip().replace(",", "")))

price = "$1,299.00"
print(float(price.replace("$", "").replace(",", "")))

eu = "1.234,56"                       # German/French style
print(float(eu.replace(".", "").replace(",", ".")))

print(float("12.5%".rstrip("%")) / 100)
1234.56
1299.0
1234.56
0.125

When the junk varies (several currencies, stray letters), a regular expression that keeps only the characters a number can contain is more robust:

import re

def clean_number(text):
    # keep digits, sign, decimal point and exponent markers
    return re.sub(r"[^0-9eE+\-.]", "", text)

print(float(clean_number("USD -2,500.75 ")))
-2500.75

Be careful: this assumes the dot is the decimal separator. Applied to "1.234,56" it would yield 1.23456 without any error, which is worse than a crash. Decide the convention per data source, not per value.

Locale-aware parsing with locale.atof

If the separators follow a known locale, the standard library can parse them for you. locale.atof uses the current LC_NUMERIC setting:

import locale

locale.setlocale(locale.LC_NUMERIC, "de_DE.UTF-8")
print(locale.atof("1.234,56"))

locale.setlocale(locale.LC_NUMERIC, "en_US.UTF-8")
print(locale.atof("1,234.56"))
1234.56
1234.56

Two caveats. The locale must be installed on the machine; otherwise setlocale raises locale.Error: unsupported locale setting (common in slim Docker images). And setlocale changes process-wide state, so avoid it in threaded web servers; prefer explicit replacements there.

Fix 2: a tolerant conversion helper

For user input or messy files, wrap the conversion in try/except and decide what a bad value should become. Catch ValueError specifically; a bare except would also hide real bugs. Note that float(None) raises a different exception, TypeError: float() argument must be a string or a real number, not 'NoneType', so handle None before calling float().

def to_float(value, default=None):
    """Convert value to float, returning default when it is not numeric."""
    if value is None:
        return default
    if isinstance(value, (int, float)):
        return float(value)
    text = str(value).strip().replace(",", "")
    if text.upper() in {"", "N/A", "NA", "NULL", "NONE", "-"}:
        return default
    try:
        return float(text)
    except ValueError:
        return default

for v in ["3.5", " 1,024 ", "", "N/A", "abc", None, 7]:
    print(repr(v), "->", to_float(v))
'3.5' -> 3.5
' 1,024 ' -> 1024.0
'' -> None
'N/A' -> None
'abc' -> None
None -> None
7 -> 7.0

Silently returning a default is convenient but can hide data problems. When parsing a file, it is often better to report where the bad value came from:

for line_no, line in enumerate(["10.5", "11.0", "12,0", "13.25"], start=1):
    try:
        value = float(line)
    except ValueError as exc:
        print(f"line {line_no}: {exc}")
line 3: could not convert string to float: '12,0'

Fix 3: pandas to_numeric with errors="coerce"

In pandas the same error shows up when you call .astype(float) on a text column:

import io
import pandas as pd

df = pd.read_csv(io.StringIO('price\n"1,299.50"\n12.00\n'))
df["price"].astype(float)
ValueError: could not convert string to float: '1,299.50'

pd.to_numeric converts a whole column at once. By default it raises (ValueError: Unable to parse string "N/A" at position 1); with errors="coerce" every unparseable value becomes NaN:

s = pd.Series(["10.5", "1,200", "", "N/A", "$7", " 3 "])
print(pd.to_numeric(s, errors="coerce"))
0    10.5
1     NaN
2     NaN
3     NaN
4     NaN
5     3.0
dtype: float64

Notice that "1,200" and "$7" were also coerced to NaN, which is probably not what you want. Clean first, then coerce, and count the NaN values afterwards to see what was lost:

s = pd.Series(["$1,200.50", "€ 80", "N/A", "(15.00)"])
cleaned = (
    s.str.replace(r"[^\d.\-()]", "", regex=True)      # drop currency, commas, spaces
     .str.replace(r"^\((.*)\)$", r"-\1", regex=True)  # accounting negatives
)
print(pd.to_numeric(cleaned, errors="coerce"))
0    1200.5
1      80.0
2       NaN
3     -15.0
dtype: float64

errors="ignore" was deprecated in pandas 2.2 and removed in 3.0 (it now raises ValueError: invalid error value specified); use "coerce" or handle the exception yourself.

Fix 4: parse numbers correctly in read_csv

The cleanest fix is to never create the bad strings. pd.read_csv understands thousands separators, decimal commas and custom missing-value markers:

import io
import pandas as pd

us_csv = io.StringIO("""product,price,qty
Widget,"1,299.50",3
Gadget,unknown,
Doohickey,"12,000.00",1
""")
df = pd.read_csv(us_csv, thousands=",", na_values=["unknown"])
print(df)

eu_csv = io.StringIO("""product;price
Widget;1.299,50
Gadget;-
""")
df_eu = pd.read_csv(eu_csv, sep=";", thousands=".", decimal=",", na_values=["-"])
print(df_eu)
print(df_eu.dtypes)
     product    price  qty
0     Widget   1299.5  3.0
1     Gadget      NaN  NaN
2  Doohickey  12000.0  1.0
  product   price
0  Widget  1299.5
1  Gadget     NaN
product        str
price      float64
dtype: object

Strings such as "N/A", "NA", "null" and empty cells are already in the default na_values list; pass extra markers for anything else. (pandas 3.0 reports text columns as str; earlier versions show object.)

Troubleshooting

Summary

Read the quoted value in the error message, then fix the input rather than the symptom: strip separators and symbols with replace() or a regex, use locale.atof when the format follows a known locale, wrap conversions in a try/except ValueError helper for untrusted input, and in pandas prefer read_csv(thousands=..., decimal=..., na_values=...) followed by pd.to_numeric(errors="coerce") for whatever is left.