ValueError: could not convert string to float means you passed float() a string that Python does not accept as a number. The single most common cause in real data is the decimal comma: float("77.59") returns 77.59, while float("77,59") raises, because Python only recognises a dot as the decimal separator.
The comma is not the only cause. Next come an empty string left at the end of a file, an invisible BOM in the first cell of a spreadsheet export, and a non-breaking space between thousands, as in 1 234,50. Each is covered below, starting from where the string came from.
What does could not convert string to float actually mean?
It is a ValueError: float() received a string and could not parse it as a number. Python accepts only a strict format — an optional sign, digits, a dot as the decimal separator, and optionally an exponent such as 1e3. Anything else — a comma, a currency symbol, letters, emptiness — raises. The useful part is that the message prints the offending string in quotes, so you can identify the cause at a glance.
price = "77,59"
print(float(price))
The last line of the output:
ValueError: could not convert string to float: '77,59'
Read what sits in the quotes:
'77,59' — a comma instead of a dot;
'' — an empty string arrived;
'\ufeff77,59' — a BOM glued to the front;
'1\xa0234,50' — a non-breaking space between thousands;
'12.5 EUR' — text leaked into the value.
If the quotes contain \ufeff or \xa0, the character is invisible and you will never spot it in a terminal. So the first move on any confusing string is print(repr(raw)).
That only helps if the message survives long enough to be read. Launched by double-click, the console window dies with the process and takes the ValueError with it — run the file from a terminal first, as covered in the console window closes immediately.
Why does float('12,5') fail while float('12.5') works?
Because float() parses the string by Python's own syntax rules, not your locale's. In source code a fractional part is written with a dot — x = 12.5 — and the function reuses that same parser. A comma is not a separator to it but a foreign character, which invalidates the whole string. The 77,59 spelling arrives from Excel, from European bank exports and from plain input(), which is why it triggers this error more than anything else.
| String |
float(...) |
Why |
"77.59" |
77.59 |
a dot is the only decimal separator |
"77,59" |
ValueError |
a comma is not parsed |
"1e3" |
1000.0 |
exponent notation is allowed |
" 12.5\n" |
12.5 |
surrounding whitespace is dropped |
"" |
ValueError |
empty string |
"nan" |
nan |
a valid float value |
The minimal fix:
price = "77,59"
print(float(price.replace(",", ".")))
Which strings Python treats as numbers is summarised in the type conversion handbook entry.
When is replace(',', '.') not enough?
The replacement fixes the comma and nothing else. A real export also carries thousands separators in two flavours: an ordinary space and a non-breaking space \xa0 (U+00A0), which spreadsheets insert so the number does not break across lines. On screen the two look identical.
raw = " 1\u00a0234,50\n"
print(repr(raw))
' 1\xa0234,50\n'
A subtlety: float() strips whitespace around the string, including the non-breaking kind, but never inside the number. So clean the whole set at once:
def to_float(raw: str) -> float:
cleaned = raw.replace("\ufeff", "").replace("\u00a0", "").replace(" ", "")
return float(cleaned.replace(",", "."))
print(to_float("77,59"))
print(to_float(" 1\u00a0234,50\n"))
print(to_float("12.5"))
77.59
1234.5
12.5
| What is in the string |
replace(",", ".") only |
Full cleanup |
"77,59" |
77.59 |
77.59 |
"1 234,50" (plain space) |
ValueError |
1234.5 |
"1\xa0234,50" (non-breaking) |
ValueError |
1234.5 |
"\ufeff8,0" (BOM) |
ValueError |
8.0 |
Note the third line: to_float("12.5") works too. Replacing a comma with a dot is harmless for strings without one, so a single function handles a whole column.
Reaching for rstrip() here is tempting, but it is a trap: it removes a set of characters, not a suffix. How that silently eats real letters is covered in the article on removing a file extension.
Does a trailing newline break float()?
No. The popular explanation that a trailing \n is the culprit is wrong: float() discards whitespace at both ends, so float("12.5\n") returns 12.5. What breaks is the empty string the newline produces — if a file ends with \n, splitting its text into lines leaves '' as the last element.
text = "77,59\n1 234,50\n\n8,0\n"
for line in text.split("\n"):
print(repr(line))
'77,59'
'1 234,50'
''
'8,0'
''
Two empty strings: a genuine blank line and the tail after the last \n. Both produce the same ValueError with empty quotes. The cure is a filter — do not parse what is empty after cleaning.
lines = ["77,59", "", "8,0"]
prices = [float(x.replace(",", ".")) for x in lines if x.strip()]
print(prices)
print(sum(prices))
[77.59, 8.0]
85.59
Why does the error hit only the first row of a CSV?
The usual culprit is a BOM — three service bytes Excel writes at the start of a file saved as "CSV UTF-8". Read with encoding="utf-8", those bytes become an invisible \ufeff character glued to the first value of the first row. Every other row parses fine, which is why only the first line seems cursed even though it looks identical to the second.
row = "\ufeff77,59"
print(len(row))
print(repr(row))
The fix belongs in open(), not in twenty cleaning calls. The utf-8-sig codec swallows the BOM:
from pathlib import Path
Path("prices.csv").write_bytes("product;price\ncoffee;77,59\n".encode("utf-8-sig"))
with open("prices.csv", encoding="utf-8") as f:
print(repr(f.readline()))
with open("prices.csv", encoding="utf-8-sig") as f:
print(repr(f.readline()))
'\ufeffproduct;price\n'
'product;price\n'
If the file gives you mojibake or a UnicodeDecodeError instead, the encoding itself is wrong — see the article on cp1251 versus UTF-8. Parsing CSV properly, with csv.reader, is in the CSV handbook entry.
How is this different from invalid literal for int()?
Both are ValueError, but they come from different functions. float() complains with could not convert string to float, while int() says invalid literal for int() with base 10. The key point is that int() is stricter: it rejects even a correctly written fractional number, since it has no mandate to round or truncate.
print(int("12.5"))
ValueError: invalid literal for int() with base 10: '12.5'
For a fractional value, go through float() first:
print(float("12.5"))
print(int(float("12.5")))
The wording tells you which call failed: "float" in the message means float() raised, "int() with base 10" means int() did.
Should you validate first or catch the exception?
Validating with isdigit() does not work: "12.5".isdigit() is False because a dot is not a digit, so the check rejects every valid decimal. A hand-rolled regex for "a number with a comma, spaces and maybe a BOM" is half a day of work and still incomplete. Clean the string, try the conversion and catch ValueError — the EAFP style Python is built around.
def parse_price(raw: str):
cleaned = raw.replace("\ufeff", "").replace("\u00a0", "").replace(" ", "")
try:
return float(cleaned.replace(",", "."))
except ValueError:
return None
rows = ["77,59", "1\u00a0234,50", "", "no data", "\ufeff8,0"]
for row in rows:
print(repr(row), "->", parse_price(row))
'77,59' -> 77.59
'1\xa0234,50' -> 1234.5
'' -> None
'no data' -> None
'\ufeff8,0' -> 8.0
This parser survives dirty data and marks unparsable cells as None instead of zero — a zero would silently distort every total downstream. The trade-off between asking first and trying first is in the EAFP and LBYL entry.
What about locale and Decimal?
A frequent piece of advice is locale.atof(), which parses numbers by the active locale and therefore understands the comma.
import locale
try:
locale.setlocale(locale.LC_NUMERIC, "de_DE.UTF-8")
print(locale.atof("77,59"))
except locale.Error:
print("the de_DE.UTF-8 locale is not installed")
It works right up to the moment the code moves to a server: that call needs the locale installed on the system, and a slim Docker image does not ship one — you land in the except branch. The locale is also process-global, so it changes unrelated code. The rule: clean strings explicitly when parsing, keep locale for formatting. Money deserves its own note — a float is a binary fraction and cannot hold exact decimal cents, so sum prices with Decimal, which also wants a dot.
from decimal import Decimal
print(0.1 + 0.2)
print(Decimal("77,59".replace(",", ".")) * 3)
Common mistakes
- Replacing the comma in US-formatted numbers. In
"1,234.50" the comma is a thousands separator, so replace(",", ".") yields "1.234.50", which parses as nothing. Check the format with repr() first.
- A bare
except:. It swallows a typo in a variable name and KeyboardInterrupt alike. Catch except ValueError.
- Forgetting
None. float(None) raises TypeError, not ValueError, so except ValueError lets it through. Catch except (TypeError, ValueError) when values can be None.
- Validating with
isdigit(). It rejects valid decimals: "12.5".isdigit() returns False.
- Stripping the BOM in every row. It enters once, from the file, and belongs in
open() as encoding="utf-8-sig".
- Substituting zero for unparsable values. Zero is indistinguishable from a real price of zero and corrupts sums and averages. Return
None.
- Ignoring
nan. float("nan") does not raise. A stray "NaN" becomes a value that is not equal to itself: nan == nan is False, while nan != nan is True. Every <, >, <=, >= comparison against it is False too, so filters and sorts over that column quietly lie.
Practice
Parsing sticks once you run a parser. Three free Python Arena tasks, graded by hidden tests in the browser:
In short
could not convert string to float is a ValueError from float(), and the quotes hold the exact string that broke it.
- The number one cause is the decimal comma:
float("77,59") raises, float("77.59") works.
- The minimal fix is
replace(",", "."); real exports also need \xa0, plain spaces and the BOM removed.
- A trailing
\n is harmless — float("12.5\n") works. The empty '' left after splitting is what raises.
- An error only on the first CSV row is almost always the BOM: use
encoding="utf-8-sig" in open().
int("12.5") raises invalid literal for int() with base 10; the correct route is int(float("12.5")).
- For dirty data, cleaning plus
try/except ValueError returning None beats an up-front isdigit() check.
ValueError: could not convert string to floatmeans you passedfloat()a string that Python does not accept as a number. The single most common cause in real data is the decimal comma:float("77.59")returns77.59, whilefloat("77,59")raises, because Python only recognises a dot as the decimal separator.The comma is not the only cause. Next come an empty string left at the end of a file, an invisible BOM in the first cell of a spreadsheet export, and a non-breaking space between thousands, as in
1 234,50. Each is covered below, starting from where the string came from.What does could not convert string to float actually mean?
It is a
ValueError:float()received a string and could not parse it as a number. Python accepts only a strict format — an optional sign, digits, a dot as the decimal separator, and optionally an exponent such as1e3. Anything else — a comma, a currency symbol, letters, emptiness — raises. The useful part is that the message prints the offending string in quotes, so you can identify the cause at a glance.price = "77,59" print(float(price))The last line of the output:
Read what sits in the quotes:
'77,59'— a comma instead of a dot;''— an empty string arrived;'\ufeff77,59'— a BOM glued to the front;'1\xa0234,50'— a non-breaking space between thousands;'12.5 EUR'— text leaked into the value.If the quotes contain
\ufeffor\xa0, the character is invisible and you will never spot it in a terminal. So the first move on any confusing string isprint(repr(raw)).That only helps if the message survives long enough to be read. Launched by double-click, the console window dies with the process and takes the
ValueErrorwith it — run the file from a terminal first, as covered in the console window closes immediately.Why does float('12,5') fail while float('12.5') works?
Because
float()parses the string by Python's own syntax rules, not your locale's. In source code a fractional part is written with a dot —x = 12.5— and the function reuses that same parser. A comma is not a separator to it but a foreign character, which invalidates the whole string. The77,59spelling arrives from Excel, from European bank exports and from plaininput(), which is why it triggers this error more than anything else.float(...)"77.59"77.59"77,59"ValueError"1e3"1000.0" 12.5\n"12.5""ValueError"nan"nanThe minimal fix:
price = "77,59" print(float(price.replace(",", "."))) # 77.59Which strings Python treats as numbers is summarised in the type conversion handbook entry.
When is replace(',', '.') not enough?
The replacement fixes the comma and nothing else. A real export also carries thousands separators in two flavours: an ordinary space and a non-breaking space
\xa0(U+00A0), which spreadsheets insert so the number does not break across lines. On screen the two look identical.raw = " 1\u00a0234,50\n" print(repr(raw))A subtlety:
float()strips whitespace around the string, including the non-breaking kind, but never inside the number. So clean the whole set at once:def to_float(raw: str) -> float: cleaned = raw.replace("\ufeff", "").replace("\u00a0", "").replace(" ", "") return float(cleaned.replace(",", ".")) print(to_float("77,59")) print(to_float(" 1\u00a0234,50\n")) print(to_float("12.5"))replace(",", ".")only"77,59"77.5977.59"1 234,50"(plain space)ValueError1234.5"1\xa0234,50"(non-breaking)ValueError1234.5"\ufeff8,0"(BOM)ValueError8.0Note the third line:
to_float("12.5")works too. Replacing a comma with a dot is harmless for strings without one, so a single function handles a whole column.Reaching for
rstrip()here is tempting, but it is a trap: it removes a set of characters, not a suffix. How that silently eats real letters is covered in the article on removing a file extension.Does a trailing newline break float()?
No. The popular explanation that a trailing
\nis the culprit is wrong:float()discards whitespace at both ends, sofloat("12.5\n")returns12.5. What breaks is the empty string the newline produces — if a file ends with\n, splitting its text into lines leaves''as the last element.text = "77,59\n1 234,50\n\n8,0\n" for line in text.split("\n"): print(repr(line))Two empty strings: a genuine blank line and the tail after the last
\n. Both produce the sameValueErrorwith empty quotes. The cure is a filter — do not parse what is empty after cleaning.lines = ["77,59", "", "8,0"] prices = [float(x.replace(",", ".")) for x in lines if x.strip()] print(prices) print(sum(prices))Why does the error hit only the first row of a CSV?
The usual culprit is a BOM — three service bytes Excel writes at the start of a file saved as "CSV UTF-8". Read with
encoding="utf-8", those bytes become an invisible\ufeffcharacter glued to the first value of the first row. Every other row parses fine, which is why only the first line seems cursed even though it looks identical to the second.row = "\ufeff77,59" print(len(row)) # 6, although five characters are visible print(repr(row)) # '\ufeff77,59'The fix belongs in
open(), not in twenty cleaning calls. Theutf-8-sigcodec swallows the BOM:from pathlib import Path Path("prices.csv").write_bytes("product;price\ncoffee;77,59\n".encode("utf-8-sig")) with open("prices.csv", encoding="utf-8") as f: print(repr(f.readline())) with open("prices.csv", encoding="utf-8-sig") as f: print(repr(f.readline()))If the file gives you mojibake or a
UnicodeDecodeErrorinstead, the encoding itself is wrong — see the article on cp1251 versus UTF-8. Parsing CSV properly, withcsv.reader, is in the CSV handbook entry.How is this different from invalid literal for int()?
Both are
ValueError, but they come from different functions.float()complains withcould not convert string to float, whileint()saysinvalid literal for int() with base 10. The key point is thatint()is stricter: it rejects even a correctly written fractional number, since it has no mandate to round or truncate.print(int("12.5"))For a fractional value, go through
float()first:print(float("12.5")) # 12.5 print(int(float("12.5"))) # 12The wording tells you which call failed: "float" in the message means
float()raised, "int() with base 10" meansint()did.Should you validate first or catch the exception?
Validating with
isdigit()does not work:"12.5".isdigit()isFalsebecause a dot is not a digit, so the check rejects every valid decimal. A hand-rolled regex for "a number with a comma, spaces and maybe a BOM" is half a day of work and still incomplete. Clean the string, try the conversion and catchValueError— the EAFP style Python is built around.def parse_price(raw: str): cleaned = raw.replace("\ufeff", "").replace("\u00a0", "").replace(" ", "") try: return float(cleaned.replace(",", ".")) except ValueError: return None rows = ["77,59", "1\u00a0234,50", "", "no data", "\ufeff8,0"] for row in rows: print(repr(row), "->", parse_price(row))This parser survives dirty data and marks unparsable cells as
Noneinstead of zero — a zero would silently distort every total downstream. The trade-off between asking first and trying first is in the EAFP and LBYL entry.What about locale and Decimal?
A frequent piece of advice is
locale.atof(), which parses numbers by the active locale and therefore understands the comma.import locale try: locale.setlocale(locale.LC_NUMERIC, "de_DE.UTF-8") print(locale.atof("77,59")) except locale.Error: print("the de_DE.UTF-8 locale is not installed")It works right up to the moment the code moves to a server: that call needs the locale installed on the system, and a slim Docker image does not ship one — you land in the
exceptbranch. The locale is also process-global, so it changes unrelated code. The rule: clean strings explicitly when parsing, keeplocalefor formatting. Money deserves its own note — afloatis a binary fraction and cannot hold exact decimal cents, so sum prices withDecimal, which also wants a dot.from decimal import Decimal print(0.1 + 0.2) # 0.30000000000000004 print(Decimal("77,59".replace(",", ".")) * 3) # 232.77Common mistakes
"1,234.50"the comma is a thousands separator, soreplace(",", ".")yields"1.234.50", which parses as nothing. Check the format withrepr()first.except:. It swallows a typo in a variable name andKeyboardInterruptalike. Catchexcept ValueError.None.float(None)raisesTypeError, notValueError, soexcept ValueErrorlets it through. Catchexcept (TypeError, ValueError)when values can beNone.isdigit(). It rejects valid decimals:"12.5".isdigit()returnsFalse.open()asencoding="utf-8-sig".None.nan.float("nan")does not raise. A stray"NaN"becomes a value that is not equal to itself:nan == nanisFalse, whilenan != nanisTrue. Every<,>,<=,>=comparison against it isFalsetoo, so filters and sorts over that column quietly lie.Practice
Parsing sticks once you run a parser. Three free Python Arena tasks, graded by hidden tests in the browser:
\xa0.In short
could not convert string to floatis aValueErrorfromfloat(), and the quotes hold the exact string that broke it.float("77,59")raises,float("77.59")works.replace(",", "."); real exports also need\xa0, plain spaces and the BOM removed.\nis harmless —float("12.5\n")works. The empty''left after splitting is what raises.encoding="utf-8-sig"inopen().int("12.5")raisesinvalid literal for int() with base 10; the correct route isint(float("12.5")).try/except ValueErrorreturningNonebeats an up-frontisdigit()check.