Why This Matters: An ATM That Never Crashes
You walk up to an ATM and type the wrong amount, say Rs 250, when the machine only has Rs 100 and Rs 500 notes. A badly made machine would freeze and keep your card. A good one shows a calm message, “Please enter a multiple of Rs 100”, and lets you try again.
Your Python programs face the same situation all the time. A user types letters instead of digits, a file is missing, a dictionary key does not exist. Without a plan, Python stops the whole program with a long red message.
In this lesson you will learn what exceptions are, how to read a traceback, and how to handle problems with try, except, else and finally. You will also raise your own errors, create a custom exception class and build an input loop that keeps asking until the user enters a valid number.
What Is an Exception?
An exception is Python's way of saying “I cannot continue with this line”. When it happens and nobody handles it, the program stops at once and prints a report. Press Try it yourself: the Input box already contains five hundred.
balance = 5000
amount_text = input("Enter amount: ")
amount = int(amount_text) # ERROR: ValueError
balance = balance - amount
print("Please take your cash") # never runs
Python cannot turn the text “five hundred” into a whole number, so int() raises a ValueError, and the last line never runs.
Reading a Traceback
The report Python prints is called a traceback. It looks something like this (newer Python versions also mark the exact spot with ^ characters):
Traceback (most recent call last):
File "atm.py", line 4, in <module>
amount = int(amount_text)
ValueError: invalid literal for int() with base 10: 'five hundred'
Read a traceback from the bottom up. The last line tells you what went wrong (the error type and message). The lines just above it tell you where: the file, the line number and the code on that line. In longer programs you will see several File lines, one for each function call that led to the error.
Common Exceptions You Will Meet
| Exception | What went wrong | Example that causes it |
|---|---|---|
ValueError | The type is right but the value is not | int("abc") |
ZeroDivisionError | Dividing by zero | 5000 / 0 |
KeyError | A dictionary key that does not exist | accounts["9999"] |
IndexError | A list position that does not exist | [100, 200][5] |
FileNotFoundError | Opening a missing file for reading | open("statement.txt") |
TypeError | A value of the wrong type for the job | "Rs " + 500 |
try and except: Catch the Problem
Put the line that might fail inside a try block. If an exception happens, Python skips the rest of the try block and runs the matching except block instead of stopping. After that, the program continues normally.
amount_text = input("Enter amount: ")
try:
amount = int(amount_text)
print(f"Withdrawing Rs {amount}") # skipped if int() fails
except ValueError:
print("Please enter the amount in digits, like 500.")
print("Thank you for banking with us.") # the program keeps going
Change the input to 500 and run it again. Now int() works, the except block is skipped, and both prints run.
Always name the exception you expect, as in except ValueError. You will see in Common Mistakes why a bare except: is dangerous.
Several except Blocks
One piece of code can fail in different ways, and each way deserves its own message. Add one except block per exception type. Python checks them from top to bottom and runs the first one that matches. Writing except ValueError as error also gives you the exception object, so you can print its message.
accounts = {"1001": 5000, "1002": 1200}
def split_balance(account, holders_text):
"""Show each holder's equal share of a joint account."""
try:
balance = accounts[account] # KeyError if the account is unknown
holders = int(holders_text) # ValueError if not a whole number
share = balance / holders # ZeroDivisionError if holders is 0
print(f"Account {account}: Rs {share:.2f} each")
except KeyError:
print(f"Account {account} was not found.")
except ValueError as error:
print("Holders must be a whole number. Details:", error)
except ZeroDivisionError:
print("A joint account needs at least 1 holder.")
split_balance("1001", "4") # Account 1001: Rs 1250.00 each
split_balance("9999", "2") # Account 9999 was not found.
split_balance("1002", "two") # Holders must be a whole number. Details: invalid literal ...
split_balance("1002", "0") # A joint account needs at least 1 holder.
If two exceptions need the same message, list them in brackets: except (KeyError, IndexError):.
else and finally
A try statement can have two more parts:
elseruns only when thetryblock finished without an exception. Put the “success” work here.finallyruns always, whether there was an exception or not. It is the place for clean-up, like returning the card.
def withdraw(balance, amount_text):
print("Card inserted")
try:
amount = int(amount_text)
except ValueError:
print("That is not a valid amount.")
else:
# Runs only if the try block had NO error
balance = balance - amount
print(f"Take your cash: Rs {amount}. Balance: Rs {balance}")
finally:
# Runs ALWAYS, error or not
print("Please take your card")
return balance
balance = withdraw(5000, "1500")
print()
balance = withdraw(balance, "1,500")
print()
print("Final balance:", balance)
# Card inserted
# Take your cash: Rs 1500. Balance: Rs 3500
# Please take your card
#
# Card inserted
# That is not a valid amount.
# Please take your card
#
# Final balance: 3500
Keep the try block small. Put only the line that can fail insidetry, and the code that should run after success inelse. Then anexcept ValueErrorcan only catch the error you expected, not a surprise from some other line.
raise: Report Your Own Errors
Python raises exceptions for its own problems. Your program has its own rules too, such as “only multiples of Rs 100”. When a rule is broken, use raise with a clear message. The function stops at once, and the caller decides how to show the problem.
def check_amount(amount, balance):
if amount <= 0:
raise ValueError("The amount must be more than Rs 0.")
if amount % 100 != 0:
raise ValueError("Please enter a multiple of Rs 100.")
if amount > balance:
raise ValueError(f"Not enough balance. You have Rs {balance}.")
print(f"Rs {amount}: OK")
for amount in [2000, 0, 450, 9000]:
try:
check_amount(amount, 5000)
except ValueError as error:
print(f"Rs {amount}: {error}")
# Rs 2000: OK
# Rs 0: The amount must be more than Rs 0.
# Rs 450: Please enter a multiple of Rs 100.
# Rs 9000: Not enough balance. You have Rs 5000.
Notice the split of jobs: check_amount only checks and raises. The loop outside catches the error and talks to the user. This keeps each part simple.
Custom Exception Classes, Briefly
Sometimes a built-in name like ValueError is too general. You can create your own exception type in two lines with class InsufficientBalanceError(Exception):. The words in brackets mean “this is a kind of Exception”. You will learn classes properly in the Classes and Objects lesson. For now, copy the pattern.
class InsufficientBalanceError(Exception):
"""Raised when the account does not have enough money."""
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalanceError(
f"You asked for Rs {amount}, but your balance is Rs {balance}."
)
return balance - amount
try:
new_balance = withdraw(3000, 4500)
except InsufficientBalanceError as error:
print("Sorry!", error)
# Sorry! You asked for Rs 4500, but your balance is Rs 3000.
print(isinstance(InsufficientBalanceError("x"), Exception)) # True
A custom name makes your code read like plain English: “except insufficient balance error”. It also lets you catch only your own problems, while real bugs still show up.
A Safe Input Loop
A real ATM does not give up after one wrong entry. It asks again. Combine while True, try/except and continue to keep asking until the value is valid, then return it. The Input box has three answers ready: abc, then -200, then 500.
def ask_amount():
while True:
text = input("Enter amount (multiple of 100): ")
try:
amount = int(text)
except ValueError:
print("Please type digits only, like 500.")
continue # ask again
if amount <= 0 or amount % 100 != 0:
print("Please enter a positive multiple of 100.")
continue
return amount # a good value: leave the loop
amount = ask_amount()
print("You asked for Rs", amount)
Real-Life Example: The Village Bank ATM
Here is the complete withdrawal screen. ATMError is a parent class for all problems the customer can fix, and the two specific errors are kinds of it. So one except ATMError catches both. read_amount() keeps asking until it gets digits, withdraw() raises an error for a wrong amount or low balance, else pays out, and finally always returns the card. The Input box tries four amounts: five hundred, 250, 9000 and finally 2500.
class ATMError(Exception):
"""Base class for problems the customer can fix."""
class InvalidAmountError(ATMError):
pass
class InsufficientBalanceError(ATMError):
pass
def read_amount():
while True:
text = input("Enter amount: ").strip()
try:
return int(text)
except ValueError:
print(" Please type the amount in digits, like 2000.")
def withdraw(balance, amount):
if amount <= 0 or amount % 100 != 0:
raise InvalidAmountError("This ATM gives notes of Rs 100 and above. Enter a multiple of 100.")
if amount > balance:
raise InsufficientBalanceError(f"Not enough balance. You have Rs {balance:,}.")
return balance - amount
balance = 6000
print("Welcome to the Village Bank ATM")
try:
while True:
amount = read_amount()
try:
balance = withdraw(balance, amount)
except ATMError as error: # catches both of our error types
print(" " + str(error))
else:
print(f" Please collect Rs {amount:,}. Balance left: Rs {balance:,}")
break
finally:
print("Please take your card. Have a nice day!")
Try these changes:
- Change the inputs so the very first amount is valid.
- Add a
DailyLimitErrorfor amounts above Rs 5,000 and raise it inwithdraw(). - Count the wrong attempts and stop after three, printing “Too many attempts”.
Common Mistakes
- Using a bare
except:. It catches everything, including your own spelling mistakes. Here the real bug is a wrong variable name, but the user sees a misleading message and you never find out:
amount_text = "500"
try:
amount = int(amount_txt) # a spelling mistake in the variable name!
except:
print("Please enter a valid amount.") # wrong message: the real bug is hidden
Fix: catch only what you expect. Now the spelling mistake shows up clearly as a NameError, and you can fix it:
amount_text = "500"
try:
amount = int(amount_txt) # ERROR: NameError
except ValueError:
print("Please enter a valid amount.")
- Putting too much code inside
try. A big block can hide unrelated errors behind one message. Fix: wrap only the risky line, and move the rest toelse. - Putting a general except first.
except Exceptionbeforeexcept ValueErrormeans the second block can never run. Fix: list the most specific exceptions first. - Using exceptions for simple checks. If an
ifcan test it clearly, such asif key in accounts, anifis often easier to read. Usetrywhen the check is hard to do in advance, like converting user text to a number. - Hiding the error completely.
except ValueError: passmakes problems disappear without a trace. Fix: always print a message, ask again, or log what happened.
Practice Task: The Cash Deposit Machine
Write the logic for a deposit machine with its own limit of Rs 20,000 per deposit. Complete the TODO steps:
deposit()raisesDepositErrorwhen the text is not a number. (Catch theValueErrorand raise your own error inside theexcept.)- It also raises
DepositErrorfor amounts that are not a multiple of 100, or above the limit. - It returns the new balance when everything is fine.
- The loop uses
try,except,elseandfinallyto show each result and “Machine ready” every time.
class DepositError(Exception):
pass
MAX_DEPOSIT = 20000 # this machine's own limit
def deposit(balance, amount_text):
# TODO 1: turn amount_text into an int. If that fails, raise
# DepositError("Please enter digits only.")
# TODO 2: raise DepositError if the amount is not a multiple of 100
# TODO 3: raise DepositError if the amount is more than MAX_DEPOSIT
# TODO 4: return the new balance
pass
balance = 1000
for text in ["abc", "150", "25000", "2000"]:
# TODO 5: call deposit() inside try / except DepositError / else / finally
# except: print the error message
# else: print the new balance
# finally: print "Machine ready"
pass
Show one possible answer (try it yourself first!)
class DepositError(Exception):
pass
MAX_DEPOSIT = 20000 # this machine's own limit
def deposit(balance, amount_text):
try:
amount = int(amount_text)
except ValueError:
raise DepositError("Please enter digits only.")
if amount <= 0 or amount % 100 != 0:
raise DepositError("Please deposit notes: a multiple of Rs 100.")
if amount > MAX_DEPOSIT:
raise DepositError(f"You can deposit at most Rs {MAX_DEPOSIT:,} at a time.")
return balance + amount
balance = 1000
for text in ["abc", "150", "25000", "2000"]:
print("Deposit:", text)
try:
balance = deposit(balance, text)
except DepositError as error:
print(" " + str(error))
else:
print(f" Done. New balance: Rs {balance:,}")
finally:
print(" Machine ready")
# Deposit: abc
# Please enter digits only.
# Machine ready
# Deposit: 150
# Please deposit notes: a multiple of Rs 100.
# Machine ready
# Deposit: 25000
# You can deposit at most Rs 20,000 at a time.
# Machine ready
# Deposit: 2000
# Done. New balance: Rs 3,000
# Machine ready
Key Takeaways
- An exception stops the program unless you handle it. Read the traceback from the bottom: the error type and message, then the line.
tryholds the risky line;except SomeErrorhandles one kind of problem; use severalexceptblocks for different messages.elseruns only on success;finallyruns every time and is perfect for clean-up.- Use
raise ValueError("clear message")for your own rules, and a custom class likeclass InsufficientBalanceError(Exception)for named problems. - A
while Trueloop withtry/exceptkeeps asking until the user enters a valid value. Never use a bareexcept:.