Introduction
In this article, I'm going to show you how you can build a currency converter GUI app using Python and customTKinter.
The goal of the app is very simple: take a value, along with a currency to convert from and to and then show how much the converted currency is worth against the source currency.
YouTube Video
If you would prefer to see a video of this article, there is a video version available on YouTube below:
Source Code
If you would like a copy of the source code, it is available here on GitHub.
Requirements
To build this app, you will need to have Python installed (I used version 3.12), along with TKinter, which may or may not be installed with the version of Python you have. Also, an IDE or text editor of your choice.
The last thing you'll need is an API key from https://freecurrencyapi.com. I opted for this site as their free-tier was quite good and offered what I needed (at the time of writing). I was not sponsored by them; I just found them when I was looking for a free API to use.
Building the App Modules
To begin, you'll need to create a new virtual environment for the app. If you aren't familiar with Python virtual environments, I have an article that goes over them here.
To create a virtual environment, run:
For macOS / Linux:
python -m venv venv
For Windows:
py -m venv venv
This will create a new virtual environment called venv.
Next, activate the virtual environment:
For macOS / Linux:
source ./venv/bin/activate
For Windows (make sure your PowerShell script execution policy allows for running scripts):
.\venv\Scripts\activate
With that setup, it's time to install some packages. To do that, simply run:
pip install customtkinter freecurrencyapi pillow python-dotenv
Next, create the following folders and files:
.
├── images
│ ├── header.png
│ └── icon.png
├── modules
│ ├── currency
│ │ ├── __init__.py
│ │ ├── convert.py
│ │ └── currency_list.py
│ ├── env
│ │ ├── __init__.py
│ │ ├── .env
│ │ └── env_vars.py
│ └── image_check
│ ├── __init__.py
│ └── image_check.py
├── main.py
└── requirements.txt
With that done, let's take a quick look at the folder structure:
- images: This has an image for the apps icon and the header of the app.
- modules: This has some custom modules that have been built for the app. I'll go over these when we get to the code.
- venv: This is the virtual environment.
Before any code can be added, download the two images that stored in here on GitHub. One is an icon for the app and the second is a header for the top section of the app where the title till be shown.
Now let's go through the code for the app, starting with the modules/env
folder.
First, open the .env
file, paste in the below and save it.
API_KEY="change-me"
This contains a single environment variable that will be used to store the API key from freecurrencyapi. You just need to put it in between the parenthesis and save it.
Next open the env_vars.py
file. Paste in the below code and save it.
# --- Import the required libraries / modules:
from dotenv import load_dotenv
from tkinter import messagebox
import os
def load_env_vars() -> None:
"""_summary_
This function is used to load the environment variables that are stored in the .env file
in the same folder as this file.
Returns:
None: Nothing is returned.
"""
# --- Load environment variables from .env file:
try:
# load_dotenv(dotenv_path = f"{BASE_DIR}/modules/env/.env", verbose=False)
load_dotenv(dotenv_path = f"{os.path.abspath(os.path.join(os.path.dirname(__file__)))}/.env",
verbose=False)
except OSError as error:
message = "Could not locate the .env file. Please check that the file is present."
messagebox.showerror("Error", message)
raise Exception(message)
# --- Check to see if one of the environment variables is present. If not, raise an exception:
if os.getenv("API_KEY") == None:
message = "No environment variables were loaded. Please check the .env file exists."
messagebox.showerror("Error", message)
raise Exception(message)
This has a single function that will load the environment variable stored in the .env
file and check that it loaded as expected, meaning if the value of the environment variable named API_KEY
is anything but None
, it is done. Also, if the .env
file cannot be found, it will also raise an error in the form of a message box.
Next, open the image_check.py
in the modules/image_check
folder. Paste in the below code and save it.
# --- Import the required libraries / modules:
from PIL import Image, ImageTk
from tkinter import messagebox
def check_image_present(image_path: str, error_message: str) -> ImageTk:
"""_summary_
This function will check if a file is present in the specified directory.
Args:
image_path (str): The full path and file name to check.
error_message (str): The error that needs to be displayed.
Raises:
Exception: Raise an exception if the file is not present.
Returns:
ImageTk: The image.
"""
try:
image_file = Image.open(image_path)
except FileNotFoundError as error:
messagebox.showerror("Error", f"{error}.\n\n{error_message}")
raise Exception(error)
return image_file
This again has a single function which will check to see if an image is present and if so, it will load the image and return the image back to what called it. If it cannot be found, it will raise a message box with an error.
Now onto the final module, that being the modules/currency
folder, which contains
two files.
First open the currency_list.py
file. Paste in the below and save the file.
currencies: list = [
'EUR - Euro (€)',
'USD - US Dollar ($)',
'GBP - British Pound Sterling (£)',
'JPY - Japanese Yen (¥)',
'CNY - Chinese Yuan (CN¥)',
'HKD - Hong Kong Dollar (HK$)',
'CAD - Canadian Dollar (CA$)',
'AUD - Australian Dollar (AU$)',
'BGN - Bulgarian Lev (BGN)',
'CZK - Czech Republic Koruna (Kč)',
'DKK - Danish Krone (Dkr)',
'HUF - Hungarian Forint (Ft)',
'PLN - Polish Zloty (zł)',
'RON - Romanian Leu (RON)',
'SEK - Swedish Krona (Skr)',
'CHF - Swiss Franc (CHF)',
'ISK - Icelandic Króna (Ikr)',
'NOK - Norwegian Krone (Nkr)',
'HRK - Croatian Kuna (kn)',
'RUB - Russian Ruble (RUB)',
'TRY - Turkish Lira (TL)',
'BRL - Brazilian Real (R$)',
'IDR - Indonesian Rupiah (Rp)',
'ILS - Israeli New Sheqel (₪)',
'INR - Indian Rupee (Rs)',
'KRW - South Korean Won (₩)',
'MXN - Mexican Peso (MX$)',
'MYR - Malaysian Ringgit (RM)',
'NZD - New Zealand Dollar (NZ$)',
'PHP - Philippine Peso (₱)',
'SGD - Singapore Dollar (S$)',
'THB - Thai Baht (฿)',
'ZAR - South African Rand (R)'
]
This is a single Python list variable that contains a collection of currencies. This will be used to build the combo boxes that I'll cover later on.
Now, open the convert.py
file. Paste in the below code and save the file.
# --- Import the required libraries / modules:
from decimal import *
from tkinter import messagebox
import freecurrencyapi
import os
import re
def check_value_is_numeric(value: str) -> bool | object:
"""_summary_
This function takes a string value and attempts to convert it to a Decimal value.
Args:
value (str): The value that needs to be converted from a string to a Decimal.
Returns:
bool | object: If the value can be converted, it will return it as an object (Decimal). If not, it will return a bool.
"""
# --- Check if the value is a string:
if not isinstance(value, str):
return False
# --- Check if the value has a value:
if not value:
return False
# --- Check for mathematical symbols
if re.search(r'[+\-*/]', value):
return False
# --- Attempt to convert the value to a decimal number:
try:
convert_value = float(value)
except ValueError:
return False
# --- Check if the value is over 0:
if convert_value <= 0:
return False
# --- Return the converted value if all checks pass:
return Decimal(convert_value).quantize(Decimal('.0001'), rounding=ROUND_DOWN)
def get_exchange_rate(from_currency: str, to_currency: str) -> object:
"""_summary_
This function will get the current exchange rate for 1 of the from_currency and how much it will be in the to_currency.
Args:
from_currency (str): the currency to convert from.
to_currency (str): The currency to convert to.
Raises:
Exception: Raise an exception if either currency is not valid.
Returns:
object: Returns a Decimal object with the value of the converted currency.
"""
client = freecurrencyapi.Client(api_key = os.getenv("API_KEY"))
try:
result = client.latest(base_currency = from_currency, currencies = [to_currency])
except:
messagebox.showerror("Error", "Please check the currencies list as one of the symbols was not recognised. Also, check to see if the API key is entered correctly.")
raise Exception
return Decimal(result["data"][to_currency]).quantize(Decimal('.0001'), rounding=ROUND_DOWN)
def convert_currency(from_currency: str, to_currency: str, value: object) -> object:
"""_summary_
This function will convert a value from one currency to another and return the converted value.
Args:
from_currency (str): The currency code to convert from.
to_currency (str): The currency code to convert to.
value (float, optional): The value to convert from.
Returns:
object: The converted value as a Decimal.
"""
from_currency_code = from_currency.split(sep = " ")[0]
to_currency_code = to_currency.split(sep = " ")[0]
exchange_rate = get_exchange_rate(from_currency = from_currency_code, to_currency = to_currency_code)
return Decimal(value * exchange_rate).quantize(Decimal('.0001'), rounding=ROUND_DOWN)
There are three functions. The first one, check_value_is_numeric
will take a string, check if the value contains any mathematical operators and then attempt to convert the string to a float. If either of these fails, it will return False
to the caller. More on that later. If all goes well, the returned value will be a Decimal object with the value to convert in it.
The next function, get_exchange_rate
will take the currency to convert from and to, attempt to call the API for a unit of 1 for the from currency and get the converted amount back. If it fails, a message box will appear. If the value is numeric, it will be converted to a float and then to a Decimal object that is returned to the caller.
The last function, convert_currency
will take the value to convert, along with the currency to convert from and to. It will then take the first three digits of the from and to currencies and set them as variables to be used. The next part will then call the get_exchange_rate
function to set the exchange_rate variable. Lastly, it will return the converted amount, which is set to four decimal places and rounded down.
Main File
With the modules covered, the last file for the app is the main.py
file at the root of the app. This will tie it all together and build the user interface.
Open the main.py
and paste in the below code and then save it.
# --- Import the required libraries / modules:
from customtkinter import *
from PIL import ImageTk
from tkinter import messagebox
from modules.env.env_vars import load_env_vars
from modules.currency.currency_list import currencies
from modules.currency.convert import convert_currency, check_value_is_numeric
from modules.image_check.image_check import check_image_present
import os
def main() -> None:
"""_summary_
This is the main function for the application
"""
# --- Load the environment variables:
load_env_vars()
# --- Get the absolute path for main.py:
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__)))
# --- Check to see if the icon image is present and apply it if so:
icon_check = check_image_present(image_path = f"{BASE_DIR}/images/icon.png",
error_message = f"Check that the icon file exists in that directory with the name icon.png.\n\nPress Ok to close.")
header_check = check_image_present(image_path = f"{BASE_DIR}/images/header.png",
error_message = f"Check that the header image file exists in that directory with the name icon.png.\n\nPress Ok to close.")
# --- Set the name and defaults for the app:
app_name: str = "Currency Converter"
default_font_size: int = 24
# --- Setup the application and window:
app = CTk()
app.title(app_name)
app.geometry(geometry_string = "450x840")
app.resizable(width = False, height = False)
# --- Set the icon for the app:
icon = ImageTk.PhotoImage(icon_check)
app.wm_iconphoto(True, icon)
# --- Set icon for Windows 10 / 11:
app.wm_iconbitmap()
# --- Define the variables to store the combo boxes values:
from_currency: StringVar = StringVar()
to_currency: StringVar = StringVar()
# --- Build the UI --- #
# --- 1. The header for the app with the name:
header_image = CTkImage(light_image = header_check,
dark_image = header_check,
size = (450, 160))
header_label = CTkLabel(master = app, width = 450, height = 160,
text = app_name, font=("", 38),image = header_image,
text_color = "white")
header_label.grid(row = 0, column = 0)
# --- 2. The frame and entry box for the value to convert:
value_frame = CTkFrame(master = app, width = 400, height = 150)
value_frame.grid(row = 1, column = 0, padx = 10, pady = 10, sticky = "nesw")
value_label = CTkLabel(text = "Convert:", master = value_frame, width = 413,
font=("", default_font_size), anchor = "w")
value_label.grid(row = 0, column = 0, padx = 10, pady = 10)
value_entry = CTkEntry(master = value_frame, width = 413,
placeholder_text = "Enter a number to convert",
font=("", default_font_size), justify = CENTER)
value_entry.grid(row = 1, column = 0, padx = 10, pady = 5)
# --- 3. The frame and combo box for the currency to convert from:
convert_from_frame = CTkFrame(master = app)
convert_from_frame.grid(row = 2, column = 0, padx = 10, pady = 10, sticky = "nesw")
from_currency_label = CTkLabel(text = "From:", master = convert_from_frame, width = 413,
font=("", default_font_size), anchor = "w")
from_currency_label.grid(row = 0, column = 0, padx = 10, pady = 10)
from_currency_code = CTkComboBox(master = convert_from_frame, width = 413, values = currencies,
font=("", default_font_size), justify = LEFT,
variable = from_currency, state = "readonly")
from_currency_code.grid(row = 1, column = 0, padx = 10, pady = 5)
from_currency_code.set(currencies[0])
# --- 4. The frame and combo box for the currency to convert to:
convert_to_frame = CTkFrame(master = app)
convert_to_frame.grid(row = 3, column = 0, padx = 10, pady = 10, sticky = "nesw")
to_currency_label = CTkLabel(text = "To:", master = convert_to_frame, width = 413,
font=("", default_font_size), anchor = "w")
to_currency_label.grid(row = 0, column = 0, padx = 10, pady = 10)
to_currency_code = CTkComboBox(master = convert_to_frame, width = 413, values = currencies,
font=("", default_font_size), justify = LEFT,
variable = to_currency, state = "readonly")
to_currency_code.grid(row = 1, column = 0, padx = 10, pady = 5)
to_currency_code.set(currencies[1])
# --- 5. The frame and label for the converted amount to be shown in. No value initially is shown:
to_currency_value = CTkLabel(master = app, width = 413, height = 260,
text = "The converted amount\nwill be shown here.",
font=("", default_font_size), justify = CENTER)
to_currency_value.grid(row = 4, column = 0, padx = 10, pady = 5)
# --- 6a. The function that is used by the convert button to perform the currency conversion:
def convert_currency_button_command() -> None:
"""_summary_
This function will check that the value to convert is valid (numeric and > 0). If so, it will then convert it.
Raises:
Exception: If a boolean is returned, an error message will be shown indicating that the value entered is not valid.
"""
# --- 1. Check that the from and to currencies are not the same:
if from_currency.get() == to_currency.get():
messagebox.showinfo("Info", f"Both the from and to currencies are the same. Please specify a different one for one of the two currencies.")
raise Exception()
# --- 2. Check that the value is numeric:
convert_from_value = check_value_is_numeric(value = value_entry.get())
if convert_from_value == False:
messagebox.showerror("Error", f"Please check the value you entered is a number and it is higher than 0.")
raise Exception()
# --- 3. If so, perform conversion:
converted_value = convert_currency(from_currency = str(from_currency.get()),
to_currency = str(to_currency.get()),
value = convert_from_value)
# --- 4. Update the to_currency_value label with the original and converted currencies:
to_currency_value.configure(text = f'{convert_from_value} {str(from_currency.get()).split(sep = " ")[0]}\n=\n{converted_value} {str(to_currency.get()).split(sep = " ")[0]}')
# --- 6b. The button to execute the conversion and update the to_currency_value label:
convert_button = CTkButton(master = app, text = "Convert", font=("", default_font_size),
fg_color="green", hover_color=("#38B215"),
height = 50, command = convert_currency_button_command)
convert_button.grid(row = 5, column = 0, padx = 10, pady = 10, sticky = "nesw")
# --- Keep the window active until it is closed:
app.mainloop()
if __name__ == "__main__":
main()
To cover the main points:
- First, there are the libraries and modules that need to be imported
- Next, there is a
main
function that contains the code to bring the app together. This includes:- Loading the environment variables (
load_env_vars
) - Get the base directory for the
main.py
file - Check to see if the app icon image is present (
icon_check
). If not, a message box with an error will appear - Do the same again but for the header image (
header_check
). - After that, set some defaults for the app (
app_name
anddefault_font_size
) - Then, instantiate the app and set the window size. Also, set it so that it cannot be resized.
- Next, set the icon for the app. This will be shown in the dock or task bar.
- Then, define two variables that will store the from and to currencies.
- Now to build the user interface:
- First, there is the header image, along with a label to show the apps name
- Next, a frame, label and entry field that is used by the user to enter the amount they want to convert
- Then there is a similar layout for the from currency but instead of an entry field, there is a combo box. The combo box gets its values from the
currencies
Python list in thecurrencies_list
file that was imported - Below that, there is the same but for the to currency instead
- Next, there is a label that will show the amount from and to once a conversion is done. Otherwise, it will just show The converted amount will be shown here
- After that, there is a function that is used to perform a number of actions when the convert button is clicked. These are:
- Checking that the from and to currencies are not the same. If so, show a message box asking the user to change one
- Check if the value is numeric. If not, show a message box asking the user to change the value to a number
- Perform the conversion if all of the above are good
- Update the
to_currency_value
text to show the from and to currency values
- The last part of the UI is the convert button that will call the
convert_currency_button_command
when clicked - And lastly for the main function, keep the window open until the user closes the app
- Loading the environment variables (
- The last part of the
main.py
file will call the main function if the name ismain
.
With that all done, you can run the app by running:
For macOS / Linux:
python main.py
For Windows:
py main.py
Here is an example of what the app will look like:
And there we have it. A currency converter, made with customTKinter that uses an API to get the conversion rate and then presents the converted amount to the user.
I hope you found that to be of interest. Thanks for reading and have a nice day!