Organizing Code with Functions

When Things Go Wrong: Exceptions and Input Validation


Learning Objectives

  • You can explain what an exception is and what happens when one is raised.
  • You can catch an exception with try/except and decide whether to recover or re-raise.
  • You can use raise to signal that a function received input it cannot handle.
  • You can articulate when not to catch an exception.

So far, the functions you have written have assumed their inputs make sense. total_minutes([45, 30, 60]) is well-defined; total_minutes(["forty-five"]) is not. int(input(...)) is fine when the user types a number; it crashes when the user types a word. The question this chapter answers is: what should your code do when the input is wrong?

The mechanism Python provides is called an exception. You have already met several of them in this course, even if you did not have the name for them. This chapter gives them a name, shows how to catch them when you want to recover, and shows how to raise them when your own functions need to signal a problem.

Exceptions Are For Signaling About Problems

You have seen these errors before:

  • IndexError — when you accessed subjects[3] on a list with only three items.
  • ValueError — when you tried int("forty-five").
  • TypeError — when you tried "5" + 3.
  • SyntaxError — when Python could not understand the code, such as a missing colon after if.
  • NameError — when you used a variable that did not exist.
  • AssertionError — when an assert failed.

Every one of these is an exception. The word means roughly what it sounds like: a signal that something exceptional has happened, that the normal flow of execution cannot continue.

When Python raises an exception, two things happen. First, the line that caused the problem stops executing immediately. Second, Python looks for code that has volunteered to handle exceptions of this kind. If it finds one, that handler runs. If it does not, the program ends with a traceback — a multi-line error message that includes the type of the exception, a message describing the problem, and a stack trace showing where in the code the exception happened.

So far, your programs have not volunteered to handle any exceptions. Whenever one happened, the program crashed. That has been fine for short experiments, but it is not fine for a study log that a user is interacting with. If the user types forty-five instead of 45, the program should ask again, not exit.

Exception names

0 / 5 points

Which exception is raised by int("forty")?

Handling Exceptions: try and except

The Python syntax for handling an exception is try/except:

try:
minutes = int(input("Minutes: "))
except ValueError:
print("That was not a number.")

Read the structure as: try to do this, and if a ValueError happens, do this instead.

The try block holds the code that might fail. The except block holds the code that runs only if that specific exception is raised. When the try block succeeds, the except block is skipped entirely. When the try block fails with the specified exception, control jumps to the except block. When the try block fails with a different exception, the program crashes as before — the except block does not catch what it was not asked to catch.

This allows creating a situation where the exceptions are no longer fatal, and the program keeps running even when the user makes a mistake. The program can also do something sensible in response to the problem, such as printing a message or asking again.

minutes = 0
while True:
try:
minutes = int(input("Minutes: "))
break
except ValueError:
print("Please enter a whole number.")
print(f"Got {minutes} minutes.")

The above example shows the standard “re-prompt on bad input” loop. The try block contains both the input and the conversion. If the conversion fails, the except block prints a message and the loop iterates — asking the user again. If the conversion succeeds, the break ends the loop and the program continues with a valid minutes.

Re-prompt until minutes are valid

0 / 10 points

Write a program that asks for study minutes until the user enters a valid whole number.

Your program must:

  • ask for minutes with input(...),
  • use try and except ValueError,
  • print Please type minutes as a whole number. after invalid input,
  • ask again after invalid input,
  • and print Got {minutes} minutes. after a valid number.

Example input:

forty
45

Required output text:

Please type minutes as a whole number.
Got 45 minutes.

Catch Specifically

The except ValueError: form catches only ValueError. If a different kind of exception happens inside the try block — say, KeyboardInterrupt because the user pressed Ctrl-C — it is not caught by this handler, and Python responds to that exception in the usual way. This is almost always what you want.

Python also lets you write a bare except: with no exception type, which catches everything:

try:
minutes = int(input("Minutes: "))
except: # don't do this
print("Something went wrong.")

The above code is valid Python, and it will catch the ValueError from a bad input, but it will also catch any other exception that happens in the try block — including things you did not expect, and things you did not mean to catch.

Whenever you can, write except SomeSpecificError:, catching the narrowest possible exception type that covers the problem you are trying to recover from. This way, if something else goes wrong, it will not be silently caught and hidden from you.

The exception types you need to catch in this course — e.g. ValueError, IndexError, KeyError, and FileNotFoundError — are all narrow and specific.

