Skip to content

Currency Converter

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 requests + BeautifulSoup. 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 requests.
  • 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).
  • Python 3.6 or above.
  • A code editor or IDE.
  • Internet connection.
  • pip available.
install
pip install requests beautifulsoup4
  1. Create folder CurrencyConverter.
  2. Inside, create currencyconverter.py.
Currency Converter pch.viewSource
Currency Converter
# Currency Converter

# Importing the required modules
import requests
from bs4 import BeautifulSoup


def ask(prompt="", default=""):
    """Read a line, or fall back to `default` when nobody is there to type.

    Without this the script raises EOFError the moment it runs unattended — in
    a test, a scheduled job, or the build that captures this output for the
    docs. The fallback is printed rather than silent, so a reader can always
    tell which answers were typed and which were assumed.
    """
    try:
        return input(prompt).strip() or default
    except EOFError:
        print(f"{default}   (no input available, using the default)")
        return default

# 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 = ask("From Currency: ", '1').upper()
to_currency = ask("To Currency: ", '1').upper()
amount = ask("Amount: ", '7')

# 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}')
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

Running the file exactly as it ships takes 1.9 s and prints:

python currencyconverter.py
Currency Converter
 
      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
...

The first 20 of 30 lines are shown; the run continues past this point.

currencyconverter.py
import requests
from bs4 import BeautifulSoup
  • requests fetches the page.
  • BeautifulSoup parses the returned HTML.
currencyconverter.py
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%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}"
currencyconverter.py
from_currency = input("From Currency: ").upper().strip()
to_currency   = input("To Currency: ").upper().strip()
amount        = input("Amount: ").strip()

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

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=10 prevents the program freezing if the site hangs.
  • soup.find("span", class_="ccOutputRslt") locates the result span by its CSS class.
  • .text extracts the rendered text content.

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

  • Result is None even though the page loads in a browser.
  • HTTP 200 but no matching element.
  • Layout shifts to a <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")
  • One call, structured response, no HTML parsing.
  • r.raise_for_status() raises an exception on 4xx/5xx.
  • r.json() decodes the body into a Python dict.

JSON looks like:

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

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

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())

@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

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.")
ProblemCauseFix
AttributeError: NoneTypeScraper selector outdatedSwitch to JSON API
JSONDecodeErrorBody was HTML, not JSONConfirm endpoint URL; print r.text to debug
Same rate cached foreverNo TTLAdd expiry to disk cache
Hangs on slow networkNo timeoutPass timeout=10 to every requests.get
100,000 USD rejectedComma in numberStrip non-digit characters before float()
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))

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}")

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

Pull /timeseries data and plot with matplotlib:

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()

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

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

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

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

  • 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.
  • 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 — ConnectionError, Timeout, HTTPError mean different things.
  • Validate currency codes before sending — fail fast on typos.
  • 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.
  • 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.

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.

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.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading