You’re going to build a currency converter in Python: a small CLI that takes 100 USD EUR, fetches the live rate from the Exchange Rate API, and prints the converted amount. The finished code is on GitHub at apiverve/currency-converter-python-tutorial if you’d rather clone first and read later.
The Request
One GET, two query parameters, one header:
curl -G "https://api.apiverve.com/v1/exchangerate" \
-H "x-api-key: YOUR_API_KEY" \
--data-urlencode "currency1=USD" \
--data-urlencode "currency2=EUR"
And what comes back:
{
"status": "ok",
"error": null,
"data": {
"currency1": "USD",
"currency2": "EUR",
"exchangeRate": "0.851223"
}
}
exchangeRate is how many units of currency2 you get for one unit of currency1. So 100 USD is 100 * 0.851223 = 85.12 EUR.
Notice the rate is a string, not a number. That trips people up. You have to parse it before doing math, and it’s also a hint about how to treat it (more on that below).
Everything else in this tutorial is wrapping that call in something usable.
Step 1: Set Up the Project
You need Python 3.8+ and the requests library. That’s the entire dependency list.
git clone https://github.com/apiverve/currency-converter-python-tutorial.git
cd currency-converter-python-tutorial
pip install -r requirements.txt
Starting from scratch instead? pip install requests and create a converter.py.
Then grab a key. Get a free API key and paste it into the config block at the top of converter.py:
import requests
import sys
API_KEY = 'your-api-key-here'
API_URL = 'https://api.apiverve.com/v1/exchangerate'
Step 2: Write the Conversion Function
This is the core of the repo. It calls the API, parses the rate, multiplies, and returns a dict. Errors come back as {'error': ...} instead of exceptions so the CLI can print them without a traceback.
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
if API_KEY == 'your-api-key-here':
return {'error': 'API key not configured. Add your key to converter.py'}
try:
response = requests.get(
API_URL,
params={
'currency1': from_currency.upper(),
'currency2': to_currency.upper()
},
headers={
'x-api-key': API_KEY
}
)
data = response.json()
if data.get('status') == 'ok':
rate = float(data['data']['exchangeRate'])
converted = amount * rate
return {
'success': True,
'amount': amount,
'from': from_currency.upper(),
'to': to_currency.upper(),
'rate': rate,
'result': round(converted, 2)
}
else:
return {'error': data.get('error', 'Conversion failed')}
except requests.RequestException as e:
return {'error': f'API request failed: {str(e)}'}
except (KeyError, ValueError) as e:
return {'error': f'Invalid response: {str(e)}'}
A few things worth pointing out:
- The placeholder check isn’t just politeness. A missing or malformed key gets blocked before it reaches the API, and that rejection isn’t JSON. Without the guard,
response.json()blows up with a confusing decode error instead of telling you the real problem. .upper()on both codes meansusdandUSDbehave the same. The API normalizes too, but uppercasing locally keeps your output consistent.- It branches on
status, not the HTTP code. Every APIVerve response uses the samestatus/error/dataenvelope, so checkingstatus == 'ok'works across all of them. ValueErrorcovers two failures: a body that isn’t JSON, and a rate string that won’t parse as a float.
Step 3: Add the CLI
The repo supports two modes: arguments for scripting, and an interactive prompt when you run it bare. Here’s the argument path:
def print_result(result: dict):
if 'error' in result:
print(f"\nError: {result['error']}\n")
else:
print(f"\n{'='*40}")
print(f" {result['amount']:,.2f} {result['from']}")
print(f" = {result['result']:,.2f} {result['to']}")
print(f"{'='*40}")
print(f" Exchange Rate: 1 {result['from']} = {result['rate']} {result['to']}")
print()
def main():
if len(sys.argv) == 4:
try:
amount = float(sys.argv[1])
result = convert_currency(amount, sys.argv[2], sys.argv[3])
print_result(result)
except ValueError:
print("Usage: python converter.py <amount> <from_currency> <to_currency>")
sys.exit(1)
else:
interactive_mode()
if __name__ == '__main__':
main()
(interactive_mode() is a while True loop around input() that calls the same two functions and exits on quit. It’s in the repo; it’s not interesting enough to paste here.)
Run it:
python converter.py 100 USD EUR
========================================
100.00 USD
= 85.12 EUR
========================================
Exchange Rate: 1 USD = 0.851223 EUR
Your numbers will differ. The rate is live.
Step 4: Handle the Errors You’ll Actually Hit
Pass a currency code the API doesn’t know (XYZ, or a typo like EURO) and you get a 404 with the standard envelope:
{
"status": "error",
"error": "currency rates not found",
"data": null
}
The repo’s else branch surfaces that error string as-is, which is the right call. Don’t replace it with your own vaguer message.
The other two you’ll run into:
- 401 means the key isn’t recognized. Check for a copy-paste error.
- 429 means you hit the rate limit or ran out of credits. Retrying helps with the first and does nothing for the second, so don’t wrap it in a blind retry loop.
The full list lives in the error handling docs.
Step 5: Harden It for Real Use
The repo is deliberately minimal. Before this goes anywhere near production, make three changes: read the key from the environment, set a timeout (requests waits forever by default), and use Decimal for the money math.
import os
from decimal import Decimal, ROUND_HALF_UP
import requests
API_KEY = os.environ['APIVERVE_API_KEY']
API_URL = 'https://api.apiverve.com/v1/exchangerate'
def get_rate(from_currency: str, to_currency: str) -> Decimal:
response = requests.get(
API_URL,
params={'currency1': from_currency.upper(), 'currency2': to_currency.upper()},
headers={'x-api-key': API_KEY},
timeout=10,
)
body = response.json()
if body.get('status') != 'ok':
raise RuntimeError(body.get('error') or f'HTTP {response.status_code}')
return Decimal(body['data']['exchangeRate'])
def convert(amount: str, from_currency: str, to_currency: str, places: int = 2) -> Decimal:
rate = get_rate(from_currency, to_currency)
quantum = Decimal(1).scaleb(-places)
return (Decimal(amount) * rate).quantize(quantum, rounding=ROUND_HALF_UP)
print(convert('100', 'USD', 'EUR')) # 85.12
print(convert('100', 'USD', 'JPY', 0)) # yen has no minor unit
Remember that string rate? This is why it’s nice. Decimal('0.851223') is exact. float('0.851223') isn’t, and binary float errors are exactly the kind of bug that shows up as a one-cent discrepancy in an invoice six months from now.
Need the reverse direction (EUR to USD)? The response includes an inverseRate field, but it’s gated to paid plans. On the free tier, either make a second call with the currencies swapped or compute 1 / rate yourself.
What to Get Right Once This Is in an App
The converter works. Here’s what separates a demo from something you’d put in front of customers.
Round per currency, not with a hardcoded 2. JPY and KRW have no minor unit. A few currencies (KWD, BHD) use three decimal places. The places argument above exists for this reason; drive it from a lookup table, not a guess.
Store the rate you used alongside the result. If you convert an order total, save amount, currency, rate, and a timestamp. Rates move. When someone asks why their receipt says 85.12 and today’s conversion says something else, the stored rate is your answer. Recomputing later won’t give you the same number.
Never add amounts across currencies. $100 + €100 is meaningless. Keep money as (amount, currency) pairs and convert explicitly before any arithmetic.
Decide whether you’re converting for display or for charging. Showing a German visitor ”≈ €85” on a USD price is a display conversion. Call the API, format it, label it approximate. Actually charging in euros is a different problem: you set real prices per currency, and your payment processor settles them. Don’t let an API-converted number become the amount you bill unless you’ve decided that’s your pricing policy. We go deeper on this in multi-currency e-commerce.
Cache per currency pair. A pricing page that renders 50 products in EUR needs one USD→EUR rate, not 50 calls. How long to cache is your call based on how much drift you’ll tolerate. The trade-offs are in currency conversion best practices.
Format with locale awareness at the edge. 85.12 becomes 85,12 € in Germany. In Python, Babel (format_currency) handles this so you don’t hand-roll separators.
Alternative: Let the API Do the Multiplication
If you only need converted amounts and don’t care about the rate itself, the Currency Converter API takes the amount directly and returns the result:
response = requests.get(
'https://api.apiverve.com/v1/currencyconverter',
params={'value': 100, 'from': 'USD', 'to': 'EUR'},
headers={'x-api-key': API_KEY},
timeout=10,
)
data = response.json()['data']
print(data['convertedValue'], data['rate'])
Note the different parameter names: value, from, to instead of currency1 and currency2. The response gives you convertedValue and rate as numbers rather than strings, so wrap them with Decimal(str(...)) if you’re doing further money math. The repo sticks with Exchange Rate because keeping the rate separate makes caching and storing it straightforward.
Next Steps
- Clone the full working code: github.com/apiverve/currency-converter-python-tutorial
- Browse supported currency codes on the Exchange Rate API page
- New to calling APIs from Python? Start with the Python quickstart or the quickstart docs
- Get a free API key and run
python converter.py 100 USD EUR - Sanity-check your CLI’s output against the Currency converter on DigestYourFinances, which converts at live rates
From there, wrap convert_currency in a Flask route or add a TTL cache. The function doesn’t change either way.