Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →A currency converter is one of the most useful first Python projects because it forces you to use the core tools in a realistic way: storing values, reading user input, converting types, writing functions, making decisions, and handling bad data. Build it in two stages. First, write a command-line converter that uses a small fixed table of rates. Then replace that table with live data from an exchange-rate API that you request over HTTP and read as JSON. The fixed version teaches the logic; the API version teaches how real programs get information from other services.
What the project teaches you
Each part of a converter maps to a basic Python idea you will use in almost every program:
- Values and variables: the amount, the two currency codes, and the rate table are all values stored under names.
- User input:
input()returns text, so you must convert it before doing arithmetic. - Numeric conversion:
float()turns text such as"25.50"into a number and raises an error when the text is not numeric. - Functions: the calculation can live in its own function, separate from the code that prompts the user.
- Conditionals: checks such as “is this currency supported?” and “is this amount positive?” decide what the program does next.
- HTTP requests and JSON: in the second stage, your program asks a service for data and reads the structured reply.
- Error handling: bad input, unknown codes, and network failures each need a clear, predictable response.
Stage 1: A converter with fixed rates
Start without any network code. Store one rate for each currency, expressed as units of that currency per one US dollar. The numbers below are illustrative placeholders for the exercise, not current market rates, and they will go stale the moment you stop updating them by hand. That is acceptable at this stage because the goal is to understand the logic.
Step 1: Store the rates
RATES_PER_USD = {
"USD": 1.0,
"EUR": 0.92,
"GBP": 0.79,
"JPY": 149.50,
}
Each key is a currency code and each value is how many units of that currency equal one US dollar. Because every rate uses the same base, you can convert between any two listed currencies by going through dollars.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors#1 Best Overall
Step 2: Write the calculation as a function
def convert(amount, source, target, rates):
amount_in_usd = amount / rates[source]
return amount_in_usd * rates[target]
This function does no input and no printing. You can test it on its own: convert(100, "USD", "EUR", RATES_PER_USD) should return 92.0 with the table above. Keeping calculation separate from input and output is the habit that makes later changes, such as swapping in an API, far easier.
Step 3: Read and validate the amount
import math
def read_amount():
text = input("Amount: ").strip()
try:
amount = float(text)
except ValueError:
raise ValueError("Enter a number such as 25.50")
if not math.isfinite(amount) or amount <= 0:
raise ValueError("Amount must be a positive number")
return amount
The math.isfinite() check matters. float() accepts strings such as "nan" and "inf", which are valid floats but meaningless as money amounts.
Step 4: Normalize and check the currency codes
Users type usd, Usd, and USD interchangeably, so normalize with .strip().upper() before checking the table. Then confirm both codes exist in RATES_PER_USD. Checking membership with in is simpler and clearer than letting a KeyError crash the program.
Rank #2
Step 5: Connect the pieces
def main():
source = input("From currency code: ").strip().upper()
target = input("To currency code: ").strip().upper()
try:
amount = read_amount()
except ValueError as error:
print(error)
return
if source not in RATES_PER_USD or target not in RATES_PER_USD:
print("Unsupported code. Choose from:", ", ".join(RATES_PER_USD))
return
result = convert(amount, source, target, RATES_PER_USD)
print(f"{amount:,.2f} {source} = {result:,.2f} {target}")
if __name__ == "__main__":
main()
Run the file and try a valid conversion, a negative amount, the text abc, and an unknown code such as XYZ. Each bad input should produce a readable message and no traceback. If one of them crashes, find which check is missing.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchStage 2: Replacing the table with an exchange-rate API
A fixed table cannot tell you today’s rate. To fetch rates from a provider, your program sends an HTTP request, checks the response, and reads the data it needs. Two things are worth knowing before you start.
- Some providers need no account or key for basic requests. Frankfurter’s Python guide shows a plain
requestscall without an SDK or API key. - Other providers require a free account and API key. ExchangeRate-API’s Python guide describes a GET request and states that you need an account to get a key. If you use a key, keep it out of the source file you share, for example by reading it from an environment variable.
Step 1: Install requests and choose an endpoint
Install the library with pip install requests. Then copy the endpoint address from the provider’s own Python guide. Use the address as published there and confirm the current terms and address on the provider’s site, because endpoints and plan rules change.
Step 2: Request and check the response
import requests
def fetch_rates(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as error:
raise ValueError(f"Could not reach the rate service: {error}")
data = response.json()
rates = data.get("rates")
if not rates:
raise ValueError("The service response did not include a rates table")
return data, rates
Three checks happen here. The timeout keeps the program from waiting forever. raise_for_status() turns HTTP error codes into an exception you can handle. The rates check confirms the response has the structure your code expects. The key name "rates" is shown for illustration; use the name that appears in the sample response from the provider you choose, because providers name their fields differently.
Step 3: Reuse the same conversion function
Because you kept convert() separate, the only change needed in main() is where the table comes from. Replace RATES_PER_USD with the rates returned by fetch_rates(), and keep the same validation. Confirm the response states its base currency. If the base is listed in the table with a rate of 1.0, the dollar-based formula still works. If the base is something else, the formula still works as long as the base currency appears in the table.
Step 4: Show the rate date
Live rates are only meaningful with a date. Display the date or last-updated field from the response alongside the result, using the field name from that provider’s sample. currencyapi’s conversion example, for instance, includes a last-updated timestamp in its sample response, and showing it helps the reader judge how fresh the number is.
What an API rate does and does not tell you
The number from a provider is a reference rate. It is not a price anyone will give you. Keep these points in mind before you present the converter as anything more than a learning project.
- Refresh frequency varies. Frankfurter says its latest blended rates change as providers publish them, at most a few times per working day. currencyapi’s documentation, as it described its own update frequencies at the time of writing, ranges from daily to minutely depending on the product. Check the provider’s page for the frequency that applies to your endpoint.
- Official reference rates can lag. A pinned rate from an official source follows that source’s publication schedule and may differ from a blended latest rate on the same day.
- Your bank or card issuer sets its own rate. Banks, exchange counters, and card networks apply their own rates, spreads, and fees. A converter that shows a reference rate will not match the amount you are charged.
Frankfurter’s guide recommends caching the latest rates for a short period and allows longer caching for historical rates pinned to a specific date. Caching is a sensible later extension, because it reduces repeated requests for data that changes slowly.
Money arithmetic: why floats and Decimal differ
Binary floating-point numbers cannot represent many decimal fractions exactly. In Python, 0.1 + 0.2 gives 0.30000000000000004. For a display-only lesson, rounding the output with an f-string is fine. For anything involving money, Frankfurter’s Python guide recommends parsing rates with Decimal from the standard library, because floats are acceptable for display but not for accounting.
Best Value
from decimal import Decimal
rate = Decimal("0.92")
amount = Decimal("100.00")
print(amount * rate) # 92.0000
Build the Stage 1 version with float so the logic stays simple. When you move to the API version, switch to Decimal for parsing and arithmetic, and convert to a string only at the output step. This is a useful habit to learn even though a learning project is not accounting software.
Common failures and how to handle them
Each row below is a failure you should expect to see while testing. The response in the right-hand column is the behavior the code should produce.
| Failure | Typical symptom | Handling |
|---|---|---|
Amount is text, such as abc |
float() raises ValueError |
Catch the error and ask the user for a number |
Amount is zero, negative, nan, or inf |
Calculation runs but the result is meaningless | Reject with math.isfinite() and amount <= 0 checks |
| Unknown or misspelled currency code | Dictionary lookup raises KeyError |
Check membership first and list supported codes |
| Currency code is valid but the provider does not supply it | Lookup fails after a successful request | Check the target code against the response’s rates and report it as unavailable |
| Network failure or timeout | requests raises a RequestException |
Catch it, print a plain message, and suggest trying again later |
| HTTP error status | raise_for_status() raises an exception |
Report that the service returned an error rather than showing a traceback |
| Unexpected JSON structure | Missing key returns None or fails later |
Confirm the expected field exists before using it |
Comparing the providers mentioned in this tutorial
You do not need to choose a provider to finish the lesson, but you will need one for Stage 2. The table compares what the providers’ own documentation states at the time of writing. Plan limits and terms change, so read the current page before you build against them.
| Question | Frankfurter | ExchangeRate-API | currencyapi |
|---|---|---|---|
| API key or account needed? | Not needed for the Python example in its guide | Free account and API key required, per its Python guide | Not stated in the pages reviewed for this article |
| Rate source and schedule | Blended latest rates that change at most a few times per working day, per its guide | Not stated in the pages reviewed for this article | Update frequency ranges from daily to minutely, per its documentation |
| Historical rates | Pinned historical rates supported, per its guide | Not stated in the pages reviewed for this article | Not stated in the pages reviewed for this article |
| Conversion method | Reader multiplies the amount by the returned rate | Reader makes a GET request and reads the rate | Direct requests or an SDK; its documentation says the conversion endpoint is not available on the free plan |
| Request limits and cost tiers | Not stated in the pages reviewed for this article | Not stated in the pages reviewed for this article | Plan details as described on its site; check current terms |
Whichever provider you choose, read its terms for request limits, attribution requirements, and acceptable use before building anything you plan to share.
Optional extensions after the command-line version works
Add these only after both versions run cleanly. Each one builds on the functions you already have.
- Caching: store the last response with its timestamp and reuse it for a short period, following the provider’s caching guidance.
- Conversion history: append each successful conversion to a list, then print or save it to a file.
- A graphical interface: Tkinter, which ships with most Python installers, can wrap the same
convert()andfetch_rates()functions. Keep the calculation code unchanged so the interface stays thin.
The core lesson of the project is that the calculation, the data source, and the interface are three separate concerns. Keeping them separate lets you change any one without rewriting the others.
Quick Recap
“
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




