Skip to content

Currency Converter

Abstract

A currency converter is the smallest “real” web-aware project you can build. The original version in this tutorial scrapes a public rates calculator with requestsrequests + BeautifulSoupBeautifulSoup. That works, but scraping is brittle: a single layout change on the site breaks everything. So we will start with the scraper, then refactor to use a proper JSON API (no key required), then add caching, batch conversion, and a CLI mode.

You will leave comfortable with:

  • HTTP requests with requestsrequests.
  • HTML parsing with BeautifulSoup — and why a JSON API is better when one exists.
  • Caching responses so you do not re-hit the network for the same query.
  • Robust error handling for offline / 4xx / 5xx scenarios.
  • Building a multi-mode tool (interactive, CLI flags, library).

Prerequisites

  • Python 3.6 or above.
  • A code editor or IDE.
  • Internet connection.
  • pippip available.

Install Dependencies

install
pip install requests beautifulsoup4
install
pip install requests beautifulsoup4

Part 1 — The Scraper Version

Getting started

  1. Create folder CurrencyConverterCurrencyConverter.
  2. Inside, create currencyconverter.pycurrencyconverter.py.

Write the code

Currency ConverterSource
Currency Converter
# Currency Converter
 
# Importing the required modules
import requests
from bs4 import BeautifulSoup
 
# URL
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%s"
 
# Getting the user input
print("Currency Converter")
print('''
      List of Currencies:
        1. USD - US Dollar
        2. EUR - Euro
        3. GBP - British Pound
        4. INR - Indian Rupee
        5. AUD - Australian Dollar
        6. CAD - Canadian Dollar
        7. SGD - Singapore Dollar
        8. CHF - Swiss Franc
        9. MYR - Malaysian Ringgit
        10. JPY - Japanese Yen
        11. CNY - Chinese Yuan Renminbi
        12. NZD - New Zealand Dollar
        13. THB - Thai Baht
        14. HUF - Hungarian Forint
        15. AED - Emirati Dirham
        16. HKD - Hong Kong Dollar
        17. MXN - Mexican Peso
        18. ZAR - South African Rand
        19. PHP - Philippine Peso
        20. SEK - Swedish Krona
        
        Don't Enter the Number. Enter the currency code.
    ''')
from_currency = input("From Currency: ").upper()
to_currency = input("To Currency: ").upper()
amount = input("Amount: ")
 
# Requesting the URL
response = requests.get(url % (from_currency, to_currency, amount))
soup = BeautifulSoup(response.text, "html.parser")
 
# Finding the converted amount
converted_amount = soup.find("span", class_="ccOutputRslt").text
 
# Printing the converted amount
print(f'{amount} {from_currency} = {converted_amount}')
Currency Converter
# Currency Converter
 
# Importing the required modules
import requests
from bs4 import BeautifulSoup
 
# URL
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%s"
 
# Getting the user input
print("Currency Converter")
print('''
      List of Currencies:
        1. USD - US Dollar
        2. EUR - Euro
        3. GBP - British Pound
        4. INR - Indian Rupee
        5. AUD - Australian Dollar
        6. CAD - Canadian Dollar
        7. SGD - Singapore Dollar
        8. CHF - Swiss Franc
        9. MYR - Malaysian Ringgit
        10. JPY - Japanese Yen
        11. CNY - Chinese Yuan Renminbi
        12. NZD - New Zealand Dollar
        13. THB - Thai Baht
        14. HUF - Hungarian Forint
        15. AED - Emirati Dirham
        16. HKD - Hong Kong Dollar
        17. MXN - Mexican Peso
        18. ZAR - South African Rand
        19. PHP - Philippine Peso
        20. SEK - Swedish Krona
        
        Don't Enter the Number. Enter the currency code.
    ''')
from_currency = input("From Currency: ").upper()
to_currency = input("To Currency: ").upper()
amount = input("Amount: ")
 
# Requesting the URL
response = requests.get(url % (from_currency, to_currency, amount))
soup = BeautifulSoup(response.text, "html.parser")
 
# Finding the converted amount
converted_amount = soup.find("span", class_="ccOutputRslt").text
 
