Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 7 of 9

So far you've caught exceptions Python raised. You can raise them yourself with raise — that's how a function reports "you gave me something I can't work with."

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

print(set_age(30))
print(set_age(-5))

Why raise instead of returning an error?

Compare the two approaches:

def withdraw(balance, amount):
    if amount > balance:
        return "Insufficient funds"
    return balance - amount

result = withdraw(100, 500)
new_balance = result - 10

Returning a string means the caller can ignore the problem, and result - 10 blows up much later with a confusing TypeError far from the real cause.

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount

try:
    print(withdraw(100, 500))
except ValueError as e:
    print(f"Rejected: {e}")

An exception can't be silently ignored — the caller either handles it or the program stops with a traceback pointing at the real problem.

Rough guideline: return a value for expected outcomes the caller will inspect anyway (.get() returning None), raise when the function genuinely cannot do its job.

Pick a meaningful type

Use the exception that describes the problem:

def get_item(items, index):
    if not isinstance(items, list):
        raise TypeError("items must be a list")
    if index >= len(items):
        raise IndexError(f"index {index} out of range")
    return items[index]

for bad in [lambda: get_item("abc", 0), lambda: get_item([1, 2], 5)]:
    try:
        bad()
    except (TypeError, IndexError) as e:
        print(f"{type(e).__name__}: {e}")

ValueError for a bad value, TypeError for a wrong type, KeyError for a missing key. A specific type lets callers catch precisely what they care about.

Validating early

Check inputs at the top of a function and raise immediately. This is often called a guard clause, and it keeps the rest of the body free of defensive nesting:

def create_user(name, age):
    if not name:
        raise ValueError("Name is required")
    if not isinstance(age, int):
        raise TypeError("Age must be an integer")
    if age < 0 or age > 150:
        raise ValueError(f"Age {age} is out of range")
    return {"name": name, "age": age}

print(create_user("Ada", 36))

try:
    create_user("", 36)
except ValueError as e:
    print(e)

Re-raising

Sometimes you want to react to an exception but still let it propagate. A bare raise inside except re-raises the original, traceback intact:

def load(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        print(f"Logging: {path} not found")
        raise

try:
    load("nope.txt")
except FileNotFoundError:
    print("Caller handled it.")

Write raise on its own here, not raise e — the bare form preserves the original traceback.

raise ... from

When one failure causes another, from records the connection so the traceback shows both:

def parse_config(text):
    try:
        return int(text)
    except ValueError as e:
        raise ValueError(f"Invalid config value: {text!r}") from e

try:
    parse_config("abc")
except ValueError as e:
    print(e)
    print(type(e.__cause__).__name__)

Try it yourself

Write a validator that raises the right exception type for each problem.

Checkpoint

  • raise SomeError("message") signals failure from your own code.
  • Raising beats returning an error string — it can't be silently ignored.
  • Match the exception type to the problem: ValueError, TypeError, KeyError.
  • Validate inputs early with guard clauses.
  • A bare raise re-raises and keeps the traceback; raise ... from e links causes.

Questions about this lesson

Log in to ask a question.