Python · Syntax · Beginner

Error Handling Basics

10 tasks

Learn how to handle errors gracefully using try/except/else/finally, raise your own exceptions, and write robust Python programs.

try / except / else / finally

#
Errors in Python are called **exceptions**. Instead of letting your program crash, you can **handle** exceptions using a `try` block. ## Basic try/except ```python try: number = int("abc") # this raises ValueError except ValueError: print("That's not a valid number!") # Program continues here normally print("After the try block.") ``` Without `try/except`, the program would crash at `int("abc")`. With it, we catch the error and continue. ## Catching multiple exception types ```python try: value = int(user_input) result = 10 / value except ValueError: print("Please enter a number.") except ZeroDivisionError: print("Number cannot be zero.") ``` You can also catch several at once: ```python except (ValueError, TypeError): print("Invalid input type or value.") ``` ## The else clause The `else` block runs only if **no exception was raised** in `try`: ```python try: result = int("42") except ValueError: print("Conversion failed.") else: print(f"Success! Result is {result}") # runs only when try succeeds ``` Use `else` for code that should only run on success — it keeps "happy path" logic separate from error handling. ## The finally clause `finally` **always** runs — whether an exception occurred or not: ```python try: f = open("data.txt") data = f.read() except FileNotFoundError: print("File not found.") finally: f.close() # always close the file ``` `finally` is typically used for cleanup: closing files, releasing connections, releasing locks. ## Accessing the exception object ```python try: result = 10 / 0 except ZeroDivisionError as e: print(f"Error: {e}") # Error: division by zero print(type(e)) # <class 'ZeroDivisionError'> ``` ## raise: triggering exceptions ```python def set_age(age): if age < 0: raise ValueError(f"Age cannot be negative: {age}") if age > 150: raise ValueError(f"Age {age} is unrealistically large.") return age try: set_age(-5) except ValueError as e: print(e) # Age cannot be negative: -5 ``` `raise` lets you signal errors in your own code with a meaningful message. ## Re-raising ```python try: do_something() except ValueError as e: print(f"Logging error: {e}") raise # re-raises the same exception, do not swallow it ```

Error Handling Patterns

#
## Pattern 1: safe type conversion Convert user input safely and return a sensible default on failure: ```python def safe_int(s, default=0): try: return int(s) except (ValueError, TypeError): return default print(safe_int("42")) # 42 print(safe_int("abc")) # 0 print(safe_int(None)) # 0 print(safe_int("7", -1)) # 7 ``` ## Pattern 2: input validation with raise Validate at the boundary of your system; raise if data is invalid: ```python def validate_email(email): if not isinstance(email, str): raise TypeError(f"email must be a string, got {type(email).__name__}") if "@" not in email: raise ValueError(f"Invalid email: {email!r}") return email.strip().lower() try: email = validate_email(" [email protected] ") print(email) # [email protected] except (TypeError, ValueError) as e: print(f"Validation error: {e}") ``` ## Pattern 3: multiple operations, single handler Group related risky operations together: ```python def load_config(path): try: with open(path) as f: import json data = json.load(f) return data except FileNotFoundError: print(f"Config file not found: {path}") return {} except json.JSONDecodeError as e: print(f"Invalid JSON in {path}: {e}") return {} ``` ## Pattern 4: use else to separate success logic ```python def divide(a, b): try: result = a / b except ZeroDivisionError: print("Cannot divide by zero.") return None else: # Only runs if no exception occurred print(f"{a} / {b} = {result:.4f}") return result divide(10, 3) # 10 / 3 = 3.3333 divide(10, 0) # Cannot divide by zero. ``` ## Pattern 5: finally for guaranteed cleanup ```python def read_first_line(path): f = None try: f = open(path) return f.readline().strip() except FileNotFoundError: return None finally: if f: f.close() # runs even if an exception occurred ``` In modern Python, the `with` statement handles cleanup automatically: ```python def read_first_line(path): try: with open(path) as f: return f.readline().strip() except FileNotFoundError: return None ```

Error Handling Reference

