Skip to content
VLSI Mentor

Python Part I · Module 15 · File Handling

Opening and Reading a File Safely

Getting the contents of a file into a Python string takes one statement, and that statement contains a keyword this chapter will use constantly and deliberately not explain. The mechanics are genuinely small — a function, an object, a method. What is not small is the number of ways a perfectly correct read can fail on a machine other than yours, and most of them come down to a single assumption about where the file is that the language resolves differently from how people read it.

Foundation15 min readPythonPart IFundamentalsFilesopen

Part I · Module 15 · Chapter 15.2 · Opening and Reading a File Safely

1. The Whole Thing

Azvya Education Pvt. Ltd.VLSI Mentor
sim.log
Compile start
UVM_INFO top.sv(42) smoke begin
UVM_ERROR drv.sv(88) bad handshake
Simulation complete
Azvya Education Pvt. Ltd.VLSI Mentor
read.py
with open("sim.log") as f:
    text = f.read()

print(type(text).__name__, len(text))
print(text)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
str 101
Compile start
UVM_INFO top.sv(42) smoke begin
UVM_ERROR drv.sv(88) bad handshake
Simulation complete

Three things happened, and it is worth naming each:

  1. open("sim.log") located the file and returned a file object.
  2. f.read() pulled its entire contents back as one string.
  3. with closed the file when the block ended.

Note str 101: read() returns a single string of 101 characters, not a list of lines. Turning it into lines is a separate step, and Chapter 15.4 gives you three ways to do it.

2. The File Object Is Not the Contents

A common early confusion:

Azvya Education Pvt. Ltd.VLSI Mentor
confuse.py (BROKEN ON PURPOSE)
with open("sim.log") as f:
    print(len(f))
Azvya Education Pvt. Ltd.VLSI Mentor
sim.log
Compile start
Simulation complete
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "confuse.py", line 2, in <module>
    print(len(f))
          ~~~^^^
TypeError: object of type '_io.TextIOWrapper' has no len()

f is a handle — a thing you can ask for data — not the data itself. Its type name, TextIOWrapper, says what it is: a wrapper that turns bytes on a disk into text on demand. Chapter 15.3 explains the wrapping.

You get data by calling a method on the handle. f.read() is the one that returns everything.

3. with Is the Form You Always Write

Every example in this module opens a file like this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
with open("sim.log") as f:
    text = f.read()

with guarantees the file is closed when the block ends — whether the block finished normally or raised. Chapter 15.5 is entirely about what that guarantee is and why it matters, so this chapter will simply use it.

For now, one rule, no exceptions:

Never call open() outside a with statement.

The alternative exists and Chapter 15.5 shows it, along with the specific damage it does on a shared compute machine.

4. The Path Is Just Text

open takes a path, and a path is an ordinary string that the operating system knows how to interpret:

Azvya Education Pvt. Ltd.VLSI Mentor
paths.py
name = "sim"
path = f"{name}.log"
print("opening:", path)

with open(path) as f:
    first_20 = f.read()[:20]

print(repr(first_20))
Azvya Education Pvt. Ltd.VLSI Mentor
sim.log
Compile start
UVM_INFO top.sv(42) smoke begin
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
opening: sim.log
'Compile start\nUVM_IN'

Building the path with an f-string works because it is just a string. Module 18 introduces pathlib, which treats paths as objects and is better for anything non-trivial; for a single filename, text is fine.

Note the repr output again: the newline between the two lines is a character inside the string, sitting between start and UVM. It is not a list boundary. That is Chapter 15.3's subject.

5. Reading a Real Log

Putting Chapter 15.1's verdict function onto an actual file — the substitution that chapter promised:

Azvya Education Pvt. Ltd.VLSI Mentor
run.log
Chronologic VCS simulator copyright 1991-2024
UVM_INFO @ 0: reporter [RNTST] Running test smoke_test...
UVM_ERROR drv.sv(88) @ 1420: uvm_test_top.env.agt.drv [DRV] bad handshake
UVM_INFO @ 9180: reporter [FINISH] *** TEST FAILED ***
Azvya Education Pvt. Ltd.VLSI Mentor
verdict.py
def verdict(lines):
    for line in lines:
        if "TEST FAILED" in line:
            return "FAIL"
        if "TEST PASSED" in line:
            return "PASS"
    return "NO_VERDICT"


with open("run.log") as f:
    text = f.read()

lines = text.splitlines()
print("lines:", len(lines))
print("verdict:", verdict(lines))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines: 4
verdict: FAIL

The function is character-for-character the one from Chapter 15.1. Only its input changed, from a literal list to a file. That is the entire point of the module, and everything after this is refinement.

Note splitlines() rather than split("\n"), for the reason Chapter 15.1 Section 8 measured.

6. Where Does It Look?

This is the detail that produces "it works on my machine".

open("sim.log") does not look next to your script. It looks in the working directory — the directory your shell was in when the script started:

Azvya Education Pvt. Ltd.VLSI Mentor
whereami.py
with open("sim.log") as f:
    print("found it:", len(f.read()), "characters")
Azvya Education Pvt. Ltd.VLSI Mentor
sim.log
Compile start
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
found it: 14 characters

That run worked because the harness ran the script from the directory holding both files. Run the identical script from one level up and it fails, because sim.log is resolved from wherever you are:

