Files
Problem
The program forgets
You are writing a program for your expenses. You enter them in the evening and close it, and the next day it knows nothing about yesterday. Here is the shortest example:
visits = 0visits += 1print(f"Started {visits} time.")Started 1 time.No matter how many times you run it, it says "1 time". Variables live in memory while the program runs. When it ends, the memory is freed and everything disappears.
To remember, the program has to write the data where it stays after the program ends: in a file on the disk.
The idea
A file with open and with
Here is a whole program that writes a note to a file and reads it back:
try: with open("notes.txt", "w", encoding="utf-8") as file: file.write("Buy milk\nPay the power bill by the 15th\n") with open("notes.txt", encoding="utf-8") as file: text = file.read() print(text, end="")except OSError as e: print(f"Problem with the file: {e}")Buy milkPay the power bill by the 15thopen(name, mode)opens the file."notes.txt"is a file with this name in the folder the program was started from. The mode"w"(write) is for writing: it creates the file if it is not there, and empties it if it is. Without a mode, the file is opened for reading.with ... as file:gives you the open file under the namefileand closes it by itself when the indented lines end. You will see later why closing matters.file.write(text)writes, andfile.read()reads the whole file as one string.encoding="utf-8"says how letters become bytes. Always write it: without it, Python uses the computer's encoding, and on Windows that is often not UTF-8, so letters like "é" or "€" come out garbled.OSErroris the general exception for a problem with a file: the disk is full, the file is locked or missing. Python does not make you catch it (the Java compiler requires it), but withoutexceptthe program stops.
Run the program, then open the folder: notes.txt is there, and you can open it with any text editor. The next run will find it.
The idea
Add and read the lines
Expenses are added one by one. The mode "a" (append) adds at the end, instead of writing the file from scratch. read().splitlines() splits the text you read into lines:
try: with open("expenses.txt", "w", encoding="utf-8") as file: file.write("Lunch;12.50\n") with open("expenses.txt", "a", encoding="utf-8") as file: file.write("Taxi;8.20\n") with open("expenses.txt", "a", encoding="utf-8") as file: file.write("Book;18.90\n") with open("expenses.txt", encoding="utf-8") as file: lines = file.read().splitlines() total = 0 for line in lines: parts = line.split(";") total += float(parts[1]) print(f"{parts[0]}: €{parts[1]}") print(f"{len(lines)} expenses, total €{total:.2f}")except OSError as e: print(f"Problem with the file: {e}")Lunch: €12.50Taxi: €8.20Book: €18.903 expenses, total €39.60lines is a list of strings, one for each line, without the new line character. You go through it with for, and len(lines) is the number of lines. There is more about lists in the next module, "Collections".
More precisely, if you are curious
"a"creates the file if it is not there, so the first append gives no error.read()reads the whole file into memory. For a file of several gigabytes it is better to read line by line. See the card onfor line in file.- In other people's code you will also see
file.readlines(). It also gives a list of the lines, but each one keeps its"\n"at the end.
Try it
Two shifts
What will the program print?
try: with open("shift.txt", "w", encoding="utf-8") as file: file.write("First shift: Ivo\n") with open("shift.txt", "w", encoding="utf-8") as file: file.write("Second shift: Mila\n") with open("shift.txt", encoding="utf-8") as file: print(file.read(), end="")except OSError as e: print(e)"w" empties the file as soon as it opens it, and then writes the new contents. The first line is gone even before the reading. This is the most common reason for "my file got erased".
The idea
When the file is not there
The program's settings are in config.txt, and someone deleted it. If nobody catches the exception:
def read_config(): with open("config.txt", encoding="utf-8") as file: return file.read()
print(read_config())Traceback (most recent call last): File "/app/app.py", line 6, in <module> print(read_config()) ^^^^^^^^^^^^^ File "/app/app.py", line 2, in read_config with open("config.txt", encoding="utf-8") as file: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^FileNotFoundError: [Errno 2] No such file or directory: 'config.txt'Python orders the calls the opposite way from Java: at the top is the outermost one, line 6, where read_config() is called. Below it is line 2 in read_config, where open cannot find the file. At the very bottom is the error itself: FileNotFoundError and the message. That is why it says "most recent call last" at the start: the last call is at the bottom.
FileNotFoundError is a subclass of OSError. Catch it separately, before the general case:
import os
def read_config(): with open("config.txt", encoding="utf-8") as file: return file.read()
try: print(read_config())except FileNotFoundError as e: print(f"No file {e.filename}. Using the default settings.")except OSError as e: print(f"Cannot read the settings: {e}")print(os.path.exists("config.txt"))No file config.txt. Using the default settings.Falsee.filename is the name of the missing file. os.path.exists(path) checks whether the file exists. The check does not replace except: the file may disappear between the check and the reading, and there are other reasons the reading may fail.
The idea
Line by line and with
The file from open can be written in parts and read line by line, without the whole thing being in memory. It keeps the file open while you work with it, so it has to be closed at the end:
try: with open("report.txt", "w", encoding="utf-8") as out: for day in range(1, 4): out.write(f"Day {day}: €{day * 120}\n")except OSError as e: print(f"Cannot write: {e}")
try: with open("report.txt", encoding="utf-8") as file: for line in file: print(line.rstrip("\n"))except OSError as e: print(f"Cannot read: {e}")Day 1: €120Day 2: €240Day 3: €360writewrites text. A new line does not come by itself: you write the\n.for line in filegives the lines one by one, each with its\nat the end.rstrip("\n")removes it, otherwiseprintwould put an empty line between them.withcloses the file when the indented lines end, in whatever way: even with an exception.
Closing is not a formality: while the file is open, the text may still be in its buffer in memory, not on the disk. To keep the code short, there is no try here:
import os
with open("report.txt", "w", encoding="utf-8") as out: for day in range(1, 4): out.write(f"Day {day}: €{day * 120}\n") print(f"In the block: {os.path.getsize('report.txt')} bytes")print(f"After the block: {os.path.getsize('report.txt')} bytes")with open("report.txt", encoding="utf-8") as file: for line in file: print(line.rstrip("\n"))Step by step, in words
- os has the function for the size of the file. On the left is the memory of the program, on the right is the disk.
- open with "w" creates the empty file report.txt and a file object with a buffer in memory.
- write writes into the buffer. Nothing reaches the disk yet.
- The file on the disk is still 0 bytes.
- The with block ends and closes the file. The buffer goes to the disk: the file becomes 42 bytes.
- 42 bytes for 36 characters with the new lines: in UTF-8 each Latin letter is 1 byte, and € is 3.
- for line in file reads the three lines back from the disk.
We promised this in the lesson on finally. Here is the same writing without with, using finally:
out = open("report.txt", "w", encoding="utf-8")try: out.write("Day 1: €120\n")finally: out.close()with does exactly this for you: it closes the file, whatever happens in the block.
More precisely, if you are curious
- One
withcan hold several files, separated with a comma:with open("app.log", encoding="utf-8") as src, open("errors.log", "w", encoding="utf-8") as out:. - Buffer. The file collects the text in memory and sends it to the disk in big chunks. Each separate write to the disk is slow, and this way there are few of them.
file.flush()sends the buffer right away, without closing the file. - Bytes and characters. Underneath, a file is a sequence of bytes. The modes
"rb"and"wb"read and write bytes: for photos, archives, anything that is not text. Withoutbthe file is a text file, and it turns bytes into characters by theencoding. file.readline()reads one line. When the file ends, it returns an empty string"". An empty line in the file is"\n", so the two do not get mixed up.
Try it
When does it close?
Any class with the methods __enter__ and __exit__ can go in a with. __enter__ is called at the start of the block, and __exit__ at its end, even with an exception. Here is the receipt printer from the lesson on finally, as a resource:
What will the program print?
class Printer: def __enter__(self): print("The printer is busy.") return self
def __exit__(self, exc_type, exc, tb): print("The printer is free.")
try: with Printer(): print("Printing the receipt...") qty = int("three") print(f"Quantity: {qty}")except ValueError: print("Wrong quantity.")int("three") throws ValueError, and the "Quantity" line is skipped. On the way out of with, Python calls __exit__ right away, even before except. That is why "free" comes before "Wrong quantity." By the time except runs, the printer is already released.Try it
Find the bug
0. Which line has the bug?Pick the line with the bug.
with and is not closed. The text is still in the buffer in memory and has not reached the disk, so at the reading the file is empty. The program gives no error. It quietly counts wrong. Open it with with open(...) as out: and read the file after the block: then it is closed and the line is there.The idea
Folders
The register's backups go into a folder, one per day:
import os
backup = "backup"try: os.makedirs(os.path.join(backup, "old"), exist_ok=True) with open(os.path.join(backup, "2026-10-01.csv"), "w", encoding="utf-8") as file: file.write("Café,2.40\n") with open(os.path.join(backup, "2026-10-02.csv"), "w", encoding="utf-8") as file: file.write("Café,2.40\nTea,1.80\n") for name in sorted(os.listdir(backup)): p = os.path.join(backup, name) if os.path.isdir(p): print(f"{name}/") else: print(f"{name} ({os.path.getsize(p)} bytes)") os.remove(os.path.join(backup, "2026-10-01.csv")) print(os.path.exists(os.path.join(backup, "2026-10-01.csv")))except OSError as e: print(e)2026-10-01.csv (11 bytes)2026-10-02.csv (20 bytes)old/False| Function | What it does |
|---|---|
os.makedirs(p, exist_ok=True) |
creates the folder and all the folders above it, if they are not there |
os.path.join(dir, "name") |
the path to name inside dir |
os.path.isdir(p), os.path.exists(p) |
is it a folder, is it there |
os.path.getsize(p) |
the size in bytes |
os.remove(p) |
deletes a file (os.rmdir(p) deletes an empty folder) |
os.listdir(dir) |
a list with the names of everything in the folder |
The sizes are 11 and 20 bytes, though there are fewer characters: in UTF-8, the "é" in "Café" is 2 bytes.
os.listdir gives the names in no particular order, so we sort them with sorted.More precisely, if you are curious
os.rmdirdeletes only an empty folder. Otherwise it givesOSError: [Errno 39] Directory not empty.shutil.rmtree(p)(theshutilmodule) deletes the folder with everything in it, so be careful with it.shutil.copy(src, dst)copies a file, andos.rename(src, dst)moves or renames it.- In a lot of code you will see the
pathlibmodule:Path("backup") / "old"is the path to the folderoldinbackup, andp.exists()andp.read_text(encoding="utf-8")are methods of the path. It is the same thing, with objects instead of functions. - The path
"notes.txt"is relative: to the folder the program was started from."/home/ana/notes.txt"is absolute.os.path.abspath("notes.txt")shows exactly where the file is.
Exercise
Programs that remember
Solve the tasks in your editor. In each task, the program first creates the input file itself with open(..., "w"), so you can try it right away.
1. A configuration file. Create server.conf with these contents, then read it and print each setting as key: value. Skip the empty lines and the lines that start with #. Hint: split("=", 1) splits only at the first = (the number is how many times to cut).
# server settingsmax_connections=500log_file_path=/var/log/server.log
db_timeout=30max_connections: 500log_file_path: /var/log/server.logdb_timeout: 302. A log file filter. Create app.log with four lines (two INFO and two ERROR). Read a level from the keyboard, for example ERROR, and write the lines with that level to errors.log line by line, with both files in one with. Print errors.log and how many lines you wrote.
ERRORERROR: Failed to load configurationERROR: Null pointer exceptionWrote 2 lines to errors.log3. Folder contents. Create a folder project with the files document.txt, image.jpg, script.js and the folder backup. Write their names, sorted in alphabetical order, to directory_contents.txt, with a / after the names of folders. Print the file.
backup/document.txtimage.jpgscript.js4. An index file. Create text.txt with the three lines below. Read a word from the keyboard and write to index.txt each place it appears, in the form line:position: the line number (from 1) and the index in the line (from 0). If the word is on one line twice, that is two entries.
hello worldworld hellohello again, hellohello1:02:63:03:13More tasks
- Run counter. Fix the program from the first card: count the runs in
visits.txt. If the file is not there, start from 0. - Find and replace in a file. The task from the lesson "Search and replace": read a file, replace "old" with "new", and write the result to a new file.
- Dates from a file. The date task from the lesson "Regular expressions": read the dates from a file, one per line.
- A copy with line numbers. Read a file and write a copy in which each line starts with its number:
1: ...,2: ....
Test