Errors & Exceptions
Intermediate · 18 min read · ▶ live playground · ✦ checkpoint
A robust program is not a program that never meets bad data. It is a program that knows what to do when a score is typed as "ten", a file is missing, or a rule gets broken. Errors and exceptions are Python's way of stopping, naming the problem, and giving your code a chance to respond.
You already know how to read a traceback calmly. Now you will learn how to design the parts of your program that should recover, stop loudly, or clean up before leaving.
Syntax errors vs runtime exceptions
A syntax error is invalid Python grammar, so the interpreter cannot even start running the program. It is like handing a literal-minded assistant a recipe with a missing verb.
if True
print("Mina is ready")Python has to reject that before the first line runs. The colon is missing after if True.
A runtime exception is a problem that happens while valid code is running. The grammar is fine, but the data or situation is not.
scores = ["42", "35", "not ready", "39"]
for score_text in scores:
try:
score = int(score_text)
print("score:", score)
except ValueError:
print("bad score:", score_text)score: 42score: 35bad score: not readyscore: 39
An exception is a special object Python raises when something goes wrong at runtime. ValueError means the type was acceptable, but the actual value was not. "not ready" is text, and int() can receive text, but that text does not describe an integer.
The exception hierarchy
Python's exceptions sit in an exception hierarchy, a family tree where broad exception types have more specific children. Exception is the broad parent for most problems you handle. ValueError, KeyError, FileNotFoundError, and ZeroDivisionError are more specific children.
players = {"Mina": 42, "Ravi": 0}
requests = [
("Mina", "2"),
("Noor", "3"),
("Ravi", "0"),
("Mina", "many"),
]
for name, bonus_text in requests:
try:
base_score = players[name]
bonus = int(bonus_text)
print(name, "average chunk:", base_score / bonus)
except KeyError:
print("unknown player:", name)
except ValueError:
print("bonus is not a number:", bonus_text)
except ZeroDivisionError:
print("bonus cannot be zero")Mina average chunk: 21.0unknown player: Noorbonus cannot be zerobonus is not a number: many
Specific catches keep your response honest. If you catch only Exception, you know something failed, but you lose the story. A missing player, a bad number, and division by zero need different messages and often different recovery steps.
try, except, else, and finally
The try statement marks code that might raise an exception. except handles a named kind of exception. else runs only when the try block succeeds. finally runs no matter what.
Think of it as a careful kitchen routine: try the risky step, handle a spill if it happens, plate the dish if nothing spilled, and clean the counter either way.
reports = [
{"player": "Mina", "score": "42"},
{"player": "Ravi", "score": "oops"},
]
for report in reports:
try:
score = int(report["score"])
except ValueError:
print(report["player"], "needs a numeric score")
else:
print(report["player"], "saved score", score)
finally:
print("checked", report["player"])Mina saved score 42checked MinaRavi needs a numeric scorechecked Ravi
Put only the risky lines inside try. If you wrap half your program, you make it harder to see which operation failed.
Try it — load a tiny tracker safely
This program writes one valid JSON file, then tries three loads: one good file, one missing file, and one broken JSON file. Notice that every file example writes and reads inside the same run.
raise and custom exceptions
Sometimes your code discovers a problem and should stop the current operation itself. raise means "create this exception now."
class EmptyTrackerError(Exception):
pass
def clean_score(player, score):
if score < 0:
raise ValueError(f"{player} has a negative score")
return score
def summarize_tracker(owner, items):
if not items:
raise EmptyTrackerError(f"{owner} has no items to summarize")
return f"{owner}: {len(items)} items"
for player, score in [("Mina", 42), ("Ravi", -3), ("Noor", 39)]:
try:
print(player, clean_score(player, score))
except ValueError as error:
print("cannot save:", error)
for owner, items in [("Mina", ["apples", "tea"]), ("Ravi", [])]:
try:
print(summarize_tracker(owner, items))
except EmptyTrackerError as error:
print("tracker problem:", error)Mina 42cannot save: Ravi has a negative scoreNoor 39Mina: 2 itemstracker problem: Ravi has no items to summarize
A custom exception is your own named exception type for a problem your program understands. It uses class, a way to define a new kind of object; full classes come in lesson 10.2. Custom exceptions are useful when callers should catch your program's rule failure separately from built-in failures like missing files.
assert and defensive programming
Defensive programming means checking assumptions at the edge of your code, before bad data travels deeper. assert is a quick development-time check that raises AssertionError when an assumption is false.
def average_score(scores):
assert scores, "scores must not be empty"
return sum(scores) / len(scores)
print(average_score([42, 35, 39]))
scores = {"Mina": 42, "Ravi": 35}
name = "Noor"
if name in scores:
print("LBYL:", scores[name])
else:
print("LBYL: missing", name)
try:
print("EAFP:", scores[name])
except KeyError:
print("EAFP: missing", name)38.666666666666664LBYL: missing NoorEAFP: missing Noor
Use assert for mistakes in your own code, not for normal user input. User input deserves clear if checks and exceptions you choose. assert can be disabled when Python runs in optimized mode, so it is not a security gate.
Both versions are valid. Choose the one that keeps the happy path readable and handles the failure you truly expect.
Checkpoint
Answer all three to mark this lesson complete
You now have the last tool the Part II project needs: not just saving data, but surviving the messy cases around it. Next you will combine functions, collections, files, JSON or CSV, and careful exception handling into one persistent tracker.