Skip to content
wannacode

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 = 0
visits += 1
print(f"Started {visits} time.")
Output
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:

notes.py
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}")
Output
Buy milk
Pay the power bill by the 15th
  • open(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 name file and closes it by itself when the indented lines end. You will see later why closing matters.
  • file.write(text) writes, and file.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.
  • OSError is 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 without except the 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}")
Output
Lunch: €12.50
Taxi: €8.20
Book: €18.90
3 expenses, total €39.60

lines 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 on for 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)
Answer

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:

app.py
def read_config():
with open("config.txt", encoding="utf-8") as file:
return file.read()
print(read_config())
Error
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"))
Output
No file config.txt. Using the default settings.
False

e.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}")
Output
Day 1: €120
Day 2: €240
Day 3: €360
  • write writes text. A new line does not come by itself: you write the \n.
  • for line in file gives the lines one by one, each with its \n at the end. rstrip("\n") removes it, otherwise print would put an empty line between them.
  • with closes 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
  1. 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.
  2. open with "w" creates the empty file report.txt and a file object with a buffer in memory.
  3. write writes into the buffer. Nothing reaches the disk yet.
  4. The file on the disk is still 0 bytes.
  5. The with block ends and closes the file. The buffer goes to the disk: the file becomes 42 bytes.
  6. 42 bytes for 36 characters with the new lines: in UTF-8 each Latin letter is 1 byte, and € is 3.
  7. 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 with can 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. Without b the file is a text file, and it turns bytes into characters by the encoding.
  • 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.")
Answer

Try it

Find the bug

The program writes an order and counts the lines in the file. It prints 0. Which line has the bug?

Pick the line with the bug.

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)
Output
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.rmdir deletes only an empty folder. Otherwise it gives OSError: [Errno 39] Directory not empty. shutil.rmtree(p) (the shutil module) deletes the folder with everything in it, so be careful with it.
  • shutil.copy(src, dst) copies a file, and os.rename(src, dst) moves or renames it.
  • In a lot of code you will see the pathlib module: Path("backup") / "old" is the path to the folder old in backup, and p.exists() and p.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.conf
# server settings
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Output
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. 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.

Input
ERROR
Output
ERROR: Failed to load configuration
ERROR: Null pointer exception
Wrote 2 lines to errors.log

3. 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.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. 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.

text.txt
hello world
world hello
hello again, hello
Input
hello
index.txt
1:0
2:6
3:0
3:13

More 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

Check yourself

1. open("config.txt", encoding="utf-8") cannot find the file, and there is no try around it. What happens?
2. You open a file with "w" and write "A\n". Then you open it with "w" again and write "B\n". What is in the file?
3. When does with close the file?
4. What does file.readline() return when the file ends?
5. You open a file with open() without with, and you do not close it. What may be in the file while the program is still running?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·