# Printing the converted amount
print(f'{amount} {from_currency} = {converted_amount}')

Run it

command
C:\Users\Your Name\CurrencyConverter> python currencyconverter.py
Currency Converter
 
      List of Currencies:
        1. USD - US Dollar
        2. EUR - Euro
        ...
 
From Currency: USD
To Currency: INR
Amount: 100
100 USD = 8,309.928657 INR
command
C:\Users\Your Name\CurrencyConverter> python currencyconverter.py
Currency Converter
 
      List of Currencies:
        1. USD - US Dollar
        2. EUR - Euro
        ...
 
From Currency: USD
To Currency: INR
Amount: 100
100 USD = 8,309.928657 INR

Step-by-Step Explanation

1. Imports

currencyconverter.py
import requests
from bs4 import BeautifulSoup
currencyconverter.py
import requests
from bs4 import BeautifulSoup
  • requestsrequests fetches the page.
  • BeautifulSoupBeautifulSoup parses the returned HTML.

2. Build the URL

currencyconverter.py
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%s"
currencyconverter.py
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%s"

%s%s placeholders get filled by the printf-style %% operator on the next call. F-strings are nicer:

fstring.py
url = f"https://www.x-rates.com/calculator/?from={src}&to={dst}&amount={amt}"
fstring.py
url = f"https://www.x-rates.com/calculator/?from={src}&to={dst}&amount={amt}"

3. Collect input

currencyconverter.py
from_currency = input("From Currency: ").upper().strip()
to_currency   = input("To Currency: ").upper().strip()
amount        = input("Amount: ").strip()
currencyconverter.py
from_currency = input("From Currency: ").upper().strip()
to_currency   = input("To Currency: ").upper().strip()
amount        = input("Amount: ").strip()

.upper().upper() normalizes case; .strip().strip() removes accidental whitespace.

4. Fetch and parse

