File Handling in Python
By the Syncfusion DevTutorials team · Last updated
Opening, the nine modes, three ways to read, why closing matters, the with statement, file errors, and text, CSV and JSON files.
Everything you have written so far has disappeared.
Every mark, every name, every list — gone the moment the program ended. A variable lives in memory, and memory is cleared when the program stops.
A school cannot work that way. The marks have to still be there tomorrow morning. That means writing them somewhere that outlives the program: a file.
Here is that idea in action — a file that keeps its contents after the program has ended:
Something that survives the program
with open("fh_first.txt", "w") as sheet:
sheet.write("Asha,78\n")
sheet.write("Ravi,85\n")
with open("fh_first.txt") as sheet:
print(sheet.read())
A file is storage that outlives your program. Python opens one, hands you an object to read from or write to, and the file must be closed when you are done — which is why with exists.
What You Will Learn
In this lesson, you will learn:
- What a file object is, and why opening one is not the same as reading it
- All nine opening modes, and the one difference that matters most:
wdestroys,aadds - Three ways to read, and when each is the right one
- Why an unclosed file can lose data you thought you had written
- The
withstatement, and why nothing else in this lesson matters as much - How to handle a file that is missing, locked or not what you expected
- Text, CSV and JSON — the three file shapes you will actually meet
By the end of this lesson, you will be able to open files in each mode, read them back three ways, write text safely, explain why closing matters, use with so closing happens on every path, handle missing files, and choose between plain text, CSV and JSON.
Every cell on this page writes its own file before reading it, so you can run any cell on its own and get the same answer. The files live in the browser tab, not on your computer, and they disappear when you reload the page.
Introduction to File Handling
What File Handling Is
File handling is the set of things a program does with files: open, read, write, close. Python builds all of it on one function, open(), and one kind of object, the file object.
File Operations in Python
There are four, and they always happen in this order:
- Open the file, saying what you intend to do with it.
- Read from it, or write to it.
- Close it.
Step 3 is the one people forget, and it is the one that loses data.
Working with Files
Here are the four steps, written out longhand:
The four steps, written out the long way
sheet = open("fh_steps.txt", "w")
sheet.write("Asha,78\n")
sheet.close()
sheet = open("fh_steps.txt", "r")
print(sheet.read())
sheet.close()
That works, and you will see it in older code. Later on this page you will replace all six lines with four better ones.
File Objects
open() does not give you the contents of the file. It gives you a file object: a thing that knows which file it is attached to, what you are allowed to do with it, and where you have got to in it.
A file object is not the text
sheet = open("fh_object.txt", "w")
print(sheet)
print(type(sheet).__name__)
sheet.close()
That position marker is the part beginners miss. Reading a file moves it to the end, and a second read from the same object gives you nothing.
The second read comes back empty
with open("fh_twice.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_twice.txt") as sheet:
print("First read:", repr(sheet.read()))
print("Second read:", repr(sheet.read()))
Nothing is wrong with the file. The marker is simply sitting at the end.
Opening Files
Using the open() Function
The function takes a file name and, optionally, a mode:
file_object = open(filename, mode)
filename is the name of the file. mode says what you intend to do. If you leave the mode out, you get "r" — read.
The mode is optional and defaults to read
with open("fh_default.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_default.txt") as sheet:
print(sheet.read().strip())
File Opening Modes
There are nine modes. The table is the reference; the two that matter most are w and a:
| Mode | Meaning | If the file is missing | If it exists |
|---|---|---|---|
r | read | FileNotFoundError | read from the start |
w | write | created | emptied first |
a | append | created | added to at the end |
x | exclusive create | created | FileExistsError |
r+ | read and write | FileNotFoundError | kept, marker at the start |
w+ | write and read | created | emptied first |
a+ | append and read | created | kept, writes go to the end |
Add b for binary or t for text to any of them. t is the default, so "r" and "rt" are the same thing.
w empties the file the instant it is opened — before you write a single character. If you meant to add to a file and typed w, the old contents are gone, and there is no undo. When in doubt, use a.
File Properties
A file object will tell you about itself.
name, mode and closed
sheet = open("fh_props.txt", "w")
print("name:", sheet.name)
print("mode:", sheet.mode)
print("closed:", sheet.closed)
sheet.write("Asha,78\n")
sheet.close()
print("closed after close():", sheet.closed)
closed is the useful one: it is how you check whether a file still needs closing.
File Modes
Now each mode in turn, with the ones you will actually use first.
Read Mode (r)
Opens an existing file for reading, with the marker at the start. The file must already exist.
Reading a file that is there
with open("fh_read.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\n")
with open("fh_read.txt", "r") as sheet:
print(sheet.read())
If the file is not there, the result is an exception:
Reading a file that is not there
with open("fh_nothing_here.txt", "r") as sheet:
print(sheet.read())
Write Mode (w)
Creates the file if it is missing, and empties it if it is not.
w empties the file first
with open("fh_write.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\nMeera,60\n")
with open("fh_write.txt") as sheet:
print("Before:", repr(sheet.read()))
with open("fh_write.txt", "w") as sheet:
sheet.write("Dev,70\n")
with open("fh_write.txt") as sheet:
print("After: ", repr(sheet.read()))
Three marks went in and one came out. That is w doing exactly what it says.
Append Mode (a)
Creates the file if it is missing, and adds to the end if it is not. Nothing already there is touched.
a adds to the end
with open("fh_append.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_append.txt", "a") as sheet:
sheet.write("Ravi,85\n")
with open("fh_append.txt", "a") as sheet:
sheet.write("Meera,60\n")
with open("fh_append.txt") as sheet:
print(sheet.read())
This is the mode for a log, an attendance register, or anything that only ever grows.
Exclusive Create Mode (x)
Creates the file, and refuses if it already exists. Use it when overwriting would be a disaster.
x refuses to overwrite
import os
if os.path.exists("fh_exclusive.txt"):
os.remove("fh_exclusive.txt")
with open("fh_exclusive.txt", "x") as sheet:
sheet.write("Asha,78\n")
print("Created")
try:
with open("fh_exclusive.txt", "x") as sheet:
sheet.write("Ravi,85\n")
except FileExistsError as error:
print("FileExistsError — refused to overwrite", error.filename)
The first three lines delete the file if it is there, so that you can press Run again and see the same thing.
Binary Mode (b)
Text mode gives you str and handles the encoding for you. Binary mode gives you bytes — the raw contents, untouched. Images, sound and compressed files are all binary.
bytes, not text
with open("fh_binary.bin", "wb") as data:
data.write(b"Asha 78")
with open("fh_binary.bin", "rb") as data:
raw = data.read()
print(type(raw).__name__, raw)
print(raw.decode("utf-8"))
Note the b before the quotes: b"Asha 78" is a bytes literal, not text.
Text Mode (t)
The default. Python decodes the bytes into text for you, and converts line endings so that \n works the same on every operating system. You almost never type t, because leaving it out means the same thing.
Read and Write Mode (r+)
Opens an existing file for both, with the marker at the start. Nothing is emptied.
r+ keeps what is there
with open("fh_rplus.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_rplus.txt", "r+") as sheet:
print("Read first:", repr(sheet.read()))
sheet.write("Ravi,85\n")
with open("fh_rplus.txt") as sheet:
print("Now holds: ", repr(sheet.read()))
The write landed at the end because reading had already moved the marker there. Had you written first, you would have overwritten the start.
Write and Read Mode (w+)
Both, but it empties the file on open. So the first read gives you nothing — there is nothing left to read.
w+ empties first, then lets you read
with open("fh_wplus.txt", "w") as sheet:
sheet.write("old marks\n")
with open("fh_wplus.txt", "w+") as sheet:
print("Read straight after opening:", repr(sheet.read()))
sheet.write("Asha,78\n")
sheet.seek(0)
print("After seek(0):", repr(sheet.read()))
seek(0) moves the marker back to the start. Without it, the read after a write returns nothing.
Append and Read Mode (a+)
Both, nothing is emptied, and every write goes to the end no matter where the marker is.
a+ always writes at the end
with open("fh_aplus.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_aplus.txt", "a+") as sheet:
sheet.write("Ravi,85\n")
sheet.seek(0)
print(sheet.read())
Reading Files
The modes are done. Now the three ways to actually get text out of a file — and the fourth way, the one to prefer.
Reading the Whole File with read()
read() returns the entire file as one string.
read gives you one string
with open("fh_whole.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\nMeera,60\n")
with open("fh_whole.txt") as sheet:
contents = sheet.read()
print(repr(contents))
print(len(contents), "characters")
Good for a small file. A large one goes entirely into memory, so this is not the tool for a file bigger than your computer is comfortable with.
Reading One Line with readline()
readline() returns the next line, including its \n, and moves the marker past it.
readline, one call at a time
with open("fh_lines.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\nMeera,60\n")
with open("fh_lines.txt") as sheet:
print(repr(sheet.readline()))
print(repr(sheet.readline()))
print(repr(sheet.readline()))
print(repr(sheet.readline()))
The last call returns an empty string. That is how readline() says there is nothing left — not None, and not an exception.
Reading All Lines with readlines()
readlines() returns a list, one item per line, each still carrying its \n.
readlines gives you a list
with open("fh_all.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\nMeera,60\n")
with open("fh_all.txt") as sheet:
rows = sheet.readlines()
print(rows)
print(len(rows), "rows")
for row in rows:
name, mark = row.strip().split(",")
print(name, "scored", mark)
Looping Over the File Itself
This is the one you should reach for. A file object can be looped over directly, and it reads one line at a time instead of loading everything.
The usual way to read a file
with open("fh_loop.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\nMeera,60\n")
total = 0
with open("fh_loop.txt") as sheet:
for row in sheet:
name, mark = row.strip().split(",")
total += int(mark)
print(f"{name:<8}{mark:>4}")
print("Total:", total)
Every line you read keeps its \n. .strip() removes it, along with any stray spaces. Forgetting it is why a printed name sometimes has a blank line under it, and why int(mark) sometimes works and sometimes does not.
Writing Files
Reading has three routes; writing is simpler — but the newline catches everyone.
Writing with write()
write() writes exactly the text you give it — and no newline. If you want lines, you add \n yourself.
write adds nothing of its own
with open("fh_nonewline.txt", "w") as sheet:
sheet.write("Asha,78")
sheet.write("Ravi,85")
with open("fh_nonewline.txt") as sheet:
print(repr(sheet.read()))
Two writes, one line. That is almost never what you wanted.
With the newlines put back
with open("fh_newline.txt", "w") as sheet:
sheet.write("Asha,78\n")
sheet.write("Ravi,85\n")
with open("fh_newline.txt") as sheet:
print(sheet.read())
write() also returns the number of characters it wrote, which is occasionally useful and often surprising.
write returns a count
with open("fh_count.txt", "w") as sheet:
written = sheet.write("Asha,78\n")
print("characters written:", written)
Writing Several Lines with writelines()
writelines() takes a list of strings and writes them one after another. Despite the name, it does not add newlines either.
writelines, with and without newlines
rows = ["Asha,78", "Ravi,85", "Meera,60"]
with open("fh_wl_bad.txt", "w") as sheet:
sheet.writelines(rows)
with open("fh_wl_bad.txt") as sheet:
print("Without newlines:", repr(sheet.read()))
with open("fh_wl_good.txt", "w") as sheet:
sheet.writelines(row + "\n" for row in rows)
with open("fh_wl_good.txt") as sheet:
print("With newlines:")
print(sheet.read())
Overwriting against Appending
The whole difference, side by side.
w against a, from the same starting point
for mode in ["w", "a"]:
with open("fh_compare.txt", "w") as sheet:
sheet.write("Asha,78\n")
with open("fh_compare.txt", mode) as sheet:
sheet.write("Ravi,85\n")
with open("fh_compare.txt") as sheet:
print(f"mode {mode!r} leaves:", repr(sheet.read()))
Closing Files
You have been using with since the first example. Here is what it saves you from.
Using close()
close, and what happens after it
sheet = open("fh_close.txt", "w")
sheet.write("Asha,78\n")
print("closed?", sheet.closed)
sheet.close()
print("closed?", sheet.closed)
try:
sheet.write("Ravi,85\n")
except ValueError as error:
print("ValueError:", error)
Why Closing Files Matters
Three reasons, and the first one is the one that bites.
- Your writing may not have happened yet. Python holds recent writes in a buffer and sends them to the file in batches.
close()flushes that buffer. A program that ends badly without closing can leave the file short. - An open file holds a resource. An operating system allows a limited number of open files at once, and a loop that opens thousands without closing them will eventually fail.
- Other programs may be locked out. On some systems a file that one program holds open cannot be opened by another.
You can watch reason 1 happen with your own eyes:
The contents only exist once it is closed
sheet = open("fh_flush.txt", "w")
sheet.write("Asha,78\n")
with open("fh_flush.txt") as check:
print("While still open:", repr(check.read()))
sheet.close()
with open("fh_flush.txt") as check:
print("After close(): ", repr(check.read()))
Run that and look at the first line. The text you "wrote" was not in the file yet.
close() written at the end of a block is not enough. If any line above it raises, the close() is skipped and the file stays open with its buffer unflushed. This is the problem with exists to solve, and it is why you should not write open() and close() by hand.
Context Managers
The with Statement
A context manager is an object that knows how to tidy up after itself. A file is one. with asks it to do that tidying automatically, at the end of the block — whichever way the block ends.
with open(filename, mode) as file_object:
work with file_object
with closes the file for you
with open("fh_with.txt", "w") as sheet:
sheet.write("Asha,78\n")
print("inside the block, closed?", sheet.closed)
print("after the block, closed?", sheet.closed)
Nobody called close(). It happened anyway.
Automatic Closing, Even When Things Go Wrong
This is the part that matters. The file is closed even if the block raises.
Closed even on the way out of an exception
try:
with open("fh_safe.txt", "w") as sheet:
sheet.write("Asha,78\n")
raise ValueError("something went wrong halfway through")
except ValueError as error:
print("Caught:", error)
print("closed?", sheet.closed)
with open("fh_safe.txt") as sheet:
print("and the write survived:", repr(sheet.read()))
The exception escaped, the file was closed on the way out, and the buffer was flushed. Hand-written close() would have been skipped.
Benefits of Context Managers
- You cannot forget to close the file, because you never wrote the close.
- The file is closed on every path out — normal end,
return, or exception. - The block makes it obvious how long the file is open for.
- Several files can be opened in one
with, separated by commas.
Two files in one with
with open("fh_src.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\n")
with open("fh_src.txt") as source, open("fh_dest.txt", "w") as target:
for row in source:
target.write(row.upper())
with open("fh_dest.txt") as target:
print(target.read())
Exception Handling in File Operations
Handling File Errors
Four things go wrong with files, and each has its own exception.
Four file failures, each named
jobs = ["missing", "readonly", "wrongmode", "closed"]
with open("fh_errors.txt", "w") as sheet:
sheet.write("Asha,78\n")
for job in jobs:
try:
if job == "missing":
open("fh_not_a_real_file.txt")
elif job == "readonly":
with open("fh_errors.txt", "w") as sheet:
sheet.read()
elif job == "wrongmode":
with open("fh_errors.txt", "r") as sheet:
sheet.write("Ravi,85\n")
elif job == "closed":
sheet = open("fh_errors.txt")
sheet.close()
sheet.read()
except Exception as error:
print(type(error).__name__ + ":", error)
UnsupportedOperation is a kind of ValueError, so except ValueError catches those middle two.
try-except-finally
The long form, written out so you can see what with is doing for you.
The same job without with
sheet = None
try:
sheet = open("fh_not_a_real_file.txt")
print(sheet.read())
except FileNotFoundError as error:
print("Could not find", error.filename)
finally:
if sheet is not None:
sheet.close()
print("finished either way")
Note sheet = None at the top and the if inside finally. Without them, a failed open() leaves sheet unset and the finally block raises a second exception on top of the first.
Safe File Operations
with and try together: with guarantees the close, try decides what happens when the file is not there.
The shape to use in real code
def read_marks(filename):
try:
with open(filename) as sheet:
return [row.strip() for row in sheet if row.strip()]
except FileNotFoundError:
print(f" ({filename} is missing — treating it as empty)")
return []
with open("fh_marks.txt", "w") as sheet:
sheet.write("Asha,78\nRavi,85\n")
print(read_marks("fh_marks.txt"))
print(read_marks("fh_no_such_sheet.txt"))
Do not check os.path.exists() and then open the file. Between the two lines the file can be deleted or renamed, and your careful check reports a file that is gone by the time you open it. Open it inside a try and handle FileNotFoundError — that covers both cases with one piece of code.
Working with File Types
Everything so far has been plain text. Real programs meet three shapes of file, and each has its own way back into values.
Text Files (.txt)
Plain text is the simplest case, and everything above has been working with it. The only decision is how you separate the parts of a row.
A text file read back into values
with open("fh_plain.txt", "w") as sheet:
sheet.write("Asha,Maths,78\n")
sheet.write("Ravi,Science,85\n")
with open("fh_plain.txt") as sheet:
for row in sheet:
name, subject, mark = row.strip().split(",")
print(f"{name} scored {mark} in {subject}")
That works until a name contains a comma. Which is why CSV exists.
CSV Files (.csv)
A CSV file is a table: one row per line, values separated by commas. The csv module handles the awkward cases — commas inside a value, quotes, blank fields — that splitting by hand gets wrong.
Writing a CSV
import csv
with open("fh_marks.csv", "w", newline="") as sheet:
writer = csv.writer(sheet)
writer.writerow(["name", "subject", "mark"])
writer.writerows([
["Asha", "Maths", 78],
["Ravi", "Science", 85],
["Meera", "English", 60],
])
with open("fh_marks.csv") as sheet:
print(sheet.read())
newline="" is not optional. Without it, the csv module and the file object both add line endings, and the file comes out double-spaced on some systems.
Reading a CSV row by row
import csv
with open("fh_marks.csv", "w", newline="") as sheet:
writer = csv.writer(sheet)
writer.writerow(["name", "subject", "mark"])
writer.writerows([
["Asha", "Maths", 78],
["Ravi", "Science", 85],
["Meera", "English", 60],
])
with open("fh_marks.csv", newline="") as sheet:
for row in csv.reader(sheet):
print(row)
Every value comes back as text, including the marks. 78 written is "78" read.
DictReader names the columns for you
import csv
with open("fh_marks.csv", "w", newline="") as sheet:
writer = csv.writer(sheet)
writer.writerow(["name", "subject", "mark"])
writer.writerows([
["Asha", "Maths", 78],
["Ravi", "Science", 85],
["Meera", "English", 60],
])
total = 0
with open("fh_marks.csv", newline="") as sheet:
for row in csv.DictReader(sheet):
total += int(row["mark"])
print(row["name"], "->", row["mark"])
print("Total:", total)
DictReader uses the first line as the column names, so you write row["mark"] instead of row[2] — and the code survives someone adding a column.
DictWriter writes dictionaries back out
import csv
records = [
{"name": "Asha", "mark": 78},
{"name": "Ravi", "mark": 85},
]
with open("fh_dict.csv", "w", newline="") as sheet:
writer = csv.DictWriter(sheet, fieldnames=["name", "mark"])
writer.writeheader()
writer.writerows(records)
with open("fh_dict.csv") as sheet:
print(sheet.read())
JSON Files (.json)
JSON is the format for nested data — a dictionary inside a dictionary inside a list. A CSV is flat; JSON is not.
Four function names, and the rule is simple: the ones ending in s work with strings, the ones without work with files.
| Function | Direction | Works with |
|---|---|---|
json.dump() | Python → file | a file |
json.load() | file → Python | a file |
json.dumps() | Python → text | a string |
json.loads() | text → Python | a string |
Writing JSON to a file
import json
record = {
"name": "Asha",
"class": "7A",
"marks": {"Maths": 78, "Science": 85},
}
with open("fh_record.json", "w") as sheet:
json.dump(record, sheet, indent=2)
with open("fh_record.json") as sheet:
print(sheet.read())
indent=2 is what makes it readable. Leave it out and the whole thing is one long line.
Reading JSON back
import json
record = {
"name": "Asha",
"class": "7A",
"marks": {"Maths": 78, "Science": 85},
}
with open("fh_record.json", "w") as sheet:
json.dump(record, sheet, indent=2)
with open("fh_record.json") as sheet:
loaded = json.load(sheet)
print(type(loaded).__name__)
print(loaded["name"], "is in", loaded["class"])
print("Maths:", loaded["marks"]["Maths"])
It comes back as a real dictionary, with the numbers still numbers. That is the advantage over CSV, where everything arrives as text.
The string versions
import json
record = {"name": "Asha", "mark": 78}
as_text = json.dumps(record)
print(as_text)
print(type(as_text).__name__)
back = json.loads(as_text)
print(back["mark"] + 1)
One last thing worth knowing — what happens when the JSON is not valid:
When the file is not valid JSON
import json
with open("fh_broken.json", "w") as sheet:
sheet.write("{name: Asha}")
try:
with open("fh_broken.json") as sheet:
json.load(sheet)
except json.JSONDecodeError as error:
print("JSONDecodeError on line", error.lineno)
print("Problem:", error.msg)
print("It is a kind of ValueError:", issubclass(json.JSONDecodeError, ValueError))
JSON needs double quotes around every key and every piece of text. Single quotes are valid Python and invalid JSON, which is the single most common cause of that error.
Choose by shape, not by habit. A flat table of rows and columns is a CSV, and a spreadsheet will open it. Anything nested — a record containing a list, or a dictionary of dictionaries — is JSON. Plain text is for notes and logs, where nothing needs reading back as data.
Common mistakes
Before moving on, here are some of the mistakes beginners make with files.
Each row is one mistake. The middle column is what you will actually see on screen.
| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
open(name, "w") meaning to add | The old contents are gone | w empties the file on open | Use a |
| Reading a file twice from one object | The second read is '' | The marker is at the end | seek(0), or open it again |
No close() and no with | The file is short or empty | The buffer was never flushed | Use with |
close() after a line that raises | The file stays open | The close() line is skipped | Use with |
write() without \n | Everything on one line | write() adds nothing of its own | Add "\n" yourself |
writelines(rows) expecting lines | Everything on one line | It does not add newlines either | writelines(r + "\n" for r in rows) |
Forgetting .strip() on a read line | A blank line between rows, or ValueError from int() | Each line still carries its \n | row.strip() |
csv.writer without newline="" | A blank line between every row | Two layers both add line endings | open(name, "w", newline="") |
| Expecting numbers back from a CSV | TypeError when you add them | Every CSV value reads as text | int(row["mark"]) |
| Single quotes in a JSON file | JSONDecodeError | JSON only allows double quotes | Write it with json.dump() |
os.path.exists() then open() | A FileNotFoundError anyway | The file can vanish between the two lines | Open inside a try |
Reading a huge file with read() | The program slows or stops | The whole file goes into memory | Loop over the file object |
What to Remember
- A file is storage that outlives the program.
open()gives you a file object, not the contents. - A file object remembers where you have got to. A second
read()gives you nothing until youseek(0). wempties the file on open;aadds to the end. Getting this wrong destroys data with no undo.xrefuses to overwrite.bgives you bytes;tis text and is the default.read()gives one string,readline()gives the next line,readlines()gives a list — and looping over the file object is usually better than all three.- Every line you read keeps its
\n. Use.strip(). - Neither
write()norwritelines()adds a newline. You add it. - Closing flushes the buffer. Until a file is closed, what you wrote may not be in it.
- Use
with. It closes the file on every path out, including through an exception, and it is the reason you never needclose(). - Put the
tryaround theopen()and thewithinside it. CatchFileNotFoundErrorby name. - CSV is a flat table and reads back as text.
newline=""when writing,DictReaderwhen reading. - JSON is for nested data and keeps types.
dump/loadfor files,dumps/loadsfor strings.
Next: Advanced Functions