Currency Converter
Abstract
Section titled “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 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).
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A code editor or IDE.
- Internet connection.
pipavailable.
Install Dependencies
Section titled “Install Dependencies”pip install requests beautifulsoup4Part 1 — The Scraper Version
Section titled “Part 1 — The Scraper Version”Getting started
Section titled “Getting started”- Create folder
CurrencyConverter. - Inside, create
currencyconverter.py.
Write the code
Section titled “Write the code”Currency Converter
pch.viewSource# 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}') Run it
Section titled “Run it”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 INRWhat it produces
Section titled “What it produces”Running the file exactly as it ships takes 1.9 s and prints:
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.
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Imports
Section titled “1. Imports”import requests
from bs4 import BeautifulSouprequestsfetches the page.BeautifulSoupparses the returned HTML.
2. Build the URL
Section titled “2. Build the URL”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:
url = f"https://www.x-rates.com/calculator/?from={src}&to={dst}&amount={amt}"3. Collect input
Section titled “3. Collect input”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.
4. Fetch and parse
Section titled “4. Fetch and parse”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=10prevents the program freezing if the site hangs.soup.find("span", class_="ccOutputRslt")locates the result span by its CSS class..textextracts the rendered text content.
Why Scraping Is Fragile
Section titled “Why Scraping Is Fragile”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
Noneeven 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.
Part 2 — The API Version (Recommended)
Section titled “Part 2 — The API Version (Recommended)”Use exchangerate.host — free, no key, JSON, stable interface.
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:
{ "base": "USD", "rates": { "INR": 83.10 } }The same site supports historical rates (/2024-01-15) and time series (/timeseries?...).
Add Caching
Section titled “Add Caching”Hitting the network every time is slow and wasteful — exchange rates do not change every second. Cache by source/target/date:
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:
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 rateRobust Error Handling
Section titled “Robust Error Handling”A real tool handles every failure path with a friendly message:
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
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
AttributeError: NoneType | Scraper selector outdated | Switch to JSON API |
JSONDecodeError | Body was HTML, not JSON | Confirm endpoint URL; print r.text to debug |
| Same rate cached forever | No TTL | Add expiry to disk cache |
| Hangs on slow network | No timeout | Pass timeout=10 to every requests.get |
100,000 USD rejected | Comma in number | Strip non-digit characters before float() |
Variations to Try
Section titled “Variations to Try”1. CLI mode with argparse
Section titled “1. CLI mode with argparse”python currencyconverter.py 100 --from USD --to INRimport 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
Section titled “2. Batch convert”Convert one amount into many currencies at once:
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
Section titled “3. GUI version”See Currency Exchange Rate Calculator GUI for a Tkinter version with dropdowns.
4. Historical chart
Section titled “4. Historical chart”Pull /timeseries data and plot with matplotlib:
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
Section titled “5. Crypto support”Use CoinGecko’s free API for BTC, ETH, etc.
6. Web dashboard
Section titled “6. Web dashboard”Wrap with Flask (see Basic Web Server). Form on a page; POST returns the conversion.
7. Live ticker
Section titled “7. Live ticker”Print the rate every 60 seconds; highlight changes with color.
8. Telegram bot
Section titled “8. Telegram bot”Use python-telegram-bot so a chat command 100 USD INR returns the result.
Real-World Applications
Section titled “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
Section titled “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 —
ConnectionError,Timeout,HTTPErrormean different things. - Validate currency codes before sending — fail fast on typos.
Educational Value
Section titled “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
Section titled “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
Section titled “Visualize it”Here’s how a conversion request travels from the input fields to the displayed result.
flowchart LR A["User enters amount + currencies"] --> B["Call the exchange-rate API"] B --> C["Receive rates"] C --> D["Compute converted amount"] D --> E["Display it"]
Conclusion
Section titled “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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading