Writing and exporting files
So far we have read data from files. Programs also write files: for example, to preserve transformed data, produce a report, or pass results to another program. Reading and writing use the same open function, but writing requires a second argument which specifies how the file will be used.
Creating and writing a file
The argument "w" opens a file in write mode. It creates the file if it does not exist:
with open("notes.txt", "w") as output_file:
output_file.write("First observation")The with block closes the file automatically after its indented statements have run, just as it does when reading a file.
Unlike print, the write method does not add a line break. These three calls therefore produce a single continuous line:
with open("notes.txt", "w") as output_file:
output_file.write("First observation")
output_file.write("Second observation")
output_file.write("Third observation")Add the newline character \n wherever a line should end:
with open("notes.txt", "w") as output_file:
output_file.write("First observation\n")
output_file.write("Second observation\n")
output_file.write("Third observation\n")The result is:
First observation Second observation Third observation
write expects a string. Convert numbers and other values into text, often by constructing a complete row with an f-string, before writing them.
Constructing rows before exporting them
Suppose a program has transformed catalogue data into named records:
records = [
{"id": "a1", "title": "Harbour map", "year": 1901},
{"id": "a2", "title": "Field notes", "year": 1952}
]A comma-separated file represents each record as one line and each field as one part of that line. The rows can first be constructed in a list:
rows = ["id,title,year"]
for record in records:
row = f"{record['id']},{record['title']},{record['year']}"
rows.append(row)At this point rows is ordinary in-memory data. You can print it or use assertions to check its field order and contents without changing any file. Once it is correct, writing it is a small separate step:
with open("cleaned.csv", "w") as output_file:
for row in rows:
output_file.write(row + "\n")Use this sequence for course exports:
- construct the complete output rows in memory;
- inspect or assert those rows;
- choose a destination different from every source path; and
- write the already verified rows.
This separation makes the data transformation visible before it has a side effect. It also makes testing safer: transformation tests need not touch a file, and export tests can use a temporary destination.
Passing an open file to a helper function
The variable created by as is a file handle: an object through which the program can read or write while the file is open. Like a list or dictionary, this object can be passed as an argument to a function.
For example, this helper knows how to format and write one label, but it does not decide which file to open:
def write_label(output_file, label: str, value: str) -> None:
output_file.write(f"{label}: {value}\n")The caller opens the report once and passes the open file to the helper for each piece of output:
with open("summary.txt", "w") as report_file:
write_label(report_file, "Collection", "Maps")
write_label(report_file, "Records", "24")This division of responsibility is useful when several values belong in the same file. The outer with block controls when the file is opened and closed. The helper controls how one piece is written, without repeatedly reopening the destination.
Appending instead of replacing
The argument "a" opens a file in append mode. New text is added at the end instead of replacing what is already there:
with open("notes.txt", "a") as output_file:
output_file.write("A later observation\n")Append mode is useful for data such as a log or diary where each run intentionally adds a new entry. It is usually wrong for a generated report: running the same export twice would duplicate all of its rows. The exercises below generate complete outputs, so they use write mode and should produce the same destination contents every time they run.
Apply the export pattern to an evolving pipeline
The preceding exercise introduced the complete sequence on a small collection of named records. In a larger program, it is useful to give each stage a separate responsibility:
- one function reads and parses a particular input format;
- another transforms the parsed data into result records;
- formatting functions turn those records into the representations needed for different outputs; and
- a final step writes already checked rows to their destinations.
This structure does more than make the program look tidy. If the grade boundaries change, the grade calculation should have one place to change. If a new output format is needed, it should reuse the existing result records instead of independently recalculating them. Small functions also make it possible to inspect and test a transformation before it writes anything.
The next exercise applies this structure to the Course grading pipeline from the previous section. It produces both a human-readable report and a CSV file from the same calculated student records.
You can check your current points from the blue blob in the bottom-right corner of the page.