Skip to content

Files and exceptions

~25 min · prerequisite: Modules and imports

What you will be able to do

  • Write text to a file and read it back, using with open(...).
  • Catch an error with try and except, so the program can keep running.
  • Practise file reading and writing in a runnable block, using io.StringIO.

The idea

A file is a named collection of data stored on your disk. A text file holds characters, such as lines of words. Python works with files through a file object, which you get from the built-in function open. The first argument is the file name, and the second is the mode. The mode "r" means read, "w" means write and replaces any old contents, and "a" means append to the end.

Use the with statement, with open(...) as file:, and put the work on the file in the indented lines. When the block ends, Python closes the file, even if an error happened inside. You should always pass encoding="utf-8" for text, so that letters such as é are stored the same way on every computer.

Writing to a file is different from print. The write method takes one string, and it does not add a newline at the end. If you want separate lines, write "\n" yourself. Reading gives you the text back. You can loop over the file with for line in file, and each line keeps its newline at the end.

Errors are called exceptions. When Python meets an error it cannot handle, it stops the program and shows a traceback. You can catch an exception with a try block. The code inside try runs first. If it raises a matching exception, the code under except runs instead of stopping the program. Name the exception you expect, such as ValueError, so that you do not hide other problems.

Runnable blocks in this course cannot create real files, so the file examples below are plain code. The io.StringIO class gives you an in-memory file that behaves like a text file. You can write to it, then read it back in the same way.

Worked example

This block writes two lines to a file named notes.txt, then reads them back. Run it in an empty folder.

with open("notes.txt", "w", encoding="utf-8") as file:
    file.write("first line\n")
    file.write("second line\n")

with open("notes.txt", encoding="utf-8") as file:
    for line in file:
        print(line.rstrip("\n"))

Expected output:

first line
second line

Line by line, this is what Python does:

  1. open("notes.txt", "w", encoding="utf-8") creates the file, or replaces it if it exists, and opens it for writing.
  2. Each file.write call adds one line, and the \n at the end makes the next text start on a new line.
  3. The first with block ends, and Python closes the file. The text is now saved on disk.
  4. The second open uses the default mode "r", which means read.
  5. for line in file gives one line at a time. Each line still ends with a newline.
  6. line.rstrip("\n") removes that newline, so print does not add an extra blank line.

Try it

The first block writes and reads with io.StringIO. The second block loops over lines. The third block uses try and except to handle bad input.

import io

buffer = io.StringIO()
buffer.write("first line\n")
buffer.write("second line\n")
assert buffer.getvalue() == "first line\nsecond line\n"

buffer.seek(0)
assert buffer.readline() == "first line\n"
assert buffer.read() == "second line\n"
import io

source = io.StringIO("apple\nbanana\ncherry\n")
lines = [line.rstrip("\n") for line in source]
assert lines == ["apple", "banana", "cherry"]
def parse_number(text):
    try:
        return int(text)
    except ValueError:
        return None


assert parse_number("42") == 42
assert parse_number("forty") is None

Common mistakes

  1. Opening a file that does not exist. The following code reads a file that is not in the folder:

    with open("missing.txt", encoding="utf-8") as file:
        print(file.read())
    

    Python reports:

    FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
    

    Check the file name and the folder you run the script from.

  2. Writing a number instead of a string. The following code passes an int to write:

    with open("count.txt", "w", encoding="utf-8") as file:
        file.write(42)
    

    Python reports:

    TypeError: write() argument must be str, not int
    

    Convert the value first, with file.write(str(42)).

  3. Forgetting the newline. The following code runs without an error, but the two lines run together:

    with open("joined.txt", "w", encoding="utf-8") as file:
        file.write("first")
        file.write("second")
    

    The file now holds firstsecond on one line. Add "\n" to the end of each line you write.

  4. Using a file after its with block. The following code reads the file after it is closed:

    with open("notes.txt", "w", encoding="utf-8") as file:
        file.write("hi\n")
    
    file.read()
    

    Python reports:

    ValueError: I/O operation on closed file.
    

    Do all the reading inside the with block, and store the result in a variable.

Practice

  1. Create an io.StringIO object, write the word "hello" to it, and assert that getvalue() equals "hello".
  2. Define count_lines(text) that uses io.StringIO(text) and counts the lines with a for loop. Assert that count_lines("one\ntwo\nthree\n") equals 3.
  3. Define safe_int(text, default=0) that returns int(text), or default when the conversion raises ValueError. Assert that safe_int("12") equals 12, that safe_int("x") equals 0, and that safe_int("x", default=-1) equals -1.
  4. Define total_score(source) where source is a file-like object with lines such as "Ada 90". Return the sum of the numbers. Assert that total_score(io.StringIO("Ada 90\nGrace 85\n")) equals 175.
  5. Stretch: define average(numbers) that raises ValueError("no numbers") for an empty list, and otherwise returns the mean. Define safe_average(numbers) that returns None when average raises that error. Assert that safe_average([2, 4]) equals 3.0, and that safe_average([]) is None.
Solution
  1. ```python import io

buffer = io.StringIO() buffer.write("hello") assert buffer.getvalue() == "hello" ```

  1. ```python import io

def count_lines(text): total = 0 for line in io.StringIO(text): total = total + 1 return total

assert count_lines("one\ntwo\nthree\n") == 3 ```

  1. ```python def safe_int(text, default=0): try: return int(text) except ValueError: return default

assert safe_int("12") == 12 assert safe_int("x") == 0 assert safe_int("x", default=-1) == -1 ```

  1. ```python import io

def total_score(source): total = 0 for line in source: name, score = line.split() total = total + int(score) return total

assert total_score(io.StringIO("Ada 90\nGrace 85\n")) == 175 ```

  1. ```python def average(numbers): if not numbers: raise ValueError("no numbers") return sum(numbers) / len(numbers)

def safe_average(numbers): try: return average(numbers) except ValueError: return None

assert safe_average([2, 4]) == 3.0 assert safe_average([]) is None ```

Recap

  • with open(name, mode, encoding="utf-8") as file: opens a file and closes it when the block ends. "w" writes, and "r" reads.
  • write does not add a newline, and for line in file keeps the newline at the end of each line.
  • try and except ValueError catch a specific error, so the program can handle it and continue.

Sources