Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 8 of 8

Time to combine everything: fetch rates from a public API, cache them, validate input, handle every failure mode, and convert.

The real version

This is the code to run on your own machine. It uses exchangerate.host, which needs no API key.

import json
import os
import time

import requests

API_URL = "https://api.exchangerate.host/latest"
CACHE_FILE = "rates.json"
CACHE_MAX_AGE = 3600


class ConversionError(Exception):
    """Raised when a conversion cannot be completed."""


def cache_is_fresh(path=CACHE_FILE, max_age=CACHE_MAX_AGE):
    if not os.path.exists(path):
        return False
    return (time.time() - os.path.getmtime(path)) < max_age


def fetch_rates(base="USD"):
    """Fetch current rates, raising ConversionError on any failure."""
    try:
        response = requests.get(API_URL, params={"base": base}, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.exceptions.Timeout:
        raise ConversionError("The rate service timed out")
    except requests.exceptions.ConnectionError:
        raise ConversionError("Could not reach the rate service")
    except requests.exceptions.HTTPError as e:
        raise ConversionError(f"Rate service error: {e.response.status_code}")
    except json.JSONDecodeError:
        raise ConversionError("Rate service returned invalid data")

    if "rates" not in data:
        raise ConversionError("Unexpected response shape")
    return data["rates"]


def get_rates(base="USD"):
    """Return rates from cache when fresh, otherwise fetch and cache."""
    if cache_is_fresh():
        with open(CACHE_FILE) as f:
            return json.load(f)

    rates = fetch_rates(base)

    tmp = CACHE_FILE + ".tmp"
    with open(tmp, "w") as f:
        json.dump(rates, f)
    os.replace(tmp, CACHE_FILE)

    return rates


def convert(amount, to_currency, rates):
    """Convert amount into to_currency using rates."""
    if to_currency not in rates:
        raise ConversionError(f"Unknown currency: {to_currency}")
    return amount * rates[to_currency]


def main():
    try:
        rates = get_rates()
    except ConversionError as e:
        print(f"Could not load rates: {e}")
        return

    while True:
        raw = input("\nAmount in USD (or 'quit'): ")
        if raw == "quit":
            break

        try:
            amount = float(raw)
        except ValueError:
            print("That is not a number.")
            continue

        if amount <= 0:
            print("Amount must be positive.")
            continue

        currency = input("Convert to (e.g. EUR): ").upper()

        try:
            result = convert(amount, currency, rates)
        except ConversionError as e:
            print(e)
        else:
            print(f"{amount:.2f} USD = {result:.2f} {currency}")


if __name__ == "__main__":
    main()

Every piece is something you've learned: a custom exception (6.8), try/except/else (6.6), atomic writes and caching (8.7), specific network exceptions (8.5), and the if __name__ == "__main__" guard (7.2).

Note that every requests failure is translated into one ConversionError. The menu loop doesn't need to know anything about HTTP — that's the boundary between layers doing its job.

Try it yourself

Same logic, with the network call simulated so it runs here.

Take it further

  • Support any base currency, not just USD, with params={"base": base}.
  • Add a --list option that prints available currency codes.
  • Record every conversion to a history file with append_record from lesson 8.7.
  • Add the retry-with-backoff logic from lesson 8.5 around fetch_rates.
  • Read an API key from os.environ if you switch to a service that needs one.

A different project

If a weather app appeals more, Open-Meteo is free and needs no key. The structure is identical: fetch, validate, cache, handle errors, present.

Module 8 complete

You can now make GET and POST requests, parse and reshape JSON, send parameters and headers, keep API keys out of your source code, handle timeouts and connection failures, scrape HTML when there's no API, and build a caching pipeline that writes safely to disk.

Your programs can talk to the rest of the internet.

Next up: Module 9 — Intermediate Python & Best Practices, where you'll learn generators, decorators, regular expressions, and how to test everything you've built.

Questions about this lesson

Log in to ask a question.