- Published on
Python Currency Converter
- Authors
- Name
- Anthony Corletti
- @anthonycorletti
Most internet services are also financial services. From recurring subscription payments, investment banking, issuing transfers and payouts to bank accounts, consumer retail, forex exchanges, bitcoin, payroll services; take your pick, there is plenty of opportunity to work with finance in tech.
Building these services properly and securely should not be a challenge. No one should end up with something like $100.000000000000000034
showing up on their bank statement.
In this post, I'm going to show you how to build a minimal financial service with the use of python, Babel.numbers
, FastAPI, and Rates API.
To follow this post as written, using python 3.7+ is suggested.
To illustrate why it's never a good idea to use float point arithmetic for working with financial data in python, let's look at a simple example.
>>> float('0.1') + float('0.2')0.30000000000000004
No bueno.
On paper, we'd say that 0.1 + 0.2 = 0.3
. So now what? How do we get 0.3
?!
>>> from decimal import Decimal>>> r = Decimal('0.1') + Decimal('0.2')Decimal('0.3')
Voila! We'll get to formatting the figures later.
Let's start by modeling out our system and its features.
Create the following files in a directory so that you have a project structure that looks like the following tree:
.├── main.py├── requirements.txt└── schemas.py
Let's add everything we'll need to our requirements.txt
Babel==2.8.0fastapi==0.47.1pydantic==1.5.1requests==2.22.0uvicorn==0.11.2
Let's start writing main.py
which will serve as our main router and point of function for this example. For larger apps, you should use fastapi's APIRouter
module for defining your routes and a neat application structure for structuring the actions that take place in each route. Not to mention a database connection, I'm saving that for a later post.
from fastapi import FastAPI
from schemas import Payment
api = FastAPI()
@api.get('/health')def health(): return
@api.post('/payment', response_model=Payment, tags=['payment'])def create_payment(payment: Payment): return
Nice, nothing fancy here. We're just going to be able to create a payment and see what was returned. It looks like we've defined some pydantic schemas in our main.py
so let's write those.
Open up your schemas.py
and drop this in.
from decimal import Decimalfrom typing import Optional
import babel.numbers as bnfrom pydantic import UUID4, BaseModel, validator
CURRENCIES = bn.list_currencies()CURRENCY_VALUE_ERROR = ValueError( "Invalid currency code submitted. " f"Value must be one of the following {', '.join(CURRENCIES)}.")
class Payment(BaseModel): message: str sender_id: UUID4 sender_currency: str sender_amount: Decimal sender_amount_formatted: Optional[str] receiver_id: UUID4 receiver_currency: str receiver_amount_formatted: Optional[str] receiver_amount: Optional[Decimal]
@validator('sender_amount') def positive_sender_amount(cls: BaseModel, v: Decimal) -> Decimal: if v <= 0: raise ValueError('Amount must be a positive number.') return v
@validator('receiver_amount') def positive_receiver_amount(cls: BaseModel, v: Decimal) -> Decimal: if v and v <= 0: raise ValueError('Amount must be a positive number.') return v
@validator('receiver_currency') def valid_receiver_currency(cls: BaseModel, v: str) -> str: if v not in CURRENCIES: raise CURRENCY_VALUE_ERROR return v
@validator('sender_currency') def valid_sender_currency(cls: BaseModel, v: str) -> str: if v not in CURRENCIES: raise CURRENCY_VALUE_ERROR return v
Simple enough!
Awesome! With our schema written, we can build our currency converter in main.py
.
from decimal import Decimal, getcontext
import babel.numbers as bnimport requestsfrom fastapi import FastAPI
from schemas import Payment
api = FastAPI()
def get_rate(sender_currency: str, receiver_currency: str) -> str: result = requests.get( "https://api.ratesapi.io/api/latest?" f"base={sender_currency}&symbols={receiver_currency}") return str(result.json().get('rates').get(receiver_currency))
def calculate_receiver_amount(payment: Payment) -> Decimal: rate = get_rate(payment.sender_currency, payment.receiver_currency) # we can set and alter decimal precision setting however we want # https://docs.python.org/3.7/library/decimal.html#module-decimal getcontext().prec = 4 return Decimal(rate) * Decimal(payment.sender_amount)
def payment_with_conversion(payment: Payment) -> Payment: payment.receiver_amount = calculate_receiver_amount(payment) return payment
def formatted_payment(payment: Payment) -> Payment: payment.sender_amount_formatted = bn.format_currency( payment.sender_amount, payment.sender_currency) payment.receiver_amount_formatted = bn.format_currency( payment.receiver_amount, payment.receiver_currency) return payment
@api.get('/health')def health(): return
@api.post('/payment', response_model=Payment, tags=['payment'])def create_payment(payment: Payment): return formatted_payment(payment_with_conversion(payment))
Let's give it a shot! Run the following commands to install requirements and start-up your service.
pip3 install -r requirements.txtuvicorn main:api --reload
You should see the following.
$ uvicorn main:api --reloadINFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)INFO: Started reloader process [55640]INFO: Started server process [55651]INFO: Waiting for application startup.INFO: Application startup complete.
And now you can kick off a sample request with the following command. You may need to install jq
.
$ curl -s -X POST "localhost:8000/payment" -d '{ "sender_id": "12892e99-b4c6-4f6b-b433-01996c1d8ec0", "receiver_id": "c0626613-60d5-4d7a-9f12-8865febcfa9f", "sender_amount": "20", "sender_currency": "USD", "receiver_currency": "EUR", "message": "Cheers 🍻"}' | jq
{ "message": "Cheers 🍻", "sender_id": "12892e99-b4c6-4f6b-b433-01996c1d8ec0", "sender_currency": "USD", "sender_amount": 20, "sender_amount_formatted": "$20.00", "receiver_id": "c0626613-60d5-4d7a-9f12-8865febcfa9f", "receiver_currency": "EUR", "receiver_amount_formatted": "€18.28", "receiver_amount": 18.28}
Awesome! We have a simple service that does basic currency conversions. For this to be production ready, you'd want to implement a higher fidelity currency rate exchange service (i.e. something that gives real-time exchange rates), gunicorn
for running workers and threads, a datastore for managing transactions and other payments, and some actual infrastructure to run it all.
Want to issue payments and connect bank accounts? Lots of the details are handled for you by stripe – would totally suggest checking them out. For other cool finance software, there's also a bunch of stuff on github you should check out.
I'll be covering more on that eventually but hope this covers the basis for creating a stateless API that works with financial data reasonably well.