Specific catches

0 / 5 points

Why is except ValueError: usually better than a bare except: when converting user input with int()?

Catch When You Can Do Something

Catch an exception only when you have a meaningful way to respond to the problem. If you do not have a meaningful response, do not catch.

A meaningful except block:

  • Re-prompts the user. “That was not a number. Try again.” The program recovers in a way the user understands.
  • Returns a default value. “If the file is missing, treat it as empty.” The program continues with a sensible fallback.
  • Logs the problem and re-raises. “Note this happened, then crash anyway.” The program still fails, but with extra context for debugging.

A meaningless except block:

try:
minutes = int(input("Minutes: "))
except ValueError:
pass # silently ignore the problem

This catches the exception, throws it away, and continues with minutes undefined. The next line that tries to use minutes will crash with NameError, and the original cause — the bad input — is lost. The program is now harder to debug than if you had let the ValueError through in the first place.

One of the most common “anti-patterns” in Python is except SomeError: pass. If your except block is pass, you almost certainly should not be catching the exception at all.

The same principle applies more subtly to “just print and continue”:

try:
minutes = int(input("Minutes: "))
except ValueError:
print("Error.") # message printed, but minutes is still undefined

The program still proceeds to a line that will use minutes, and that line will now fail in a different way.

The rule that produces good except blocks: ask “what does my program look like after this except block runs?” If the answer is “it has a sensible value to work with” or “it asks again”, you are doing something meaningful.

Explain why pass hides the real problem

0 / 10 points

Study this code:

try:
minutes = int(input("Minutes: "))
except ValueError:
pass
print(f"You logged {minutes} minutes.")

Explain what happens if the user types forty-five, and why the except block makes the program harder to debug. In 3-6 sentences, also suggest one meaningful recovery.

Raising Your Own Exceptions

The other half of exception handling is raise. Your own functions can raise exceptions when they receive input they cannot work with:

def average_minutes(minutes_list):
if len(minutes_list) == 0:
raise ValueError("cannot average an empty list")
total = 0
for m in minutes_list:
total += m
return total / len(minutes_list)

raise ValueError("cannot average an empty list") does exactly what the built-in functions do when they fail: it stops the function immediately and raises an exception, with a message describing the problem. The caller either catches it with try/except or lets it propagate.

This is an example of input validation: the function checks its inputs at the top, and refuses to proceed if they do not meet its requirements.

Compare two ways to handle the empty-list case:

def longest_session_v1(minutes_list):
if len(minutes_list) == 0:
return None # quietly return
...
def longest_session_v2(minutes_list):
if len(minutes_list) == 0:
raise ValueError("empty list has no longest element")
...

The first version returns None, which the caller has to remember to check. If they forget, the bug appears later as a TypeError somewhere far from the actual problem. The second version raises an exception. The caller has to decide explicitly how to handle the case — either with try/except or by ensuring the list is not empty before calling.

Both are valid choices. Returning None makes more sense when “no result” is a common, expected case. Raising makes more sense when an empty input is genuinely a misuse of the function. Picking the right one is a design decision.

Average score with validation

0 / 10 points

Define a pure function named average_score(scores).

The function must:

  • take one parameter, scores, which is a list of numbers,
  • raise ValueError("cannot average an empty score list") if scores is empty,
  • return the arithmetic average of the scores otherwise,
  • not ask for input,
  • and not print inside the function.

Example calls and return values:

average_score([4, 5, 3]) -> 4.0
average_score([10]) -> 10.0
average_score([]) -> raises ValueError

The tests will load your program and call average_score(...) directly.

A Study Log Example

Here is a version of get_session that uses try/except to handle a non-numeric minutes input:

def get_session():
subject = input("Subject (or 'done' to finish): ").strip().lower()
if subject == "done":
return None, None
while True:
minutes_text = input("For how many minutes? ").strip()
try:
minutes = int(minutes_text)
break
except ValueError:
print("Please enter a whole number of minutes.")
return subject, minutes

If the user types a non-number, the int(...) call raises ValueError. The except block catches it, prints a friendly message, and the inner loop iterates. The user is asked again. Only when the conversion succeeds does the break exit the loop and the function returns.

Note also how the function returns two values, separated with a comma. This is a Python feature called tuple unpacking. The caller can write subject, minutes = get_session() to unpack the two values into separate variables.

The function does not handle negative numbers. Inputs such as “-5” will be accepted, as int("-5") succeeds and returns -5. A reasonable next step is to add a check after the conversion:

try:
minutes = int(minutes_text)
if minutes < 0:
raise ValueError("minutes cannot be negative")
break
except ValueError as e:
print("Please enter a non-negative whole number of minutes.")

The line except ValueError as e is new syntax. The as e part binds the exception object to a local variable e, in case you want to use its message in your response. (Here, the response is a generic message, so e is unused — but the form is worth knowing.) Notice also that the same except catches both the ValueError raised by int() and the ValueError raised by your own check. That is exactly the design: both are kinds of “bad number”, and the same recovery handles both.

Inventory item

0 / 10 points

Define a function named get_inventory_item().

The function must:

  • ask for Item (or done): ,
  • normalize the item name with .strip().lower(),
  • return (None, None) if the normalized item name is done,
  • otherwise ask for a count until the user enters a non-negative whole number,
  • use try and except ValueError,
  • raise or handle ValueError when the converted count is negative,
  • print Please type count as a non-negative whole number. after invalid counts,
  • and return (item, count) after valid input.

At the bottom of the program, call get_inventory_item() once and print the returned tuple.

Example input:

Notebooks
nope
-2
12

Required output text:

Please type count as a non-negative whole number.
Please type count as a non-negative whole number.
('notebooks', 12)

If the first input is a quit signal, the printed tuple must show that no item was returned.

Example input:

Done

Required output text:

(None, None)

The exact input prompts may differ. The required output lines above must appear exactly as written.

Common Exception Types

Here are the most common exception types that you’ll encounter in this course and what they mean. You do not need to memorize this list; the point is to recognize the names when they appear.

ExceptionRaised when
ValueErrorA value has the right type but the wrong content. int("hello") raises this.
TypeErrorA value has the wrong type for the operation. "5" + 3 raises this.
SyntaxErrorThe code is not valid Python syntax. A missing colon after if or def can raise this.
IndexErrorA list index is out of range. [1, 2, 3][5] raises this.
KeyErrorA dictionary key does not exist. You will meet this in Part 6.
FileNotFoundErrorA file does not exist. You will meet this in Part 4.
ZeroDivisionErrorDividing by zero.
AssertionErrorAn assert failed.
KeyboardInterruptThe user pressed Ctrl-C. Do not catch this.

Common Pitfalls

A short list of mistakes that come up often when learning exceptions:

  • Bare except:. Catches everything, including things you did not mean to catch. Almost always wrong. Use except SpecificError:.
  • except: pass. The classic anti-pattern. Throws away information about the problem and leaves the program in an unknown state. If you do not have a meaningful recovery, do not catch.
  • Catching too broadly with except Exception:. Better than bare except, still too broad most of the time.
  • Wrapping too much in try. Only the code that might raise the specific exception belongs in the try block. Lines that come later and depend on the result usually belong outside, after a successful try block.
  • Catching an exception you should have prevented. If you know the user might type a non-number, validating in a try/except is right. If you are catching a TypeError because your own function passed the wrong argument, the fix is to not pass the wrong argument — not to catch the resulting error.

Practice Exception Handling

The external practice below gives you another way to rehearse try, except, and exception flow while this chapter is still fresh:

Open the exception handling practice

You can attempt each problem as many times as you like. There is no penalty for incorrect attempts, and you will not be evaluated based on how many mistakes you make. After each submission, your previous guesses will appear below the problem; feel free to use them while solving the exercise.

We recommend completing the practice on a computer for the best experience, ideally soon after reading this chapter. Later chapters will move on to more advanced concepts.

Once you complete the assignment, copy and paste the code shown on the final screen into the exercise below. You do not need screenshots or any other proof of completion.

Exception handling practice

0 / 10 points

Complete the CodeCheck exception handling practice linked in the chapter.

When you reach the final screen, paste the completion code here. Do not include any extra text.

Check Your Understanding

  1. What is the difference between raising an exception and catching an exception?
  2. Why is except: (bare) often a bad idea?
  3. Look at this program. What happens if the user types forty-five? What happens if they press Ctrl-C?
try:
n = int(input("Number: "))
except ValueError:
print("Not a number.")
  1. In your own words, what is the difference between returning None for invalid input and raising an exception for invalid input? When does each make sense?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0