def read_distance(text):
try:
return int(text)
except ValueError:
return None
print(read_distance("42"))
print(read_distance("--"))42
NoneRead this as a study guide instead
Reading, writing, and what a traceback is actually telling you.
A notebook-style walk through the idea — every output shown is the real result of the code above it.
Catch a failure by its type, use try/except/else/finally, raise your own exception, and parse a batch defensively.
Real output Every Out block below was produced by running the code above it. You can copy these cells into your own python3 and run them top to bottom to see the same numbers.
On the car, a bad reading or a missing calibration value should not crash the control loop. An exception stops the work when an assumption breaks; a try/except block catches it so the controller decides what happens next. Every cell here catches its own failure.
def read_distance(text):
try:
return int(text)
except ValueError:
return None
print(read_distance("42"))
print(read_distance("--"))42
NoneThe full shape has four parts. try holds the risky work, except recovers when it raises, else runs only when nothing was raised, and finally runs on every path — the place for cleanup the loop cannot skip.
def stopping_distance(speed, decel):
try:
result = speed / decel
except ZeroDivisionError:
print("cannot stop with zero deceleration")
return None
else:
print("brake ok")
return result
finally:
print("loop done")
print(stopping_distance(10, 2))
print(stopping_distance(10, 0))brake ok
loop done
5.0
cannot stop with zero deceleration
loop done
NoneDifferent failures raise different types. Catch each by name — KeyError for a missing calibration key, IndexError for a sensor channel past the end — and handle exactly the one you expect.
calibration = {"steering": 3, "throttle": 1}
sensors = [10, 5, 0]
def setting(key):
try:
return calibration[key]
except KeyError:
return "no such setting"
def channel(index):
try:
return sensors[index]
except IndexError:
return "out of range"
print(setting("steering"))
print(setting("brightness"))
print(channel(0))
print(channel(9))3
no such setting
10
out of rangeYou can raise a failure yourself. raise ValueError(...) stops with your message the moment a command stops making sense, so a bad throttle fails at the check instead of reaching the motor. The caller catches it and reads the message.
def set_throttle(value):
if value < 0:
raise ValueError("throttle cannot be negative")
return value
try:
set_throttle(-5)
except ValueError as err:
print("rejected:", err)
print(set_throttle(30))rejected: throttle cannot be negative
30When no built-in type names your failure well, define one: a class that subclasses Exception. The controller can then catch precisely a calibration problem by name, without swallowing an unrelated one.
class CalibrationError(Exception):
pass
def load_calibration(config, name):
if name not in config:
raise CalibrationError("missing calibration: " + name)
return config[name]
profile = {"center": "neutral"}
try:
load_calibration(profile, "trim")
except CalibrationError as err:
print("calibration problem:", err)
print(load_calibration(profile, "center"))calibration problem: missing calibration: trim
neutralParsing a sweep of readings, you decide up front whether one bad reading stops everything. Often the better move is to keep the good values and collect the rejects, so one glitched sample does not sink the run — you finish with the readings you could use and a list of exactly what you could not.
def parse_readings(items):
good = []
errors = []
for item in items:
try:
good.append(int(item))
except ValueError:
errors.append(item)
return good, errors
values, bad = parse_readings(["40", "12", "x", "33", "??"])
print(values)
print(bad)
print(len(values), "ok,", len(bad), "bad")[40, 12, 33]
['x', '??']
3 ok, 2 badThese are the exact fragments the model serves — also available as an ordered study guide.
A program spends most of its life among values it made itself, where nothing surprises it. Two places break that comfort: reading and writing files, and running on inputs it did not choose. Both are the same encounter — code reaching for something it does not control — and both need the same habit: expect the world to say no, and have an answer ready.
Files are the durable edge. A path might not exist, a disk might be full, a file you meant to read might have been moved out from under you. Errors are the other edge: a value that will not turn into a number, a key that is not in the dictionary, a list index past the end. When one of these happens Python does not limp on with a wrong answer — it raises an exception, stops the current work, and prints a traceback saying exactly what went wrong and where.
That refusal is a feature. An exception is a value you can catch, inspect, and respond to, which means a failure becomes a decision your code gets to make rather than a crash it suffers. This concept has two halves that share that idea. The file half — opening, reading, and writing on a real filesystem — is shown as code to run on your own machine. The error half — catching failures, raising your own, and parsing untrusted input without falling over — runs live in the notebook, because it needs nothing but memory and behaves the same every time.
To touch a file, Python hands you a file object with open. You give it a path and a mode, and the mode decides what the object will let you do. open("notes.txt", "r") opens for reading and is the default, so open("notes.txt") means the same thing; it fails if the file is not there. open("notes.txt", "w") opens for writing and truncates the file to empty first, creating it if it does not exist — an easy way to erase work if you reach for it by reflex. open("notes.txt", "a") appends, adding to the end and leaving what was there alone. These are examples to run on your own machine, not in the notebook: a live filesystem is exactly the thing the notebook cannot reproduce the same way twice.
An open file object holds an operating-system resource that must be handed back, and forgetting to close it leaks that resource. The fix is not to remember harder; it is to use with, which makes the closing automatic. Writing with open("notes.txt") as f: binds the file to f for the indented block and closes it the instant the block ends — whether the block finished normally or blew up partway through. The guarantee that the handle closes even on failure is the whole reason with is the standard way to open a file.
For the common case of reading or writing a whole file at once, pathlib is shorter still: Path("notes.txt").read_text() opens, reads, and closes in one call, and Path("notes.txt").write_text("hello") does the same for writing. The one failure to expect up front is a path that is not there: reading a missing file raises FileNotFoundError, the specific exception to catch around any read whose file might be gone. Which brings the file edge and the error edge together — opening a file is one of the most ordinary places a real program has to be ready for something to go wrong.
When an exception is not caught, Python stops and prints a traceback — the block of text people learn to dread and then learn to read. It is not noise. It is a precise report of what went wrong and the exact path the program took to get there, and reading it is faster than guessing almost every time.
Read it from the bottom up. The last line is the payload: the exception type followed by its message, such as a KeyError naming the key that was missing or a ValueError explaining that some text could not be read as a number. The type tells you the kind of failure and the message narrows it down. Just above that, the traceback shows the innermost frame — the file, the line number, and the actual line of code where the exception was raised. That line is where to look first. The frames above it are the calls that led there, outermost at the top, each one a caller waiting on the line below, so the stack reads as a chain from where you started down to where it broke.
The message is the specific fact and the line number is the address; together they usually point straight at the bug. Working out *why* that line failed, and fixing it without breaking three other things, is the wider discipline of debugging — a concept of its own. Reading the traceback is the part that belongs here: it is the first thing every failure tells you, and learning to take it at face value instead of skimming past it is most of the distance to a fix.
An exception is not a vague "something broke" — it is an object with a type, and the type is the useful part. Python raises a different class for a different kind of failure, and that class is exactly what a handler matches on later. Knowing the common ones by name is most of what it takes to respond to a failure precisely instead of catching everything and hoping.
A handful cover most of what you will meet early. ValueError means a value has the right type but the wrong content — asking int to read "twelve" raises it, because the string is fine as a string but nonsense as a number. KeyError means a dictionary has no such key. IndexError means a list or other sequence was asked for a position past its end. ZeroDivisionError is what dividing by zero raises. TypeError means an operation was handed the wrong type entirely, like adding a number to a string. And FileNotFoundError, from the file edge, means a path you tried to open is not there.
These types form a family tree — every one of them descends from a base class named Exception — and that hierarchy matters when you catch, because catching a base type also catches everything under it. The practical takeaway is smaller: the type is a diagnosis. When a traceback ends in KeyError, you are not looking for a syntax mistake or a broken disk; you are looking for a lookup that asked for a name that was not there. Reading the type first tells you which kind of wrong you are dealing with before you read another word.
Catching an exception is a four-part shape, though most of the time you use only the first two. try wraps the risky work — the parse, the lookup, the open. except names the exception type to catch and holds the recovery: if the try body raises that type, control jumps straight to the except block instead of crashing like a safety net under a trapeze: the risky move happens above it, and if the performer slips the net catches them instead of the floor. If the try body raises nothing, the except block is skipped entirely. That much is the everyday case: guard a line that might fail, and say what to do when it does.
Two more blocks handle the tidier cases. else runs only when the try body finished without raising — a place for the work that should happen on success and should not be mistaken for the risky part. finally runs on the way out no matter what happened: success, caught exception, or even a return from inside the try. Because it always runs, finally is where cleanup goes — closing something, releasing something — the code you cannot afford to skip.
The one discipline that matters is to catch narrowly. except ValueError catches a value that would not parse and nothing else; a bare except or except Exception catches every failure alike, including bugs you never meant to hide, so a real problem gets silently swallowed and surfaces later somewhere worse. Name the exact type you expect and are prepared to handle, and let everything else keep propagating up to a traceback where you can see it. A handler you cannot explain is usually hiding a bug, not preventing one.
Catching is only half of it. Your own code also gets to decide that something is wrong and stop, and the way it does that is raise. Writing raise ValueError("speed cannot be negative") throws that exception right now, with your message attached; if nothing catches it, it becomes a traceback exactly like a built-in failure. Raising early, the moment you notice a bad value, is how you keep a wrong input from travelling deep into the program and causing a confusing failure far from its cause.
When the built-in types do not name your failure well, define one. A custom exception is just a class that inherits from Exception: class ConfigError(Exception): pass is a complete, usable exception type. Now code can raise ConfigError("missing setting: timeout") where a configuration is broken, and a caller can write except ConfigError to catch precisely that and not accidentally swallow an unrelated ValueError at the same time. The name itself documents what went wrong.
Raising and catching are two ends of one contract, and they usually live in different places. A function raises where it detects the problem, because that is where the facts are — it knows the value was negative or the key was absent. Its caller catches where the recovery makes sense, because that is where the choices are — retry, use a default, or report and move on. Keeping those two jobs apart is what lets a small function stay honest about failing while the code around it stays in charge of what to do about it.
Most real failures arrive as data. A file has a blank line where a number should be, a field is missing, a value is the string "unknown" instead of a count. The place to deal with this is the boundary — the moment the outside data first becomes program values — because a bad value caught at the door is cheap to explain, while the same value caught ten calls deep is a mystery like an inspection gate at the entrance: goods are checked at the door, where a problem is easy to spot and cheap to fix, not deep inside the warehouse where it is expensive to trace. Parsing a file's contents is exactly this boundary, which is why files and errors are one concept: the read succeeds, and then the real question is whether what you read makes sense.
The pattern is small and repeats everywhere. Wrap the conversion that might fail in a try, catch the narrow exception it raises, and respond with something a human can act on — a message that names the offending value, a sensible default, or a re-raise carrying more context than the original. What you do not do is let the raw failure escape with nothing added; a bare ValueError from three functions down tells the reader far less than "row 12: expected a number, got 'unknown'".
When you are parsing many things, decide up front whether one bad item should stop everything or just be set aside. Often the right move is to keep the good values and collect the rejects, so one malformed line does not sink an entire file of clean ones — you finish with the data you could use and a list of exactly what you could not, which is a far better position than a program that halted on the first surprise. Deciding that policy on purpose, at the boundary, is the difference between a script that works on your data and a tool that survives everyone else's.
A script that runs once on inputs you typed yourself needs none of this. The moment it reads a file someone else wrote, or runs tomorrow on data you have not seen, the difference between a tool and a toy is entirely how it behaves when something is wrong. Files and errors are where that difference lives: the program touches the world, the world does not cooperate, and the code either has an answer or it does not.
The habits are cheap and they compound. Open files with with, so a handle never leaks even when the body fails. Catch the narrow exception you expect and let the rest propagate to a traceback you can read. Raise your own failure, with a clear message, the moment a value stops making sense, rather than letting it corrupt three steps of work first. Each of these is one line of care that turns a silent wrong answer into a loud, located one — which is always the cheaper problem to have.
This is also the ground the next concepts stand on. Turning a traceback into a fix is the discipline of debugging; wiring these pieces into a program that runs on a schedule and cleans up after itself is scripting. Both assume you can already read what a failure is telling you and keep a program on its feet when the file is missing or the input is junk. Learn to expect the world to say no, and everything you build after this has somewhere solid to fail.