QVeris
Python Currency API Guide

Free Exchange Rate API Python
Guide with Working Code

Use a free exchange rate API in Python to fetch currency data, convert amounts, validate responses, and build a reliable integration.

Whiteboard workflow showing a Python app requesting free exchange rates, parsing JSON, converting currencies, and caching results

Free exchange rate API Python: the practical path

Pick by data needs

Compare currency coverage, update frequency, historical depth, licensing, authentication, and free-tier limits.

Make a safe request

Use a timeout, call raise_for_status(), validate JSON fields, and handle unknown currency codes.

Respect freshness

Reference rates may update daily rather than in real time. Store the provider timestamp with every value.

Cache deliberately

Match cache lifetime to the source update schedule and keep a last-known-good response for resilience.

How to choose a free currency exchange rate API

“Free” can mean an open public endpoint or a limited free plan. Before using any provider, verify whether an API key is required, how often rates refresh, which currencies and dates are available, and whether the terms permit your intended use.

CriterionWhat to verifyWhy it matters
AuthenticationNo key, free key, or paid keyChanges setup and secret management
FreshnessDaily, hourly, or intradayReference data is not a trading quote
CoverageCurrencies, history, base selectionPrevents missing pairs later
LimitsQuota, rate limit, attribution, licenseDetermines production suitability

Free exchange rate API without an API key

A no-key endpoint is convenient for a prototype, but it still needs timeouts, caching, respectful request volume, and a fallback plan. Frankfurter documents a public, open-source API with no API key and daily central-bank-derived rates.

Public API, free tier, or paid market feed?

Choose a public reference-rate API for prototypes, reporting, analytics, and low-frequency price localization. A registered free tier may offer broader currency coverage or more frequent updates, but introduces key rotation and quota monitoring. Trading, settlement, compliance, or customer-facing quotes may require a licensed market-data feed with explicit service levels.

Questions to answer before integration

  • Does the source publish reference, mid-market, buy, sell, or executable rates?
  • Which timezone defines the response date, and how are weekends handled?
  • Can the base currency change, or must cross-rates be calculated locally?
  • Are attribution, caching, redistribution, or commercial-use restrictions present?

Get live exchange rates in Python with requests

The example uses a documented single-pair endpoint. “Latest” means the provider's latest published reference rate; inspect the returned date and provider information instead of assuming tick-by-tick market data.

from dataclasses import dataclass
from datetime import date
from decimal import Decimal, InvalidOperation

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


@dataclass(frozen=True)
class RateObservation:
    observed_on: date
    base: str
    quote: str
    rate: Decimal


def build_session() -> requests.Session:
    retry = Retry(
        total=4,
        connect=3,
        read=3,
        status=3,
        backoff_factor=0.5,
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=frozenset({"GET"}),
        respect_retry_after_header=True,
        raise_on_status=False,
    )
    session = requests.Session()
    session.mount("https://", HTTPAdapter(max_retries=retry))
    return session


def get_rate(
    session: requests.Session,
    base: str = "USD",
    quote: str = "EUR",
) -> RateObservation:
    base, quote = base.upper(), quote.upper()
    if not (len(base) == len(quote) == 3 and base.isalpha() and quote.isalpha()):
        raise ValueError("Currency codes must contain three letters")

    url = f"https://api.frankfurter.dev/v2/rate/{base}/{quote}"
    response = session.get(url, timeout=(3.05, 10))
    response.raise_for_status()
    data = response.json()

    required = {"date", "base", "quote", "rate"}
    if not required <= data.keys():
        raise ValueError("Unexpected exchange-rate response")
    if data["base"] != base or data["quote"] != quote:
        raise ValueError("Provider returned a different currency pair")

    try:
        observed_on = date.fromisoformat(data["date"])
        value = Decimal(str(data["rate"]))
    except (TypeError, ValueError, InvalidOperation) as exc:
        raise ValueError("Invalid date or exchange rate") from exc
    if not value.is_finite() or value <= 0:
        raise ValueError("Exchange rate must be a positive finite number")

    return RateObservation(observed_on, base, quote, value)


session = build_session()
rate = get_rate(session, "USD", "EUR")
print(rate.observed_on, rate.rate)

Handle timeouts, HTTP errors, and malformed JSON

Catch requests.Timeout, requests.HTTPError, and requests.JSONDecodeError separately. Log the provider, pair, status, and timestamp without exposing credentials. Retry only transient failures with backoff.

Convert currency amounts in Python

For a single-pair response, multiply the amount by the returned rate and use Decimal with an explicit rounding rule when money is displayed or stored. Keep the original rate, timestamp, and source for auditability.

from decimal import Decimal, ROUND_HALF_UP

def convert_amount(amount, rate, minor_units=2):
    value = Decimal(str(amount)) * Decimal(str(rate))
    quantum = Decimal("1").scaleb(-minor_units)
    return value.quantize(quantum, rounding=ROUND_HALF_UP)

