
In Part 33, you learned try/except/else/finally, how to raise exceptions, and the exception hierarchy. Those are the mechanics. Now we go deeper â designing your own exception types, avoiding common anti-patterns, and writing error handling that professionals trust in production.
Before we dive in, here is the complete list of Python's built-in exceptions you should recognize. Group 1 are everyday errors you have already seen. Groups 2-4 appear less often, but you will hit them in real-world projects â especially when working with files, networks, and APIs. You do not need to memorize all of them â just glance through so you recognize them when they show up in tracebacks.
| Exception | When It Happens |
|---|---|
SyntaxError | You wrote code Python cannot even read â like if x = 5: (used = instead of ==) |
IndentationError | Your indentation is wrong â usually mixing tabs and spaces, or missing indentation inside a function/loop |
NameError | You used a variable that was never defined â often just a typo like prnit(x) |
TypeError | You did an operation on the wrong type â like "5" + 3 or len(5) |
ValueError | Right type, but the value is invalid â like int("hello") |
KeyError | Asked for a dictionary key that does not exist â d["missing"] |
IndexError | Asked for a list position that does not exist â [1, 2][5] |
AttributeError | Tried to use a method or attribute an object does not have â "hello".append(...) (strings have no append) |
ZeroDivisionError | Divided by zero â 10 / 0 |
ModuleNotFoundError | Tried to import a package you have not installed â import requests without pip install requests |
ImportError | The package exists, but the specific thing inside does not â from math import xyz |
| Exception | When It Happens |
|---|---|
FileNotFoundError | Tried to open a file that does not exist on disk |
PermissionError | The file exists, but the OS will not let you read or write it (no permission) |
OSError | Generic OS-level error and parent of FileNotFoundError, PermissionError, etc. â use when you are not sure which specific file/system error to expect |
ConnectionError | Network failed â server unreachable, internet dropped, DNS failed. Very common with API calls |
TimeoutError | Operation took too long â common when an API, database, or external service does not respond in time |
| Exception | When It Happens |
|---|---|
KeyboardInterrupt | The user pressed Ctrl+C to stop your program. Never silently catch this â always let the user kill the program if they want to |
RecursionError | A function called itself too many times (over ~1000 by default). Usually means you forgot the stopping condition in a recursive function |
UnboundLocalError | You tried to use a local variable before assigning a value to it â often a typo where you wrote count = count + 1 but count was never initialized inside the function |
StopIteration | You called next() on an iterator that has no more values left â usually happens internally when a for loop finishes |
RuntimeError | Generic runtime problem when no other exception type fits â rare to use directly, you will see it occasionally in libraries |
AssertionError | An assert statement failed â means a developer's sanity check caught a bug in their own code |
| Exception | When It Happens |
|---|---|
SystemExit | Raised when sys.exit() is called. For example: your script checks if a command-line argument is missing and calls sys.exit(1) to quit immediately. Python also uses this internally to shut down cleanly. You almost never catch it manually |
OverflowError | A math result is too large for the data type â happens with floats (e.g. math.exp(1000)). Rare with regular Python int (which has no size limit) |
UnicodeDecodeError | Tried to read bytes as text but the encoding does not match â common when opening files written in a different encoding (e.g. trying to read a Windows-1252 file as UTF-8) |
MemoryError | The program ran out of RAM â happens when you create very large data structures or have an infinite loop that keeps creating new objects |
NotImplementedError | A method exists in the code but the developer has not actually written what it should do yet â you will see this in abstract classes (Part 41) and unfinished libraries |
A useful rule of thumb: Group 1 you fix by changing your code. Groups 2-3 you fix by handling at runtime with try/except. Group 4 is mostly informational â you rarely catch them yourself, but you should know what they mean when you see them in a traceback.
Built-in exceptions cover generic errors. But real applications have domain-specific errors that deserve their own names.
# No import needed â Exception is a Python built-in (like print, len, int).
# Every exception in the Quick Reference table above is also built-in.
class InvalidAgeError(Exception):
"""Raised when an age value is outside the valid range."""
pass
class InvalidEmailError(Exception):
"""Raised when an email format is invalid."""
pass
Where does
Exceptioncome from?Exceptionis a built-in Python class â part of thebuiltinsmodule that Python automatically loads into every file. The same is true forValueError,TypeError,KeyError, and every other exception in the reference table above. You never need toimportthem. The only time you import an exception is when it lives inside a library â for example,from requests.exceptions import ConnectionErrororfrom json import JSONDecodeError.
This uses the class keyword, which we cover in depth in Part 41. For now, treat it as a copy-paste recipe: class YourErrorName(Exception): pass. You do not need to understand classes yet â just follow this pattern, and it will make full sense when you reach Part 41.
def validate_age(age):
if not isinstance(age, int):
raise InvalidAgeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0 or age > 150:
raise InvalidAgeError(f"Age must be between 0 and 150, got {age}")
return age
def validate_email(email):
if "@" not in email or "." not in email:
raise InvalidEmailError(f"Invalid email format: {email}")
return email
try:
validate_age(-5)
except InvalidAgeError as e:
print(f"Age error: {e}") # Age error: Age must be between 0 and 150, got -5
try:
validate_email("shyam")
except InvalidEmailError as e:
print(f"Email error: {e}") # Email error: Invalid email format: shyam
| Use Custom Exceptions | Use Built-in Exceptions |
|---|---|
| Domain-specific errors (InvalidAgeError, PaymentFailedError) | Generic type/value problems |
| When callers need to distinguish between different error types | When the built-in name accurately describes the problem |
| In libraries and frameworks used by other developers | In simple scripts |
This is the worst thing you can write in Python:
try:
result = dangerous_operation()
except:
pass
This catches every exception â including KeyboardInterrupt (Ctrl+C) â and silently ignores it. Bugs hide. Data corrupts. You spend hours debugging an issue that was already signaled by an exception you swallowed.
# Never do this
try:
user = get_user(user_id)
except:
pass # User is now undefined. Code below will crash mysteriously.
try:
user = get_user(user_id)
except KeyError:
print(f"User {user_id} not found")
user = None
Catch specific exceptions. Handle them explicitly. Never swallow errors silently.
# Too broad â hides real bugs
try:
result = complex_calculation(data)
except Exception:
print("Something went wrong")
If complex_calculation has a bug that raises TypeError, this code hides it. You see "Something went wrong" instead of the actual error.
# Better â catch what you expect
try:
result = complex_calculation(data)
except ValueError as e:
print(f"Invalid data: {e}")
except ZeroDivisionError:
print("Division by zero in calculation")
Catch only what you expect and know how to handle. Let unexpected errors propagate â they reveal bugs.
When catching one exception and raising another, preserve the original context:
class DataProcessingError(Exception):
pass
def process_record(record):
try:
age = int(record["age"])
except (ValueError, KeyError) as e:
raise DataProcessingError(f"Failed to process record: {record}") from e
try:
process_record({"name": "Alice", "age": "abc"})
except DataProcessingError as e:
print(e)
print(f"Caused by: {e.__cause__}")
from e chains the exceptions together. When debugging, you see both the high-level error and the original cause.
Sometimes you want to catch an exception, do something (like logging), and then let it propagate:
def process_data(data):
try:
result = transform(data)
except ValueError:
print(f"Warning: bad data encountered: {data}")
raise # Re-raises the same exception
try:
process_data("bad input")
except ValueError as e:
print(f"Caught at top level: {e}")
raise without arguments re-raises the current exception with its original traceback intact.
Two philosophies for dealing with potential errors. Both terms are officially defined in the Python language documentation â they are not community slang, but canonical Python terminology.
Official sources (read straight from python.org):
The phrase "easier to ask forgiveness than permission" itself dates back to Rear Admiral Grace Hopper, one of the pioneers of computer science. The Python community adopted it as a coding philosophy and formally added it to the language's glossary.
Check conditions before acting:
if "name" in user_data:
name = user_data["name"]
else:
name = "Unknown"
Try and handle failure:
try:
name = user_data["name"]
except KeyError:
name = "Unknown"
Python favors EAFP. The official Python glossary explicitly describes it as the "clean and fast" Pythonic style. It is often faster (avoids double lookups) and handles race conditions better. But the best solution here is:
name = user_data.get("name", "Unknown")
Use whatever is clearest for the situation.
import time
def fetch_with_retry(url, max_retries=3, delay=1):
"""Attempt to fetch a URL with retries on failure."""
for attempt in range(1, max_retries + 1):
try:
print(f"Attempt {attempt}...")
response = make_request(url) # hypothetical function
return response
except ConnectionError:
if attempt == max_retries:
raise
print(f"Failed. Retrying in {delay}s...")
time.sleep(delay)
delay *= 2 # exponential backoff
Each retry waits longer than the last. After the final attempt, the exception propagates.
def get_config_value(config, key, default=None):
try:
return config[key]
except KeyError:
return default
timeout = get_config_value(settings, "timeout", default=30)
def get_user_display_name(user_id):
try:
user = fetch_user(user_id)
return user["display_name"]
except (KeyError, ConnectionError):
return f"User #{user_id}"
If the full data is unavailable, return a reasonable alternative instead of crashing.
"Invalid email format: shyam" is better than "Error".f"Age must be positive, got {age}" helps debugging.except ValueError is better than except Exception.except: pass.raise HTTPException(status_code=404, detail="User not found") returns a proper API error response.Build a user registration system:
InvalidAgeError â for ages outside 13-120InvalidEmailError â for emails missing @ or .register_user(name, age, email):name is not empty (raise ValueError)age is between 13 and 120 (raise InvalidAgeError)email contains @ and . (raise InvalidEmailError)register_user inside a try/exceptExample session:
Name: Alice
Age: 10
Email: alice@example.com
Error: Age must be between 13 and 120, got 10
Name: Bob
Age: 25
Email: bob-email
Error: Invalid email format: bob-email
Name: Charlie
Age: 30
Email: charlie@example.com
Registered: {'name': 'Charlie', 'age': 30, 'email': 'charlie@example.com'}
Save as src/user_registration.py.
Next: Part 35 â Exceptions Part 3 (Hands-On Practice). You now have the theory of professional error design. Next, we put every exception type from this chapter on the workbench â one tiny example each â so the patterns become muscle memory before we move on to file handling.
Read & Write Files (Hands-On Practical) | Python in Kannada | Part-37.1
Part 37.1
Read & Write Files (Hands-On Practical) | Python in Kannada | Part-37.1
Part 37.1