Azvya Education Pvt. Ltd.VLSI Mentor
missing.py (BROKEN ON PURPOSE)
with open("sim.log") as f:
    print(len(f.read()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "missing.py", line 1, in <module>
    with open("sim.log") as f:
         ~~~~^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'sim.log'

This is the third time this fact has appeared: Chapter 13.5 Section 6 found it for imports, Chapter 14.3 Section 7 named it as the usual cause of FileNotFoundError, and here it is with a file in hand.

7. The Two Failures Worth Recognising

Azvya Education Pvt. Ltd.VLSI Mentor
sim.log
Compile start
Azvya Education Pvt. Ltd.VLSI Mentor
failures.py
import os

os.mkdir("runs")

for path in ["sim.log", "missing.log", "runs"]:
    try:
        with open(path) as f:
            print(f"{path:<12} read {len(f.read())} characters")
    except FileNotFoundError:
        print(f"{path:<12} no such file")
    except IsADirectoryError:
        print(f"{path:<12} that is a directory, not a file")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sim.log      read 14 characters
missing.log  no such file
runs         that is a directory, not a file

Both are OSError subclasses, and both are routinely mishandled as "the file is bad". They are different diagnoses:

  • FileNotFoundError — the tool never produced it, or you are in the wrong directory.
  • IsADirectoryError — the name exists but is the wrong kind of thing, usually a path built with a missing final component.

Chapter 15.8 asks these questions without opening anything, which is often what you want. (os.mkdir appears here only to create something to fail on; Module 18 covers directory creation properly.)

8. An Empty File Reads Successfully

A file with no content is not an error, and the difference between "no file" and "no content" is one your parser has to make:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Azvya Education Pvt. Ltd.VLSI Mentor
empty.py
with open("empty.log") as f:
    text = f.read()

print("read ok    :", repr(text))
print("length     :", len(text))
print("falsy      :", not text)
print("splitlines :", text.splitlines())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
read ok    : ''
length     : 0
falsy      : True
splitlines : []

Nothing raised. read() returned an empty string, splitlines() returned an empty list, and a for loop over the file would run zero times.

That is the correct behaviour and it is also a trap, because a parser written as "count the errors and report PASS if there are none" returns PASS for a file with nothing in it at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Azvya Education Pvt. Ltd.VLSI Mentor
verdict_empty.py
def verdict(path):
    errors = 0
    with open(path) as f:
        for line in f:
            if "UVM_ERROR" in line:
                errors += 1
    return "FAIL" if errors else "PASS"


print("empty log verdict:", verdict("empty.log"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
empty log verdict: PASS

A simulator that died at startup — licence refused, library missing — leaves exactly this file. The three states a real script must separate are missing, empty and has content, and Chapters 15.8 and 15.10 are about making that distinction properly. For now, note that open() succeeding tells you the file exists, and nothing more.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
four.log
alpha
beta
gamma
Azvya Education Pvt. Ltd.VLSI Mentor
predict.py
with open("four.log") as f:
    text = f.read()

print("A:", repr(text))
print("B:", len(text))
print("C:", len(text.splitlines()))
print("D:", text[0], text[5])

with open("four.log") as f:
    a = f.read()
    b = f.read()
print("E:", len(a), len(b))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 'alpha\nbeta\ngamma\n'
B: 17
C: 3
D: a 

E: 17 0

A — one string, with the separators visible. Three lines of 5, 4 and 5 characters plus three newlines is 17.

D prints a and then a newline character, because text[5] is the separator — which is why the output has a stray blank line in it. A file's newlines are ordinary characters at ordinary indexes.

E is the one that surprises people. The second f.read() returned an empty string. A file object remembers its position; the first read consumed everything, so there was nothing left. Reading twice does not give you the file twice.

That position is also why Chapter 15.7's line-by-line iteration works at all.

10. Exercises

Exercise 1 — read one. Create a three-line file and read it with f.read(). Print its type, its length, and its repr. Account for every character in the length.

Exercise 2 — handle versus data. Call len() on the file object and record the error. Say in one sentence what f actually is.

Exercise 3 — substitute the source. Take any function you wrote in Module 14 that processed a list of strings and feed it a real file via splitlines(). Confirm you changed nothing inside the function.

Exercise 4 — the working directory. Put a script and its data file in a folder. Run the script from inside the folder, then from its parent. Record both outcomes and explain the difference.

Exercise 5 — two failures. Produce FileNotFoundError and IsADirectoryError deliberately. Write the one-sentence diagnosis you would put in a handler for each.

Exercise 6 — the exhausted handle. Read a file twice inside one with block. Explain the second result, then find a way to get the contents twice.

Exercise 7 — build a path. Use an f-string to build a filename from a test name and a seed, and open it. Say what Module 18 would do differently.

Exercise 8 — count the separators. For a file of n lines that ends with a newline, write a formula for len(text) in terms of the line lengths. Verify it on a file you create.

11. Summary

  • open(path) returns a file object, a handle — not the file's contents.
  • f.read() returns the whole file as one string, not a list of lines.
  • Always open inside with; Chapter 15.5 explains what it guarantees.
  • A path is ordinary text, and can be built with an f-string.
  • A relative path is resolved from the working directory, not from the script's own directory — the same rule imports follow.
  • FileNotFoundError and IsADirectoryError are different diagnoses: one means it is not there, the other means it is the wrong kind of thing.
  • A file object remembers its position, so a second read() returns an empty string.
  • Newlines are characters inside the string, at ordinary indexes.
  • An empty file reads successfully, returning '' — which is a different state from a missing file, and one a naive parser reports as a pass.
  • Use splitlines() to turn file text into lines.

The next chapter looks at what open did to the bytes on the way in — the decoding step, and the newline you have been seeing in every repr.

Where this fits

Part of the Python curriculum.