converted = convert_amount("125.50", rate.rate)
print(f"125.50 USD = {converted} EUR")

Use a session, retry only transient failures

The shared requests.Session reuses connections. Its adapter retries connection and read failures, HTTP 429, and selected 5xx responses with bounded exponential backoff, while respect_retry_after_header=True follows server-supplied waiting guidance. Invalid currency codes and other deterministic 4xx responses are not retried. Keep attempts bounded so a dependency problem cannot consume every application worker.

Exchange-rate direction, cross rates, and money rounding

Define every rate as units of quote currency per one unit of base currency. If USD/EUR = 0.92, one U.S. dollar converts to 0.92 euros; the inverse EUR/USD is 1 / 0.92, not 0.92. Preserve the provider's stated pair and never infer direction from a field name such as rate alone.

Worked example: derive a cross rate with Decimal.

Assume one source publishes USD/EUR = 0.9200 and USD/JPY = 150.00 at the same observation time. Because both use USD as the base, EUR/JPY = 150.00 / 0.9200 = 163.043478…. Converting EUR 1,250 gives JPY 203,804.347… before currency-specific rounding. The rate may be stored at higher precision, while a payable amount can be rounded separately under the application's monetary rule. Do not round each intermediate leg or mix rates from different dates.

Reference is not executable

A central-bank or daily midpoint can support accounting and reporting, but it is not a guaranteed bid or ask. A payment or trading quote may also include spread, fees, timing and venue.

Observation date is not request date

A weekend request may return Friday's published rate. Store requested date, source observation date, publication time and retrieval time separately; never relabel a carried rate as a new observation.

Rounding belongs to the amount

Keep calculations in Decimal and quantize the final monetary amount using the currency and business rule. Preserve the unrounded rate and calculation trace for reconciliation.

Fetch historical exchange rates with Python

Historical endpoints commonly accept a date or date range. Validate ISO currency codes and dates before the request, then decide how weekends, holidays, missing observations, and provider revisions should be represented in your application.

Cache rates without serving stale data silently

Store the source date next to the value, set cache expiry from the provider's publication cadence, and surface stale status to callers. For production, monitor failures and keep a last-known-good value with a clear age.

Build a time series without hidden gaps

Keep the provider date as the observation key rather than replacing it with the request time. Store missing dates explicitly or document forward-fill rules. When comparing providers, normalize base currency, timezone, decimal precision, and holiday calendars before calculating returns or averages.

Production checklist for a Python exchange rate integration

A successful HTTP 200 response is only the first check. Production code should verify schema, currency pair, source date, numeric range, cache age, and provider identity before a rate reaches pricing or reporting logic.

Validate every response

Reject missing fields, non-numeric rates, unexpected base or quote currencies, impossible values, and timestamps that move backwards.

Separate fetch from conversion

Keep network access, normalization, business conversion, rounding, and presentation in separate functions so each can be tested.

Use a typed internal rate record

Normalize provider JSON into a dataclass or validated model containing base, quote, Decimal rate, observation date, retrieval time, source, and stale status. Downstream code should not depend on a vendor's raw field names.

Observe freshness and failures

Track request latency, status classes, quota headers, cache hits, source age, retries, and fallback usage.

Design a bounded fallback

Use a circuit breaker after repeated failures and define whether callers receive a clearly aged last-known-good rate or an unavailable result. Never switch providers without recording the new source and rate semantics.

Protect credentials

Load API keys from a secret manager or environment, never source control, logs, browser code, or exception messages.

Test cases worth automating

  • Valid latest and historical responses with fixed fixtures.
  • Timeout, connection reset, 429, 500, malformed JSON, and missing fields.
  • Unsupported currency codes, same-currency conversion, zero and negative amounts.
  • Weekend dates, stale cache, provider date changes, and rounding for different minor units.

How QVeris helps Python apps find exchange rate capabilities

QVeris helps developers and agents discover and inspect available capabilities before calling them. Use the QVeris tool details to evaluate relevant financial-data tools, then verify each provider's documentation, terms, freshness, and limits.

  • Discover exchange-rate and currency-data capabilities by task.
  • Inspect inputs and outputs before integrating a provider.
  • Keep provider claims separate from application-level reliability checks.

Free exchange rate API Python FAQ

Is there a free exchange rate API for Python?

Yes. Python can call public or freemium REST APIs. Compare keys, quotas, freshness, currency coverage, history, and license before choosing.

How do I get exchange rates in Python?

Send a timed GET request, check HTTP status, parse JSON, validate its fields, and cache the response according to the source schedule.

Can I get historical exchange rates for free?

Some providers offer free historical daily rates. Date range, currencies, attribution, and quota vary, so check current documentation.

Is a free currency API real time?

Not necessarily. Many free sources publish daily reference rates. Read the timestamp and update policy instead of relying on a “live” label.

Authoritative references