Normalise Prices Across Currencies and Sources in Python
To normalise prices across currencies in Python, store every price as a Decimal with its ISO currency code, convert it with a reference rate for the date you collected it, and keep the original amount, currency, rate and rate date next to the converted figure. This guide does that for three recorded responses in three currencies: Idealo in euros, Walmart in US dollars and Mercado Libre in Argentine pesos, with a parser for strings like 162,90 €.
What do the raw prices look like?
Each source returns a number and a currency, plus a formatted string in some cases. That's the best starting point: parse the number, not the display text. The excerpts below are trimmed with …. The Idealo search endpoint returned German-formatted strings next to the number:
{
"results": [
{
"id": "203597427",
"title": "Ninja AF500",
"price": 162.9,
"price_formatted": "162,90 €",
"currency": "EUR",
"offer_count": 80,
"cheapest_offer": { "shop_name": "alza.de", "shipping_costs": "4,99 €", "shipping_is_free": false, … },
…
},
…
]
}
The Walmart search endpoint returns a bare number, and the type changes with the value:
{
"data": {
"results": [
{ "us_item_id": "17268119743", "title": "Instant Pot 4QT VORTEX 6-in-1 Mini Air Fryer, …", "price": 89.99, "currency": "USD", … },
…,
{ "us_item_id": "14140920286", "title": "Thyme & Table 6 Quart Ceramic-Coated Air Fryer, Sand White", "price": 79, "currency": "USD", … }
]
}
}
The Mercado Libre category products endpoint returns whole pesos, with the earlier price and a discount label:
{
"data": {
"site": "MLA",
"products": [
{ "item_id": "MLA1829410667", "price": 9208, "previous_price": 10963, "currency": "ARS", "discount_label": "16% OFF", … },
…
]
}
}
| Source | Price field | Type in the recording | Currency field | Formatted text |
|---|---|---|---|---|
| Idealo | price |
float | currency (EUR) |
price_formatted, shipping_costs |
| Walmart | price |
float or integer | currency (USD) |
none |
| Mercado Libre | price, previous_price |
integer | currency (ARS) |
discount_label |
How do you parse price strings like "162,90 €"?
Find the decimal separator from the string itself, strip the thousands separators, and return a Decimal with the currency the symbol implies. German, French and many other locales write a comma for decimals and a dot for thousands; US and UK formats do the opposite. A string with both marks settles it: whichever comes last is the decimal separator.
"""Normalise prices from several sources to one currency, keeping the original for audit."""
import re
from decimal import ROUND_HALF_EVEN, Decimal
CENT = Decimal("0.01")
def parse_price(text):
"""'162,90 €' -> (Decimal('162.90'), 'EUR'); '$1,234.56' -> (Decimal('1234.56'), 'USD')."""
symbols = {"€": "EUR", "$": "USD", "£": "GBP"}
currency = next((c for s, c in symbols.items() if s in text), None)
digits = re.sub(r"[^\d.,]", "", text)
if "," in digits and "." in digits:
decimal_sep = "," if digits.rfind(",") > digits.rfind(".") else "."
elif "," in digits:
decimal_sep = "," if len(digits.rsplit(",", 1)[1]) == 2 else None
elif "." in digits:
decimal_sep = "." if len(digits.rsplit(".", 1)[1]) == 2 else None
else:
decimal_sep = None
thousands = {",", "."} - {decimal_sep}
for t in thousands:
digits = digits.replace(t, "")
if decimal_sep == ",":
digits = digits.replace(",", ".")
return Decimal(digits), currency
We ran it on strings from the recordings and a few of our own. 162,90 € and 4,99 € parsed to 162.90 and 4.99 EUR, 1.234,56 € and $1,234.56 both to 1234.56, and the Idealo characteristic 2.470 (watts) to 2470 with no currency. With a single separator and three digits after it, the function treats it as a thousands separator; that's right for prices, but 1,234 in a locale that means 1.234 would be misread, so keep the source's locale in mind.
Symbols are ambiguous: $ is used by many currencies, including the Argentine peso. Use the parser's currency only when the source gives no code, and prefer the currency field whenever there is one.
How do you map each source onto one record?
Write one small function per source that returns the same fields: source, ID, title, amount as a Decimal, ISO currency, shipping if known, and the original value as text. Decimal(str(x)) matters: Decimal(89.99) would carry the float's binary error into every later sum, while Decimal("89.99") is exact.
def from_idealo(r):
return {"source": "idealo", "id": r["id"], "title": r["title"],
"amount": Decimal(str(r["price"])), "currency": r["currency"],
"shipping": parse_price(r["cheapest_offer"]["shipping_costs"])[0]
if r.get("cheapest_offer", {}).get("shipping_costs") else None,
"original": r.get("price_formatted")}
def from_walmart(r):
return {"source": "walmart", "id": r["us_item_id"], "title": r["title"],
"amount": Decimal(str(r["price"])), "currency": r["currency"],
"shipping": None, "original": str(r["price"])}
def from_mercadolibre(r):
return {"source": "mercadolibre", "id": r["item_id"], "title": r["title"],
"amount": Decimal(str(r["price"])), "currency": r["currency"],
"shipping": None, "original": str(r["price"])}
Shipping is kept separate rather than added in. Idealo gives the cheapest offer's shipping; the Walmart and Mercado Libre search results don't carry a shipping amount, only, for Mercado Libre, text such as "Envío gratis" with a note that it applies to a first purchase. Comparing item prices plus shipping only where you know shipping would bias the comparison.
Which exchange rate should you use?
A published reference rate, for the date you collected the price, from a source you name. The ECB publishes euro foreign exchange reference rates for 29 currencies, usually around 16:00 CET on working days, and says they're for information purposes only (European Central Bank, 2026). That's what price analysis needs: a consistent, citable daily rate, not the rate a bank would charge.
The ECB's list doesn't include the Argentine peso. For ARS, the Banco Central de la República Argentina publishes a wholesale reference rate against the US dollar, known by its regulation, Communication A 3500, with historical series to download (BCRA, 2026). Convert ARS to USD with that, then USD to EUR with the ECB rate for the same date, and record both rates.
def to_eur(row, rates, rate_date):
"""rates: {(currency, date): units of currency per 1 EUR}, as the ECB publishes them."""
out = dict(row, target="EUR", rate_date=rate_date)
if row["currency"] == "EUR":
out.update(rate=Decimal(1), amount_eur=row["amount"].quantize(CENT))
return out
rate = rates.get((row["currency"], rate_date))
if rate is None:
out.update(rate=None, amount_eur=None) # no dated rate: leave it unconverted
return out
eur = (row["amount"] / rate).quantize(CENT, rounding=ROUND_HALF_EVEN)
out.update(rate=rate, amount_eur=eur)
return out
The function refuses to guess: a currency with no rate for that date stays unconverted, with amount_eur set to None. Load rates from the ECB's daily file or its data API, keyed by currency and date. Use the rate for the collection date, or the last published rate before it on weekends and holidays, and store which one you used.
What does the normalised output look like?
We ran the three mappers and to_eur on the recorded responses with one rate: 1.1193 US dollars per euro, the ECB reference rate for 23 October 2026. We read that figure from the ECB's published table through search results, not from its data feed, so check it against the ECB's file before you reuse it. We had no ARS rate loaded, so the Mercado Libre rows stayed unconverted, as designed.
| Source | Product | Original | Amount | Currency | EUR |
|---|---|---|---|---|---|
| idealo | Ninja AF500 | 162,90 € |
162.9 | EUR | 162.90 |
| idealo | Philips NA221/00 | 76,30 € |
76.3 | EUR | 76.30 |
| walmart | Chefman Air Fryer 1500W, 6 Quart | 59.98 |
59.98 | USD | 53.59 |
| walmart | Thyme & Table 6 Quart Ceramic-Coated Air Fryer | 79 |
79 | USD | 70.58 |
| walmart | Instant Pot 4QT VORTEX Mini Air Fryer | 89.99 |
89.99 | USD | 80.40 |
| mercadolibre | Barra Luz Led 50cm Recargable | 9208 |
9208 | ARS | none (no rate) |
Every row keeps the original text, the parsed amount, the currency, and, in the full output, the rate and its date. If the rate turns out to be wrong, or you switch to a different rate source, you can recompute every converted figure without calling the APIs again. Load the rows into a table with those columns; loading API JSON into Postgres shows the pattern.
Why is a converted price still not like for like?
Because currency is only one difference. Taxes, shipping, product and market all differ, and none of them is fixed by an exchange rate. Normalise currency first, then decide which of the others your analysis must control for.
Taxes are the largest. Germany's price indication rules require prices offered to consumers to be stated as total prices including VAT (PAngV § 3, 2026). US retail prices are usually shown before sales tax, which varies by state and city. So 162.90 EUR on Idealo and 53.59 EUR converted from Walmart differ by tax treatment as well as by product.
Products need matching, too: the table compares different air fryers, not one model in two markets. And in a currency with fast-moving rates, converting a month of prices at one rate misstates every day but one, which is why the code keys rates by date. For matching the same product across sources, see deduplicating listings across sources.
What does collecting the prices cost?
Each call to these endpoints costs 1 credit on success, and failed calls cost nothing and return X-Credits-Used: 0. Normalisation runs locally and costs nothing. The credits docs explain billing; exchange rate files from the ECB and the BCRA are free.
| Job | Calls a day | Credits a month (30 days) |
|---|---|---|
| 1 query on each of 3 sources | 3 | 90 |
| 10 queries on each of 3 sources | 30 | 900 |
| 100 queries on each of 3 sources | 300 | 9,000 |
Worked example: tracking 10 products on Idealo, Walmart and Mercado Libre daily is 10 × 3 × 30 = 900 credits a month, inside the 1,000 free monthly credits. A hundred products is 9,000 credits a month, inside a Starter pack of 10,000 credits; purchased credits last 12 months. See pricing for what credits cost. The source guides cover each endpoint's parameters: Idealo, Walmart and Mercado Libre.
To try the endpoints, get a free API key and run a search in the playground on the Idealo API page, the Walmart API page or the Mercado Libre API page. Data is collected from publicly available sources and returned as found at the time of the call.
APIs in this post
Frequently asked questions
How do I convert prices in different currencies in Python?
Parse each price into a Decimal, attach its ISO currency code, and divide by a reference rate for the collection date, such as the ECB's euro rate. Round with quantize(Decimal("0.01")). Keep the original amount, currency, rate and rate date on the row, so any conversion can be checked and recomputed later.
Why use Decimal instead of float for prices?
Floats can't represent most decimal fractions exactly, so sums and conversions drift by small amounts. Decimal(str(89.99)) is exactly 89.99. Build Decimals from strings, not floats: Decimal(89.99) carries the float's binary error into the result. Round only once, when you store or display the converted value.
Which exchange rate source should I use?
A published, dated reference rate you can cite. The ECB's euro reference rates cover 29 currencies on working days and are free; for currencies it doesn't publish, such as the Argentine peso, use that country's central bank, such as the BCRA's Communication A 3500 rate. Record which source and date you used for every row.
How do I parse prices with commas as decimal separators?
Look at the string: if it has both a comma and a dot, the one that comes last is the decimal separator. With only a comma followed by two digits, as in 162,90 €, the comma is decimal. Remove the other separator, swap a decimal comma for a dot, and build a Decimal.
Can I compare converted prices across countries directly?
Not without caveats. German consumer prices include VAT by law, while US shelf prices usually exclude sales tax; shipping and the exact product also differ. Convert currency first, then adjust for tax and shipping where your analysis needs it, and compare matched products rather than similar ones.