currencyconverter.py
response = requests.get(url % (from_currency, to_currency, amount), timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
result = soup.find("span", class_="ccOutputRslt").text
print(f"{amount} {from_currency} = {result}")
currencyconverter.py
response = requests.get(url % (from_currency, to_currency, amount), timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
result = soup.find("span", class_="ccOutputRslt").text
print(f"{amount} {from_currency} = {result}")
  • timeout=10timeout=10 prevents the program freezing if the site hangs.
  • soup.find("span", class_="ccOutputRslt")soup.find("span", class_="ccOutputRslt") locates the result span by its CSS class.
  • .text.text extracts the rendered text content.

Why Scraping Is Fragile

The selector class_="ccOutputRslt"class_="ccOutputRslt" is what breaks first. The instant the site rewrites that markup, your script returns AttributeError: 'NoneType' object has no attribute 'text'AttributeError: 'NoneType' object has no attribute 'text'. Symptoms:

  • Result is NoneNone even though the page loads in a browser.
  • HTTP 200 but no matching element.
  • Layout shifts to a <div><div> or different class.

Lesson: prefer a proper JSON API whenever one exists.

Use exchangerate.host — free, no key, JSON, stable interface.

api_version.py
import requests
 
API = "https://api.exchangerate.host/latest"
 
def convert(amount: float, src: str, dst: str) -> float:
    r = requests.get(API, params={"base": src, "symbols": dst}, timeout=10)
    r.raise_for_status()
    rate = r.json()["rates"][dst]
    return amount * rate
 
print(f"{convert(100, 'USD', 'INR'):.2f} INR")
api_version.py
import requests
 
API = "https://api.exchangerate.host/latest"
 
def convert(amount: float, src: str, dst: str) -> float:
    r = requests.get(API, params={"base": src, "symbols": dst}, timeout=10)
    r.raise_for_status()
    rate = r.json()["rates"][dst]
    return amount * rate
 
print(f"{convert(100, 'USD', 'INR'):.2f} INR")
  • One call, structured response, no HTML parsing.
  • r.raise_for_status()r.raise_for_status() raises an exception on 4xx/5xx.
  • r.json()r.json() decodes the body into a Python dict.

JSON looks like:

response.json
{ "base": "USD", "rates": { "INR": 83.10 } }
response.json
{ "base": "USD", "rates": { "INR": 83.10 } }

The same site supports historical rates (/2024-01-15/2024-01-15) and time series (/timeseries?.../timeseries?...).

Add Caching

Hitting the network every time is slow and wasteful — exchange rates do not change every second. Cache by source/target/date:

cache.py
from functools import lru_cache
from datetime import date
 
@lru_cache(maxsize=128)
def rate(src: str, dst: str, day: date) -> float:
    r = requests.get(f"{API}/{day.isoformat()}",
                     params={"base": src, "symbols": dst}, timeout=10)
    r.raise_for_status()
    return r.json()["rates"][dst]
 
def convert(amount, src, dst):
    return amount * rate(src.upper(), dst.upper(), date.today())
cache.py
from functools import lru_cache
from datetime import date
 
@lru_cache(maxsize=128)
def rate(src: str, dst: str, day: date) -> float:
    r = requests.get(f"{API}/{day.isoformat()}",
                     params={"base": src, "symbols": dst}, timeout=10)
    r.raise_for_status()
    return r.json()["rates"][dst]
 
def convert(amount, src, dst):
    return amount * rate(src.upper(), dst.upper(), date.today())

@lru_cache@lru_cache memoizes results — same arguments → no second network call.

For long-running scripts that survive restarts, write a JSON file:

disk_cache.py
import json, time, pathlib
CACHE = pathlib.Path("rates_cache.json")
TTL = 60 * 60                                  # 1 hour
def get_rate(src, dst):
    data = json.loads(CACHE.read_text()) if CACHE.exists() else {}
    key = f"{src}_{dst}"
    if key in data and time.time() - data[key]["ts"] < TTL:
        return data[key]["rate"]
    rate = fetch_live(src, dst)
    data[key] = {"rate": rate, "ts": time.time()}
    CACHE.write_text(json.dumps(data))
    return rate
disk_cache.py
import json, time, pathlib
CACHE = pathlib.Path("rates_cache.json")
TTL = 60 * 60                                  # 1 hour
def get_rate(src, dst):
    data = json.loads(CACHE.read_text()) if CACHE.exists() else {}
    key = f"{src}_{dst}"
    if key in data and time.time() - data[key]["ts"] < TTL:
        return data[key]["rate"]
    rate = fetch_live(src, dst)
    data[key] = {"rate": rate, "ts": time.time()}
    CACHE.write_text(json.dumps(data))
    return rate

Robust Error Handling

A real tool handles every failure path with a friendly message:

errors.py
import requests
try:
    rate = fetch_rate("USD", "XYZ")
except requests.ConnectionError:
    print("No internet connection.")
except requests.Timeout:
    print("Request timed out — try again later.")
except requests.HTTPError as e:
    print(f"Server returned {e.response.status_code}.")
except KeyError:
    print("Unknown currency code.")
errors.py
import requests
try:
    rate = fetch_rate("USD", "XYZ")
except requests.ConnectionError:
    print("No internet connection.")
except requests.Timeout:
    print("Request timed out — try again later.")
except requests.HTTPError as e:
    print(f"Server returned {e.response.status_code}.")
except KeyError:
    print("Unknown currency code.")

Common Mistakes

ProblemCauseFix
AttributeError: NoneTypeAttributeError: NoneTypeScraper selector outdatedSwitch to JSON API
JSONDecodeErrorJSONDecodeErrorBody was HTML, not JSONConfirm endpoint URL; print r.textr.text to debug
Same rate cached foreverNo TTLAdd expiry to disk cache
Hangs on slow networkNo timeouttimeoutPass timeout=10timeout=10 to every requests.getrequests.get
100,000 USD100,000 USD rejectedComma in numberStrip non-digit characters before float()float()

Variations to Try

1. CLI mode with argparseargparse

cli
python currencyconverter.py 100 --from USD --to INR
cli
python currencyconverter.py 100 --from USD --to INR
cli.py
import argparse
p = argparse.ArgumentParser()
p.add_argument("amount", type=float)
p.add_argument("--from", dest="src", required=True)
p.add_argument("--to",   dest="dst", required=True)
args = p.parse_args()
print(convert(args.amount, args.src, args.dst))
cli.py
import argparse
p = argparse.ArgumentParser()
p.add_argument("amount", type=float)
p.add_argument("--from", dest="src", required=True)
p.add_argument("--to",   dest="dst", required=True)
args = p.parse_args()
print(convert(args.amount, args.src, args.dst))

2. Batch convert

Convert one amount into many currencies at once:

batch.py
TARGETS = ["EUR", "GBP", "INR", "JPY", "CAD"]
r = requests.get(API, params={"base": "USD", "symbols": ",".join(TARGETS)}).json()
for code, rate in r["rates"].items():
    print(f"100 USD = {100 * rate:.2f} {code}")
batch.py
TARGETS = ["EUR", "GBP", "INR", "JPY", "CAD"]
r = requests.get(API, params={"base": "USD", "symbols": ",".join(TARGETS)}).json()
for code, rate in r["rates"].items():
    print(f"100 USD = {100 * rate:.2f} {code}")

3. GUI version

See Currency Exchange Rate Calculator GUI for a Tkinter version with dropdowns.

4. Historical chart

Pull /timeseries/timeseries data and plot with matplotlibmatplotlib:

chart.py
import matplotlib.pyplot as plt
data = requests.get("https://api.exchangerate.host/timeseries",
                    params={"start_date":"2025-01-01","end_date":"2025-01-31",
                            "base":"USD","symbols":"INR"}).json()["rates"]
dates = sorted(data.keys())
rates = [data[d]["INR"] for d in dates]
plt.plot(dates, rates); plt.xticks(rotation=45); plt.tight_layout(); plt.show()
chart.py
import matplotlib.pyplot as plt
data = requests.get("https://api.exchangerate.host/timeseries",
                    params={"start_date":"2025-01-01","end_date":"2025-01-31",
                            "base":"USD","symbols":"INR"}).json()["rates"]
dates = sorted(data.keys())
rates = [data[d]["INR"] for d in dates]
plt.plot(dates, rates); plt.xticks(rotation=45); plt.tight_layout(); plt.show()

5. Crypto support

Use CoinGecko’s free API for BTC, ETH, etc.

6. Web dashboard

Wrap with Flask (see Basic Web Server). Form on a page; POST returns the conversion.

7. Live ticker

Print the rate every 60 seconds; highlight changes with color.

8. Telegram bot

Use python-telegram-botpython-telegram-bot so a chat command 100 USD INR100 USD INR returns the result.

Real-World Applications

  • Travel apps that show prices in your home currency.
  • E-commerce checkout flows for international customers.
  • Finance dashboards that consolidate P&L in a base currency.
  • Cryptocurrency portfolio trackers.
  • Anything where users type money and need a quick second opinion.

Best Practices Demonstrated

  • API over scraping when a stable API exists.
  • Always set a timeout on network calls.
  • Cache by key + TTL to be a polite client.
  • Catch network errors specifically — ConnectionErrorConnectionError, TimeoutTimeout, HTTPErrorHTTPError mean different things.
  • Validate currency codes before sending — fail fast on typos.

Educational Value

  • HTTP fundamentals and JSON parsing.
  • Web scraping pitfalls and when to avoid it.
  • Caching strategies and TTL design.
  • Building a multi-mode tool (interactive + CLI).
  • Defensive coding around the network.

Next Steps

  • Build the API version above and retire the scraper.
  • Add disk caching with TTL.
  • Wrap with argparse for scriptable use.
  • Plot 30-day history with matplotlib.
  • Combine with Currency Exchange Rate Calculator GUI for a desktop frontend.

Visualize it

Here’s how a conversion request travels from the input fields to the displayed result.

diagram Currency conversion flow mermaid
From user input to a converted, displayed amount.

Conclusion

You started by scraping a website, hit the brittleness wall, and rebuilt the same feature against a proper JSON API with caching and clean error handling. The “API over scraping” instinct is one of the most valuable habits you can form. Full source on GitHub. Explore more on Python Central Hub.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did