PY

Python Fundamentals

18 lessons

Progress0%
1. Introduction to Python
1What is Python?2Setting Up Python
2. Variables and Data Types
1Variables in Python2Data Types
3. Control Flow
Conditional StatementsLoops
4. Functions
Function Basics
5. Data Structures
Lists Deep DiveTuples & SetsDictionaries & Comprehensions
6. Advanced Functions
Closures & Higher-Order FunctionsDecorators & Lambda
7. Object-Oriented Python
Classes & InstancesInheritance & Dunder Methods
8. Exception Handling
try / except / finallyCustom Exceptions & Context Managers
9. Modules & File I/O
Modules & PackagesFile I/O & JSON
All Tutorials
PythonException Handling
Lesson 15 of 18 min
Chapter 8 · Lesson 1

try / except / finally

try / except / finally

Errors are inevitable. Python's exception-handling mechanism lets you respond to errors gracefully instead of crashing.

The Full Syntax

python
try:
    # code that might raise an exception
except SomeError as e:
    # handle a specific error
except (TypeError, ValueError) as e:
    # handle multiple exception types
except Exception as e:
    # catch-all for any standard exception
else:
    # runs only if NO exception was raised in try
finally:
    # ALWAYS runs — cleanup code goes here

The else Clause

The else block runs only when the try block completes without an exception. It is the right place for code that should only execute on success but shouldn't be inside try itself (to avoid accidentally masking exceptions):

python
try:
    result = int(user_input)
except ValueError:
    print("Not a valid number")
else:
    print(f"You entered: {result}")   # only if no ValueError

The finally Clause

finally always executes, whether or not an exception occurred. Use it to release resources:

python
f = open("data.txt")
try:
    data = f.read()
finally:
    f.close()   # guaranteed to run

(In practice, use a with statement instead — covered in the next lesson.)

Raising Exceptions

python
raise ValueError("Age cannot be negative")

# Re-raise inside an except block
try:
    risky()
except ValueError as e:
    log(e)
    raise          # re-raise the same exception

Exception Hierarchy

All built-in exceptions inherit from BaseException. The most important branches:

code
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── TypeError
    ├── ValueError
    ├── OSError
    │   └── FileNotFoundError
    └── RuntimeError

Catch more specific exceptions first (put them above broader ones) to avoid masking useful error detail.

Best Practices

  • Catch the most specific exception type you expect.
  • Never use a bare except: — it catches SystemExit and KeyboardInterrupt too.
  • Keep the try block as short as possible.
  • Use finally (or a context manager) to guarantee resource cleanup.

Code Examples

try / except / else / finallypython
def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: cannot divide by zero")
        return None
    except TypeError as e:
        print(f"Error: invalid types — {e}")
        return None
    else:
        print(f"Success: {a} / {b} = {result:.4f}")
        return result
    finally:
        print("safe_divide() finished")

print("--- Test 1 ---")
safe_divide(10, 4)

print("--- Test 2 ---")
safe_divide(10, 0)

print("--- Test 3 ---")
safe_divide(10, "two")

The else block runs only when no exception occurs in try. finally always runs — even when an exception is caught. This ordering ensures resources are always released and success-only logic is cleanly separated from error handling.

Catching Multiple Exceptions & Exception Hierarchypython
data = {"name": "Alice", "score": "95"}

def process(d, key):
    try:
        value  = d[key]          # may raise KeyError
        number = int(value)      # may raise ValueError
        result = 100 / number    # may raise ZeroDivisionError
        return result
    except KeyError:
        print(f"Key '{key}' not found in data")
    except ValueError:
        print(f"Cannot convert '{d.get(key)}' to int")
    except ZeroDivisionError:
        print("Score is zero — division undefined")
    except Exception as e:
        print(f"Unexpected error: {type(e).__name__}: {e}")

process(data, "score")
process(data, "age")
process({"score": "abc"}, "score")

Listing specific exceptions before broad ones ensures correct handling. type(e).__name__ gives the exception class name without importing anything extra. The except Exception catch-all at the end handles anything unforeseen.

raise and Re-raisepython
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Age must be an int, got {type(age).__name__}")
    if age < 0:
        raise ValueError(f"Age cannot be negative, got {age}")
    if age > 150:
        raise ValueError(f"Age {age} seems unrealistic")
    return age

def create_user(name, age):
    try:
        valid_age = validate_age(age)
        return {"name": name, "age": valid_age}
    except (TypeError, ValueError) as e:
        print(f"Validation failed: {e}")
        raise  # re-raise for the caller to handle

# Valid
user = create_user("Alice", 30)
print("Created:", user)

# Invalid — caught and re-raised
try:
    create_user("Bob", -5)
except ValueError as e:
    print("Caught in outer scope:", e)

raise (bare) inside an except block re-raises the current exception with its original traceback intact — better than raise e, which resets the traceback. This pattern lets intermediate layers log errors while still propagating them.

Quick Quiz

1. When does the else clause of a try/except block execute?

2. What is the risk of using a bare except: clause (with no exception type)?

3. What does a bare raise statement (without arguments) do inside an except block?

Was this lesson helpful?

PreviousNext