Write an amount in words in lakh and crore style with Python
In this article (5 sections)
Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).
To print an amount in words the Indian way, round the amount to paise first, split the rupees into crore, lakh, thousand and the last three digits, and name each part with a small table of words from one to ninety-nine. Paise are named separately and "Only" closes the phrase. The function below does this in about forty lines of standard Python, and ten test cases pin down the edge cases that usually go wrong: exact lakhs, zero rupees, rounding and amounts over 100 crore.
Why the usual libraries give the wrong answer
Generic number-to-words code usually follows the international system unless told otherwise: 1,23,456 becomes "one hundred twenty-three thousand four hundred fifty-six". That is correct English and wrong for an Indian invoice, cheque register, payment voucher or offer letter, where the reader expects "One Lakh Twenty-Three Thousand Four Hundred Fifty-Six". The difference is not cosmetic. A payment approval that states an amount in words exists so a reader can cross-check the figures, and the cross-check only works if the words follow the grouping the reader is used to.
How the grouping works
Indian grouping takes the last three digits as hundreds, then groups every two digits: thousand, lakh, crore. There is no widely used name above crore in business writing, so amounts of 100 crore and more are written as a number of crores, such as "One Thousand Two Hundred Fifty Crore".
| Figure | Crore | Lakh | Thousand | Last three |
|---|---|---|---|---|
| 1,23,456 | 0 | 1 | 23 | 456 |
| 10,00,001 | 0 | 10 | 0 | 1 |
| 2,50,75,000 | 2 | 50 | 75 | 0 |
| 1250,00,00,000 | 1250 | 0 | 0 | 0 |
That table is almost the whole algorithm. Each column except the first holds at most two digits, so a function that names numbers below one hundred covers them all, and the crore column simply calls the same function again on however many crores there are.
The function and its tests
All amounts are illustrative.
from decimal import Decimal, ROUND_HALF_UP
ONES = ("Zero One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve Thirteen "
"Fourteen Fifteen Sixteen Seventeen Eighteen Nineteen").split()
TENS = "_ _ Twenty Thirty Forty Fifty Sixty Seventy Eighty Ninety".split()
def below_100(n: int) -> str:
if n < 20:
return ONES[n]
t, o = divmod(n, 10)
return TENS[t] + ("-" + ONES[o] if o else "")
def below_1000(n: int) -> str:
h, rest = divmod(n, 100)
parts = [ONES[h] + " Hundred"] if h else []
if rest:
parts.append(below_100(rest))
return " ".join(parts)
def indian_words(n: int) -> str:
"""Whole number in words using crore, lakh, thousand, hundred."""
if n == 0:
return "Zero"
crore, n = divmod(n, 10_000_000)
lakh, n = divmod(n, 100_000)
thousand, n = divmod(n, 1_000)
parts = []
if crore:
parts.append(indian_words(crore) + " Crore") # 100 crore and above recurse
if lakh:
parts.append(below_100(lakh) + " Lakh")
if thousand:
parts.append(below_100(thousand) + " Thousand")
if n:
parts.append(below_1000(n))
return " ".join(parts)
def rupees_in_words(amount) -> str:
value = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
if value < 0:
raise ValueError("amount in words is for payable amounts; handle credit notes separately")
rupees, paise = divmod(int(value * 100), 100)
text = "Rupees " + indian_words(rupees)
if paise:
text += " and " + below_100(paise) + " Paise"
return text + " Only"
CASES = {
0.5: "Rupees Zero and Fifty Paise Only",
1: "Rupees One Only",
1000: "Rupees One Thousand Only",
100000: "Rupees One Lakh Only",
118000: "Rupees One Lakh Eighteen Thousand Only",
"123456.78": "Rupees One Lakh Twenty-Three Thousand Four Hundred Fifty-Six and Seventy-Eight Paise Only",
1000001: "Rupees Ten Lakh One Only",
"25075000.05": "Rupees Two Crore Fifty Lakh Seventy-Five Thousand and Five Paise Only",
12500000000: "Rupees One Thousand Two Hundred Fifty Crore Only",
"9.995": "Rupees Ten Only",
}
for amount, expected in CASES.items():
got = rupees_in_words(amount)
assert got == expected, (amount, got)
print(f"{str(amount):>14} {got}")
print(len(CASES), "cases pass")The tests are the part to keep even if you rewrite the function. Each one exists because a simpler version failed it:
- 0.5 needs "Zero" rupees. Code that skips zero parts prints "Rupees and Fifty Paise Only".
- 1,00,000 must not produce "One Lakh Zero Thousand Zero".
- 10,00,001 checks that empty middle groups vanish without leaving double spaces.
- 12,50,00,00,000 checks the recursion above 99 crore.
- 9.995 checks rounding. With
ROUND_HALF_UPon aDecimalit becomes ₹10.00; with binary floats, 9.995 is stored slightly below 9.995, and Python'sround(9.995, 2)returns 9.99, so the words and the figure disagree by a paisa.
The input is converted with Decimal(str(amount)) for the same reason. Pass amounts as strings or Decimal values straight from your source file where you can, not as floats that have already been through arithmetic.
Style choices to settle with the finance team
The output here capitalises each word, hyphenates compound numbers such as "Twenty-Three", uses "and" only before paise, and ends with "Only". Organisations differ on all four. Some write "Rupees One Lakh Only" and others "INR One Lakh Only"; some drop the hyphens; some write "Paise Seventy-Eight" rather than "Seventy-Eight Paise". None is more correct than the others, but the invoice template, the voucher and the cheque printing file should agree, so pick one and change the string constants once rather than editing output by hand.
Negative amounts raise an error on purpose. A credit note or refund needs its own wording, and printing "Rupees Minus Five Thousand" on a document is worse than failing loudly.
Using it on a whole register
Once the function is tested, applying it to a DataFrame column is one line, df["in_words"] = df["amount"].map(rupees_in_words), and the result can be written to the invoice or voucher export. For a control check, generate the words for every payment in a batch and have a second person read a sample of them against the figures, which is the check the words exist to support. The figures themselves should be displayed with Indian digit grouping too; the formatting function for that is in showing numbers in lakhs and crores in Excel, Power BI and Python.
If the words are needed inside Excel rather than in an export, the practical routes are a VBA function or a Microsoft 365 LAMBDA, each built on the same table of words and tested against the same ten cases. Whichever you choose, keep the test list in the workbook on its own sheet.
Small, fully tested utilities like this one are the building blocks of dependable reporting, and writing tests for business logic before trusting it is a habit we build throughout the Data Analyst programme.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI