Errors, files and modules
try, except, else and finally in the right order; raising your own exceptions; the with statement; imports, __name__ and how Python finds a module.
This is the lesson that separates code that works from code that survives contact with real input.
Exceptions
An exception interrupts the flow and travels up the call stack until something catches it:
def parse_marks(text):
try:
return int(text)
except ValueError:
return None
print(parse_marks("87")) # 87
print(parse_marks("abc")) # NoneCatch the specific exception. A bare except: catches everything — including KeyboardInterrupt
when the user presses Ctrl-C and SystemExit when the program is trying to shut down:
try:
risky()
except: # never do this
pass
try:
risky()
except Exception as e: # acceptable: still excludes exit signals
log(e)
try:
risky()
except (ValueError, KeyError) as e: # best: name what you expect
log(e)except Exception is the widest catch you should normally write, and even then you should log the
exception rather than swallow it. except: pass is how a bug hides for six months.
The four clauses
try:
f = open("marks.txt")
data = f.read()
except FileNotFoundError:
print("no file")
except PermissionError:
print("cannot read it")
else:
print("read", len(data), "characters") # runs only if NO exception
finally:
print("always runs") # cleanup, exception or notexcept— handles a matching exception.else— runs when thetryblock finished without raising. Put the code that depends on success here, so it is not accidentally protected by theexcept.finally— always runs, including when the function returns or re-raises from insidetry. This is where cleanup goes.
Order matters: put the specific exception before the general one, because the first matching
except wins. except Exception before except ValueError makes the second unreachable.
Raising
def set_marks(value):
if not isinstance(value, int):
raise TypeError(f"marks must be an int, got {type(value).__name__}")
if not 0 <= value <= 100:
raise ValueError(f"marks out of range: {value}")
return valueChoosing the right built-in matters more than people expect: ValueError for a right-typed but
wrong-valued argument, TypeError for the wrong type, KeyError and IndexError for lookups,
NotImplementedError for a method a subclass must fill in.
Your own exception type is one line, and it lets callers catch precisely your failure:
class RosterError(Exception):
"""The roster file could not be used."""
raise RosterError("two students share a roll number")Re-raising after logging keeps the original traceback:
try:
process()
except RosterError as e:
log(e)
raise # bare raise — preserves the tracebackraise e also works but resets the traceback to this line, which loses the original location.
The hierarchy worth knowing
BaseException
├── SystemExit, KeyboardInterrupt, GeneratorExit
└── Exception
├── ArithmeticError → ZeroDivisionError, OverflowError
├── LookupError → IndexError, KeyError
├── OSError → FileNotFoundError, PermissionError
├── ValueError, TypeError, AttributeError, NameError
└── StopIteration, RuntimeError → RecursionErrorTwo facts fall out of it: catching LookupError catches both KeyError and IndexError, and
catching Exception deliberately does not catch Ctrl-C.
Files, and the with statement
f = open("marks.txt")
data = f.read()
f.close() # skipped entirely if read() raiseswith closes the file whatever happens, including on an exception:
with open("marks.txt") as f:
data = f.read()
# closed here, guaranteedAlways use with. It is the same guarantee as try/finally, in one line — and an object that
works with with is called a context manager, which is the term to use if asked.
Reading, three ways:
with open("marks.txt") as f:
whole = f.read() # one string — needs the file in memory
with open("marks.txt") as f:
lines = f.readlines() # list of lines, keeping "\n"
with open("marks.txt") as f:
for line in f: # lazy, one line at a time — prefer this
print(line.rstrip())Writing, and the modes:
| Mode | Meaning |
|---|---|
"r" |
read, the default, fails if missing |
"w" |
write, truncates an existing file |
"a" |
append |
"x" |
create, fails if it exists |
"rb" / "wb" |
binary — images, archives |
with open("out.csv", "w", encoding="utf-8", newline="") as f:
f.write("name,marks\n")
f.writelines([f"{n},{m}\n" for n, m in rows])Pass encoding="utf-8" explicitly. The default depends on the operating system, and code that
works on your laptop and mangles names on a server is nearly always this.
For structured data, use the standard library rather than splitting by hand:
import json, csv
with open("data.json") as f:
config = json.load(f)
with open("students.csv", newline="") as f:
for row in csv.DictReader(f):
print(row["name"], row["marks"])csv handles quoted fields containing commas. line.split(",") does not, and that is the bug.
Modules and imports
Any .py file is a module. Importing it runs it, once, and caches the result.
import math # math.sqrt(2)
import math as m # m.sqrt(2)
from math import sqrt, pi # sqrt(2)
from math import sqrt as root
from math import * # avoid — pollutes your namespace invisiblyWhere does Python look? In order: the directory of the script being run, then PYTHONPATH, then
the standard library, then site-packages. You can see the exact list:
import sys
print(sys.path)This explains the most common import failure in a beginner project: a file named random.py next
to your script shadows the standard library's random, because your own directory is searched
first.
if __name__ == "__main__"
Every module has a __name__. When run directly it is "__main__"; when imported it is the
module's name. So this block runs on python app.py and not on import app:
def main():
print("running")
if __name__ == "__main__":
main()Without it, importing your file to reuse one function also executes your whole script. This is asked in interviews often, and the one-sentence answer is: it separates "run me" from "import me".
A folder becomes a package when it can be imported; an __init__.py inside it makes that
explicit and runs when the package is first imported.
What to take into an interview
- Catch specific exceptions.
except Exceptionis the widest reasonable catch; bareexcept:also catches Ctrl-C and exit. elseruns whentrysucceeded;finallyalways runs, including on return.- Put specific
exceptclauses before general ones. - Bare
raisere-raises with the original traceback;raise eresets it. withguarantees cleanup — the object is a context manager. Always use it for files."w"truncates. Passencoding="utf-8"explicitly.sys.pathstarts with your script's own directory, which is why a localrandom.pybreaks everything.if __name__ == "__main__"separates running a file from importing it.