Lesson 09 · 10 min read · Python for placements

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"))    # None

Catch 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 not
  • except — handles a matching exception.
  • else — runs when the try block finished without raising. Put the code that depends on success here, so it is not accidentally protected by the except.
  • finally — always runs, including when the function returns or re-raises from inside try. 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 value

Choosing 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 traceback

raise 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 → RecursionError

Two 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() raises

with closes the file whatever happens, including on an exception:

with open("marks.txt") as f:
    data = f.read()
# closed here, guaranteed

Always 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 invisibly

Where 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 Exception is the widest reasonable catch; bare except: also catches Ctrl-C and exit.
  • else runs when try succeeded; finally always runs, including on return.
  • Put specific except clauses before general ones.
  • Bare raise re-raises with the original traceback; raise e resets it.
  • with guarantees cleanup — the object is a context manager. Always use it for files.
  • "w" truncates. Pass encoding="utf-8" explicitly.
  • sys.path starts with your script's own directory, which is why a local random.py breaks everything.
  • if __name__ == "__main__" separates running a file from importing it.
Lesson 9 of 12 · all 12 are free to read, no account. Course contents