Skip to main content

Command Palette

Search for a command to run...

Preserve decimal commas when pasting into Streamlit

Updated
6 min readView as Markdown

When someone copies 23,4 from a spreadsheet, they usually expect to keep that value. If an input column interprets the comma as a thousands separator, the result can become 234 before the application gets a chance to validate it.

That exact symptom appears in Streamlit issue #7866. A later report from the same reporter was closed as overlapping, so it should not be counted as an independent report or a separate confirmed bug. The useful lesson is to decide what the source punctuation means before converting the input.

Here is a small text-column workflow for apps that can let users paste text first and review converted values afterward. It uses Python's standard-library Decimal and native Streamlit widgets. The complete code below is free to use under the MIT license.

Keep the input and the number separately

Three decisions make this workflow predictable:

  • Preserve the raw string, including leading integer zeros, alongside the result.
  • Choose a specific input grammar. This first example accepts comma decimals and does not accept thousands grouping.
  • Construct Decimal directly from the validated text. Return a string for the JSON preview so it does not turn into a binary floating-point number.

Display formatting is a separate concern. The NumberColumn documentation says its formatting does not change the value returned by st.data_editor. A community discussion about localized formatting also illustrates why changing a Python locale is not the same as selecting the browser's display language. Neither operation reconstructs punctuation already lost during conversion.

A converter with a deliberately small grammar

Save this as decimal_comma.py. It accepts a string or None from the text column:

import re
from decimal import Decimal

def convert(raw):
    if raw is None:
        return {"raw": raw, "status": "empty"}
    text = raw.strip(" \u00a0\u202f")
    if not text:
        return {"raw": raw, "status": "empty"}
    if len(raw) > 256 or re.fullmatch(r"[+-]?[0-9]+(?:,[0-9]+)?", text) is None:
        return {"raw": raw, "status": "invalid"}
    return {"raw": raw, "decimal": str(Decimal(text.replace(",", "."))), "status": "valid"}

The regular expression requires ASCII digits, an optional leading sign and, if a comma is present, digits on both sides. It rejects currency symbols, percentages, exponents, underscores, tabs, newlines and non-finite values such as NaN. Ordinary space, non-breaking space (U+00A0) and narrow non-breaking space (U+202F) are allowed around the whole number.

1,234 is accepted as the decimal value 1.234. If your source intended a grouped integer, this grammar is the wrong choice. An ambiguous string cannot tell you the user's intention on its own.

Nonempty input longer than 256 characters is rejected. The demo treats whitespace-only input as empty. The raw input remains in every report, including invalid and empty results.

Put a text column in front of it

Save this as app.py beside decimal_comma.py:

import streamlit as st
from decimal_comma import convert

st.title("Paste decimal commas as text")
st.caption("Comma decimals, without thousands grouping. Original values stay visible.")

edited = st.data_editor(
    {"Raw value": ["23,4", "0012,00"]},
    column_config={"Raw value": st.column_config.TextColumn("Raw value", max_chars=256)},
    num_rows="dynamic",
    key="raw_values",
)
if st.button("Convert"):
    st.json([convert(raw) for raw in edited["Raw value"]])

Use Python 3.11 or newer. Create an environment, install the version used for this example, and start the local app:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install streamlit==1.63.0
python -m streamlit run app.py --server.address 127.0.0.1 --server.headless true --browser.gatherUsageStats false

On Windows, activate with .venv\Scripts\activate instead. Open the local URL printed in the terminal. Click the first cell and paste this single column, then finish editing and click Convert:

23,4
0012,00
1.234,50
9007199254740993,50

The expected results are 23.4, 12.00, an invalid row, and 9007199254740993.50. The third value uses dot grouping, which this minimal converter deliberately rejects. Add an empty row to see a separate empty result.

The JSON preview keeps decimals as strings. The numerical value of 0012,00 is represented as 12.00; its leading integer zeros survive in the accompanying raw string. For arithmetic in your application, use Decimal objects and an appropriate arithmetic context. Python's Decimal documentation explains the distinction between exact construction from text and later arithmetic, which can round according to that context.

This app sends the browser input to the Python process on your computer. It is not a browser-only converter. The example does not save the values or submit them to another service. A remote deployment would receive those values on the remote server instead.

Check the cases that are easy to miss

Save this as check.py beside the converter, then run python check.py. No Streamlit installation is required for these checks:

from decimal_comma import convert

assert convert("23,4")["decimal"] == "23.4"
assert convert("0012,00") == {"raw": "0012,00", "decimal": "12.00", "status": "valid"}
assert convert("1,234")["decimal"] == "1.234"
assert convert("1.234,50")["status"] == "invalid"
assert convert("9007199254740993,50")["decimal"] == "9007199254740993.50"
assert convert("NaN")["status"] == "invalid"
assert convert(None) == {"raw": None, "status": "empty"}
assert convert(" \u00a0")["status"] == "empty"
print("All eight decimal checks passed.")

These cases check parsing and the report representation. They do not replace a real paste test from the spreadsheet and browser your users have. I checked the native text-column workflow in Chrome with Streamlit 1.63.0, including multi-row paste, empty rows, invalid grouping and long values. A text column is an alternative application input path; this does not patch NumberColumn or recover input already changed upstream.

When your source includes thousands grouping

For an application that needs both 1.234,50 and 1,234.50, expose the decimal and grouping choices explicitly. I maintain a free parser and runnable Streamlit example with those controls.

Copy decimal_text.py and LICENSE from that directory beside your code, then use:

from decimal_text import parse_decimal_text

comma_value = parse_decimal_text("1.234,50", decimal_mark=",", group_mark=".")
dot_value = parse_decimal_text("1,234.50", decimal_mark=".", group_mark=",")
assert str(comma_value) == "1234.50"
assert str(dot_value) == "1234.50"

That helper validates groups of three digits, supports three explicit space characters, rejects malformed groups such as 12.34,5, and returns Decimal objects. It has 12 tests covering its documented grammar. The accompanying app keeps raw values visible and clears the previous conversion report when the table or selected format changes.

Choose one source format for a conversion batch. If rows mix formats, ask the user to separate them or provide per-row format information. Guessing from punctuation can produce a plausible but incorrect number.

Buy me a coffee, if this helped

If this saved you a little time, you're welcome to buy me a coffee. Please don't feel obliged — using the example, reporting an issue or sharing it is appreciated too.

  • USDC on Solana: 9tY6D9mwcFaJwwzEHvw2v7nhSpdjqjNBYtuooyBN6rYy
  • USDC on Base: 0x568Ab98578d682FB0B0b45619BE73EbFfbf5a6eA

Please match the asset and network exactly. Fees depend on your wallet or exchange. Thank you! — Tevinch