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.
pippipavailable.
Install Dependencies
pip install requests beautifulsoup4pip install requests beautifulsoup4Part 1 — The Scraper Version
Getting started
- Create folder
CurrencyConverterCurrencyConverter. - Inside, create
currencyconverter.pycurrencyconverter.py.
Write the code
Currency Converter
Source# 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
# 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
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 INRC:\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 INRStep-by-Step Explanation
1. Imports
import requests
from bs4 import BeautifulSoupimport requests
from bs4 import BeautifulSouprequestsrequestsfetches the page.BeautifulSoupBeautifulSoupparses the returned HTML.
2. Build the URL
url = "https://www.x-rates.com/calculator/?from=%s&to=%s&amount=%s"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:
url = f"https://www.x-rates.com/calculator/?from={src}&to={dst}&amount={amt}"url = f"https://www.x-rates.com/calculator/?from={src}&to={dst}&amount={amt}"3. Collect input
from_currency = input("From Currency: ").upper().strip()
to_currency = input("To Currency: ").upper().strip()
amount = input("Amount: ").strip()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
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}")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=10prevents 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.textextracts 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
NoneNoneeven 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.
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")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:
{ "base": "USD", "rates": { "INR": 83.10 } }{ "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:
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())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:
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 rateimport 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
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.")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
| Problem | Cause | Fix |
|---|---|---|
AttributeError: NoneTypeAttributeError: NoneType | Scraper selector outdated | Switch to JSON API |
JSONDecodeErrorJSONDecodeError | Body was HTML, not JSON | Confirm endpoint URL; print r.textr.text to debug |
| Same rate cached forever | No TTL | Add expiry to disk cache |
| Hangs on slow network | No timeouttimeout | Pass timeout=10timeout=10 to every requests.getrequests.get |
100,000 USD100,000 USD rejected | Comma in number | Strip non-digit characters before float()float() |
Variations to Try
1. CLI mode with argparseargparse
python currencyconverter.py 100 --from USD --to INRpython 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))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:
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}")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:
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()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,HTTPErrorHTTPErrormean 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.
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
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 coffeeWas this page helpful?
Let us know how we did