#
## Full syntax ```python try: # code that might raise an exception except ExceptionType: # runs if ExceptionType (or subclass) was raised except (TypeError, ValueError) as e: # catches either type; e holds the exception object except Exception: # catches any exception (use sparingly) else: # runs ONLY if no exception was raised in try finally: # ALWAYS runs, exception or not ``` ## Common built-in exceptions | Exception | When it occurs | |---|---| | `ValueError` | Right type, wrong value (`int("abc")`) | | `TypeError` | Wrong type (`"a" + 1`) | | `KeyError` | Dict key not found (`d["missing"]`) | | `IndexError` | List index out of range (`lst[99]`) | | `AttributeError` | Object has no such attribute (`None.upper()`) | | `ZeroDivisionError` | Division by zero (`1 / 0`) | | `FileNotFoundError` | File does not exist | | `PermissionError` | No permission to access file | | `ImportError` | Module not found | | `StopIteration` | Iterator exhausted | | `OverflowError` | Numeric result too large | | `MemoryError` | Out of memory | | `RecursionError` | Max recursion depth exceeded | | `AssertionError` | `assert` statement failed | | `NotImplementedError` | Abstract method not implemented | | `Exception` | Base class for all non-system exceptions | ## raise forms ```python raise ValueError("message") # raise new exception raise ValueError("msg") from original # chain exceptions raise # re-raise current exception ``` ## Exception hierarchy (simplified) ``` BaseException └── Exception ├── ValueError ├── TypeError ├── LookupError │ ├── KeyError │ └── IndexError ├── ArithmeticError │ └── ZeroDivisionError ├── OSError │ ├── FileNotFoundError │ └── PermissionError └── ... ``` ## Key rules ```python # Always catch specific exceptions, not bare `except:` # BAD: try: ... except: # catches KeyboardInterrupt, SystemExit, etc.! pass # GOOD: try: ... except ValueError: handle() # Use else for "success only" code try: result = compute() except ComputeError: ... else: save(result) # only if compute() succeeded # Use finally for guaranteed cleanup try: acquire_resource() use_resource() finally: release_resource() # always runs ```
01

Safe Convert to Int

#

Write a function `safe_int(s, default=0)` that tries to convert `s` to an integer and returns it on success, or `default` if conversion fails. It should never raise an exception. Example: `safe_int("42")` → `42`, `safe_int("abc")` → `0`, `safe_int("abc", -1)` → `-1`.

def safe_int(s, default=0):
    pass


print(safe_int("42"))        # 42
print(safe_int("abc"))       # 0
print(safe_int("abc", -1))  # -1
Solution
def safe_int(s, default=0):
    try:
        return int(s)
    except (ValueError, TypeError):
        return default


print(safe_int("42"))        # 42
print(safe_int("abc"))       # 0
print(safe_int("abc", -1))  # -1
02

Safe Division

#

Write a function `safe_divide(a, b)` that returns `a / b` as a float, or `None` if `b` is zero. Use try/except. Example: `safe_divide(10, 2)` → `5.0`, `safe_divide(10, 0)` → `None`.

def safe_divide(a, b):
    pass


print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))   # None
Solution
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None


print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))   # None
03

Multiple Exception Types

#

Write a function `parse_value(s)` that: - Returns `int(s)` if `s` can be converted to an integer - Returns `float(s)` if it can be converted to float but not int - Returns `None` if neither conversion works Do not use `.` to detect floats — use try/except only.

def parse_value(s):
    pass


print(parse_value("42"))     # 42   (int)
print(parse_value("3.14"))   # 3.14 (float)
print(parse_value("abc"))    # None
Solution
def parse_value(s):
    try:
        return int(s)
    except ValueError:
        pass
    try:
        return float(s)
    except ValueError:
        return None


print(parse_value("42"))     # 42
print(parse_value("3.14"))   # 3.14
print(parse_value("abc"))    # None
04

Validate Age

#

Write a function `validate_age(age)` that: - Raises `TypeError` if `age` is not an integer - Raises `ValueError` with message `"Age must be between 0 and 150"` if age is out of that range - Returns `age` if valid Then write code that calls `validate_age` with user input and handles both exceptions with appropriate messages.

def validate_age(age):
    pass


print(validate_age(25))   # 25
try:
    validate_age("old")
except TypeError as e:
    print(e)
try:
    validate_age(200)
except ValueError as e:
    print(e)
Solution
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Expected int, got {type(age).__name__}")
    if not 0 <= age <= 150:
        raise ValueError("Age must be between 0 and 150")
    return age


print(validate_age(25))   # 25
try:
    validate_age("old")
except TypeError as e:
    print(e)
try:
    validate_age(200)
except ValueError as e:
    print(e)
05

Use else Clause

#

Write a function `read_number(s)` that: - Tries to convert `s` to a float - If conversion fails, prints `"Invalid input"` and returns `None` - If it succeeds (use `else`), prints `"Got: {value:.2f}"` and returns the float The print in the success case must be inside the `else` block, not inside `try`.

def read_number(s):
    pass


print(read_number("3.14"))   # prints "Got: 3.14", returns 3.14
print(read_number("abc"))    # prints "Invalid input", returns None
Solution
def read_number(s):
    try:
        value = float(s)
    except ValueError:
        print("Invalid input")
        return None
    else:
        print(f"Got: {value:.2f}")
        return value


