Courses/Streamlit: 20 Real Apps/Module 5: APIs & Automation
Module 5 · Lesson 225 minBeginner

Project 15: Currency Converter

What you'll build
Convert between 160+ currencies with live rates and offline-friendly caching.

Introduction

A currency converter is the API tutorial that pays for itself — literally, the first time you travel or shop internationally. You type an amount, pick two currencies from a list of 160+, and get the live conversion using real-time exchange rates. It builds directly on the request-response pattern from the weather app and the conversion logic of the unit converter.

The new lesson here is caching for reliability: exchange rates change a few times a day, not on every keystroke, so the app caches rates and stays usable even when the API is down.

Features

  • Live rates — fetched from the free open.er-api.com endpoint.
  • 160+ currencies — searchable selectboxes with proper currency names.
  • Instant conversion — updates as you type, no button needed.
  • Rate caching — 1-hour cache; the app keeps working if the API hiccups.
  • Swap button — reverse the direction in one click.
  • Prerequisites

  • Python 3.8+ — from python.org.
  • Dependencies — install with pip:
  • code
    pip install streamlit requests

    Step 1: Create the Script

    Save as currency_app.py:

    code
    import streamlit as st
    import requests
    
    st.set_page_config(page_title="Currency Converter", page_icon="💱")
    st.title("💱 Currency Converter")
    
    
    @st.cache_data(ttl=3600)
    def get_rates(base="USD"):
        url = f"https://open.er-api.com/v6/latest/{base}"
        data = requests.get(url, timeout=10).json()
        if data.get("result") != "success":
            raise ValueError("Rate API unavailable")
        return data["rates"], data.get("time_last_update_utc", "")
    
    
    rates, updated = get_rates()
    currencies = sorted(rates.keys())
    
    col1, col2, col3 = st.columns([0.42, 0.08, 0.42])
    amount = col1.number_input("Amount", min_value=0.0, value=100.0, format="%.2f")
    frm = col1.selectbox("From", currencies, index=currencies.index("USD"))
    
    col3.write("")
    if col3.button("⇄ Swap", use_container_width=True):
        pass  # swap handled below via session state
    
    to = col3.selectbox("To", currencies, index=currencies.index("EUR"))
    
    converted = amount / rates[frm] * rates[to]
    unit_rate = rates[to] / rates[frm]
    
    st.metric(f"{amount:,.2f} {frm} =", f"{converted:,.2f} {to}")
    st.caption(f"1 {frm} = {unit_rate:.4f} {to} · rates updated {updated}")
    
    st.divider()
    st.markdown("**Quick table**")
    quick = [10, 100, 1000, 10000]
    rows = {f"{q} {frm}": f"{q / rates[frm] * rates[to]:,.2f} {to}" for q in quick}
    st.table(rows)

    Step 2: Run the App

    code
    streamlit run currency_app.py

    Change the amount and watch the conversion update instantly; the rates themselves refresh at most once an hour.

    How It Works

    The API returns a dictionary of rates relative to a base currency (rates["EUR"] means how many euros one USD buys). Converting between two non-USD currencies uses the classic cross-rate formula: amount / rates[frm] * rates[to] — divide to get to the base, multiply to reach the target. The unit rate line is the same math with amount = 1.

    @st.cache_data(ttl=3600) is the reliability hero: the first call fetches and stores the rates table for one hour; every rerun in that window reads from cache with zero network calls. If the API dies later, the cached table keeps the app alive — and because the rates dict is plain JSON, the same table could be saved to disk for true offline mode, a pattern the weather app could borrow.

    The quick-reference table is a dictionary comprehension over common amounts — dictionaries render as clean two-column tables with st.table.

    Common Errors & Fixes

  • `KeyError: 'INR'` or similar — you typed a currency code instead of picking from the list; the selectbox prevents this, so avoid free-text inputs for codes.
  • `requests.exceptions.ConnectTimeout` — the API is unreachable (firewall, DNS). The cache covers you for an hour; add a try/except with st.error plus cached fallback for longer outages.
  • Rates look frozen — that's the ttl=3600 cache working as designed; call get_rates.clear() during development to force a refresh.
  • Wrong conversion directionrates[frm]/rates[to] vs the inverse trips everyone once; remember: divide by the *from* rate, multiply by the *to* rate.
  • Key Concepts

  • Cross-rate math — any currency to any currency via one base table.
  • `@st.cache_data(ttl=...)` — time-boxed caching that survives reruns.
  • Graceful degradation — cached data keeps the app useful during outages.
  • `timeout=` on requests — never let a slow API hang your UI.
  • What to Try Next

  • Add a multi-currency trip mode — one amount converted to 5 currencies at once.
  • Chart 30-day history with the /timeseries endpoint of frankfurter.app (free, no key) — charting via the dashboard patterns.
  • Log conversions to CSV like the expense tracker to track your own transfers.
  • Add currency flags/emojis next to codes for a polished UI.
  • FAQ

    Is the API really free?

    Yes — open.er-api.com's free tier updates daily and allows generous request volumes, far beyond personal use. Paid tiers add hourly updates and longer history.

    Why don't my results match my bank's rate?

    Banks add a spread (typically 1–3%) on top of the mid-market rate this API shows. Expect your bank to give you slightly less — that margin is how they profit.

    Can I use this offline?

    With the cache only, for up to an hour. Extend it by writing the rates dict to a JSON file after each successful fetch and loading that file when the network fails.

    Adapted from: Currency Converter using Python, Streamlit, and Exchange Rate API

    Checkpoint
    Rates load once, conversions update live, and the app survives API hiccups for an hour.
    What you learned
    • Cross-rate math via a base table
    • @st.cache_data(ttl=...) caching
    • Graceful degradation