Build a Currency Converter in Python with an API

Convert currency in Python with the Exchange Rate API: one GET request, a working CLI converter, error handling, and a repo you can clone. Free key to start.

  • 8 min read
  • Published February 23, 2026
  • Updated September 13, 2026
Article featured image

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 means usd and USD behave 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 same status / error / data envelope, so checking status == 'ok' works across all of them.
  • ValueError covers 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

From there, wrap convert_currency in a Flask route or add a TTL cache. The function doesn’t change either way.

Give your software a way to act on the world.

Scaling in production?

The same key runs from your first prototype to millions of calls — on APIVerve's rails, backed by an uptime SLA.

See pricing