Making data outlive the program
Close the program and every variable disappears. Memory is a whiteboard that gets wiped when the lights go out. To keep anything, you write it to a file on disk.
with open("note.txt", "w", encoding="utf-8") as f:
f.write("first line\n")
f.write("second line\n")
Nothing prints. A file named note.txt now sits next to your script, containing two lines. Piece by piece:
open(filename, mode, encoding)opens the file and hands back a file object."w"is write mode, and it erases whatever was in the file first. Use"a"to append to the end,"r"to read without changing anything.encoding="utf-8"spells out how characters are stored. Write it every time. Leave it off and your program behaves differently on different machines, which is the worst kind of bug.\nis the newline character. Without it, both writes land on the same line —f.writeadds nothing of its own, unlikeprint.with ... as fcloses the file automatically when the block ends, even if an error interrupts it. Always usewith. Hand-writingopenandclosemeans one earlyreturnor one exception leaves the file open and your data possibly unwritten.
Reading it back
with open("note.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
Output:
first line
second line
read() pulls the entire file in as one big string. The blank line at the end is real — the final \n you wrote is still in there, and print adds one more.
For a large file, go line by line instead, so you never hold the whole thing in memory:
with open("note.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
Output:
first line
second line
Looping over a file object gives you one line at a time, each still carrying its trailing \n. strip() removes whitespace from both ends, which is why this version has no extra blank line. Nearly every time you read lines from a file, you want .strip().
Errors are information, not insults
When Python fails, it prints a traceback. It looks intimidating, so people skim it. Don't. Read it from the bottom up. The last line names the error type and the reason. The line above it points at the file and line number. Those two lines carry almost all the information.
The types you'll meet first:
| Error | What it means | Usual cause |
|---|---|---|
SyntaxError | Python couldn't parse the text | Missing colon, unbalanced parentheses or quotes |
IndentationError | The whitespace doesn't line up | Mixed tabs and spaces |
NameError | Used a name that doesn't exist | Typo, or using a variable before assigning it |
TypeError | Wrong kind of thing | Adding a string to a number |
ValueError | Right kind, impossible value | int("abc") |
IndexError | Index past the end | Item 4 of a 3-item list |
KeyError | No such key in the dictionary | Misspelled key name |
FileNotFoundError | The file isn't there | Wrong path, or wrong working directory |
TypeError versus ValueError is worth a second look, because they sound alike. int("abc") is a ValueError — a string is a perfectly acceptable thing to hand int(), but those particular characters can't become a number. int(["a"]) is a TypeError — a list is not something int() accepts at all.
A note on FileNotFoundError: open("note.txt") looks for the file in the directory your program is running from, not the directory the script lives in. If a file you can see isn't being found, that mismatch is usually why.
Catching errors on purpose
Some failures aren't bugs in your code. They're a person typing something unexpected. Those should be handled, not allowed to crash the program:
try:
age = int(input("Age: "))
print(f"Next year you turn {age + 1}")
except ValueError:
print("Please type a number, not words")
Typing abc gives:
Age: abc
Please type a number, not words
The code inside try runs normally. The moment a ValueError is raised, Python abandons the rest of the try block and jumps to except. The program survives.
The fuller form:
try:
f = open("data.txt", "r", encoding="utf-8")
except FileNotFoundError:
print("No such file. I'll create one for you.")
else:
print("Opened it")
f.close()
finally:
print("Done either way")
If data.txt doesn't exist:
No such file. I'll create one for you.
Done either way
else runs only when nothing went wrong. finally runs no matter what — errors, no errors, even an early return. It's for cleanup.
Never write a bare except:. It swallows every error, including your own typos, and turns a clear message into a program that silently does the wrong thing. You'll spend an evening hunting a bug that would have announced itself on line one. Catch the specific type you actually expect.
JSON: saving structures, not text
A text file holds characters. Lists and dictionaries are not characters, so you need a format that can represent them. JSON is that format, and it's the same one used by nearly every web API.
import json
data = {"name": "Maya", "scores": [88, 92]}
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
with open("data.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["name"])
print(loaded["scores"][1])
Output:
Maya
92
import json pulls in a module from Python's standard library — code that ships with Python and is ready to use. json.dump writes a dictionary or list to a file. json.load reads one back, rebuilt into real Python dictionaries and lists.
indent=2 formats the file with line breaks and indentation so a human can open it and read it. Leave it off and you get one enormous line. The file on disk looks like this:
{
"name": "Maya",
"scores": [
88,
92
]
}
Two things JSON can't do: it has no idea what a Python set or a datetime is. Convert those to a string or a list before saving. That's why the project in the next lesson stores dates as text.
Ten-second check
You open a file that already has content, using mode "w". Is the old content still there?
No. It's gone, immediately, the moment the file opens — before you write a single byte. To keep what's there and add to the end, use "a".
Everyone loses a file to this once. Reading it here instead is the cheaper option.