print(read_number("3.14"))   # Got: 3.14
print(read_number("abc"))    # Invalid input
06

Finally Cleanup

#

Write a function `safe_open(path)` that: - Opens a file at `path` and reads its content - Returns the content as a string if successful - Returns an empty string if `FileNotFoundError` occurs - Always prints `"Closing file"` in a `finally` block, whether or not opening succeeded (You can simulate opening with a variable that might be None.)

def safe_open(path):
    pass


print(safe_open("/tmp/test_eh.txt"))     # content or ""
print(safe_open("/tmp/no_such_file.txt")) # ""
Solution
def safe_open(path):
    f = None
    try:
        f = open(path, encoding="utf-8")
        return f.read()
    except FileNotFoundError:
        return ""
    finally:
        print("Closing file")
        if f:
            f.close()


import pathlib
pathlib.Path("/tmp/test_eh.txt").write_text("hello", encoding="utf-8")
print(safe_open("/tmp/test_eh.txt"))
print(safe_open("/tmp/no_such_file.txt"))
07

Access Nested Dict Safely

#

Write a function `get_user_city(data, username)` that retrieves `data[username]["address"]["city"]`. Handle `KeyError` if any key is missing and return `"Unknown"` in that case. Example: `get_user_city({"alice": {"address": {"city": "Kyiv"}}}, "alice")` → `"Kyiv"`, `get_user_city({}, "bob")` → `"Unknown"`.

def get_user_city(data, username):
    pass


users = {"alice": {"address": {"city": "Kyiv"}}}
print(get_user_city(users, "alice"))   # Kyiv
print(get_user_city(users, "bob"))     # Unknown
Solution
def get_user_city(data, username):
    try:
        return data[username]["address"]["city"]
    except KeyError:
        return "Unknown"


users = {"alice": {"address": {"city": "Kyiv"}}}
print(get_user_city(users, "alice"))   # Kyiv
print(get_user_city(users, "bob"))     # Unknown
08

Raise with Validation

#

Write a function `create_username(name)` that: - Raises `ValueError` if `name` is empty or contains only whitespace - Raises `ValueError` if `name` is longer than 20 characters - Raises `ValueError` if `name` contains spaces - Otherwise returns `name.lower().strip()` Include a clear error message in each `ValueError`.

def create_username(name):
    pass


print(create_username("Alice"))         # alice
print(create_username("  Bob  "))       # bob
try:
    create_username("")
except ValueError as e:
    print(e)
Solution
def create_username(name):
    name = name.strip()
    if not name:
        raise ValueError("Username cannot be empty")
    if len(name) > 20:
        raise ValueError("Username must be 20 characters or less")
    if " " in name:
        raise ValueError("Username cannot contain spaces")
    return name.lower()


print(create_username("Alice"))         # alice
print(create_username("  Bob  "))       # bob — strip() removes outer spaces
try:
    create_username("")
except ValueError as e:
    print(e)
09

Chain of Operations

#

Write a function `process(text)` that performs three operations in sequence: 1. Convert `text` to uppercase 2. Split on comma and take the first element 3. Convert the first element to an integer Handle `ValueError`, `IndexError`, and `AttributeError` in a single `except` clause and return `None` on any error. Return the integer on success.

def process(text):
    pass


print(process("hello,42,world"))   # 42
print(process("no-comma"))         # None
print(process("word,abc"))         # None
print(process(None))               # None
Solution
def process(text):
    try:
        upper = text.upper()
        first = upper.split(",")[0]
        return int(first)
    except (ValueError, IndexError, AttributeError):
        return None


print(process("hello,42,world"))   # None (first part is "HELLO", not int)
print(process("42,extra"))         # 42
print(process(None))               # None
10

Exception Report

#

Write a function `run_all(functions, input_value)` that takes a list of functions and runs each one on `input_value`. It should return a list of results where each element is either the return value (if the function succeeded) or the string `"ERROR: {exception_message}"` if any exception was raised. Example: with `functions = [str, int, lambda x: x/0]` and `input_value = "42"`, result is `["42", 42, "ERROR: division by zero"]`.

def run_all(functions, input_value):
    pass


funcs = [str, int, lambda x: x / 0]
print(run_all(funcs, "42"))
# ["42", 42, "ERROR: division by zero"]
Solution
def run_all(functions, input_value):
    results = []
    for fn in functions:
        try:
            results.append(fn(input_value))
        except Exception as e:
            results.append(f"ERROR: {e}")
    return results


funcs = [str, int, lambda x: x / 0]
print(run_all(funcs, "42"))
# ["42", 42, "ERROR: division by zero"]