File input/output in Python is performed through file objects returned by the built-in open() function, which connects a program to a file using a specified mode and encoding; the with statement is the idiomatic way to use them because it guarantees the file is closed even if an error occurs.
Reading and writing files
What you'll learn: How to open a file, read its contents, and write to it. The three rules that will prevent 90% of file I/O bugs: always use with, always specify encoding, know your mode.
How to read this tab: After reading each section, try it with an actual file on your computer. Create a text file, open it in Python, read it, then write something new to it. File I/O is best learned by doing.
To work with a file, you open it, read or write its contents, then close it. Python's with statement handles the closing automatically — so the safe, standard pattern is with open(...) as f:.
The mode table is the most important thing on this page. Know it. The most dangerous mode is "w" — it erases the file the moment open() is called, before you write anything. If you want to add to an existing file, use "a" (append). If you want to ensure you don't overwrite, use "x" (exclusive create — raises FileExistsError if the file exists).
Opening a file with open()
The built-in open() function returns a file object. Its two most important arguments are the file path and the mode — a short string saying whether you want to read, write, or append, and whether the data is text or binary.
| Mode | Meaning |
|---|---|
'r' | Read (default). Error if the file does not exist. |
'w' | Write. Creates the file, or truncates (empties) an existing one. |
'a' | Append. Writes to the end; creates the file if needed. |
'x' | Exclusive creation. Error if the file already exists. |
'r+' | Read and write (file must exist). |
'b' | Binary mode, combined with another: 'rb', 'wb'. |
't' | Text mode (default), combined: 'rt'. |
Read line by line for large files. for line in f: reads one line at a time without loading the whole file into memory. f.read() loads everything at once — fine for small files, dangerous for large ones. For anything over a few MB, iterate line by line.
Reading a file
# The standard, safe pattern — with statement closes the file for you
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read() # entire file as one string
print(content)
# Read line by line — memory-efficient for large files
with open("notes.txt", encoding="utf-8") as f:
for line in f: # iterating a file yields lines
print(line.rstrip()) # rstrip removes the trailing newline
# Read all lines into a list
with open("notes.txt", encoding="utf-8") as f:
lines = f.readlines() # ['first line\n', 'second line\n', ...]
# Read one line at a time
with open("notes.txt", encoding="utf-8") as f:
first = f.readline() # just the first linePass encoding="utf-8" explicitly when opening text files. Without it, Python uses a platform-dependent default that can differ between machines, causing the same code to behave differently on Windows, macOS, and Linux. UTF-8 is the safe, portable choice for almost all text.
write() does not add newlines. Unlike print(), f.write("hello") writes exactly "hello" — no newline at the end. You must add "\n" yourself. This trips up almost everyone the first time. Also: writelines() does NOT add newlines between items — it's just write() called for each item in the list.
Writing a file
# Write mode 'w' — creates or OVERWRITES the file
with open("output.txt", "w", encoding="utf-8") as f:
f.write("First line\n") # write() does NOT add a newline
f.write("Second line\n") # so add \n yourself
# Write a list of lines at once
lines = ["Priya\n", "Arjun\n", "Rohit\n"]
with open("names.txt", "w", encoding="utf-8") as f:
f.writelines(lines) # writelines adds no newlines either
# Append mode 'a' — adds to the end, keeps existing content
with open("log.txt", "a", encoding="utf-8") as f:
f.write("New log entry\n")
# print() can write to a file via the file= argument
with open("report.txt", "w", encoding="utf-8") as f:
print("Total:", 42, file=f) # print adds a newline automatically✅ Beginner tab complete — check your understanding
- I use "with open(...) as f:" every time — never open() without with
- I always pass encoding="utf-8" when opening text files
- I know the difference between mode "r" (read), "w" (write — erases), and "a" (append)
- I can read a file line by line with "for line in f:" instead of loading the whole thing
Binary mode, encodings, seek/tell, and pathlib
What you'll learn: Text mode vs binary mode (str vs bytes). How encodings work and why UTF-8 is the right default. The seek() and tell() API for random access within files. pathlib's read_text()/write_text() shortcuts.
How to read this tab: The encoding section is critical if you work with non-English text or legacy files. The pathlib section will simplify most of your file path code.
When to use binary mode: images, audio, video, zip files, any file that is not human-readable text. When to use text mode: .txt, .csv, .json, .py, .html — anything you could open in a text editor and read. The golden rule: if it's not text, open it in binary mode.
Binary mode and encodings
Text mode returns str objects and handles encoding/decoding for you. Binary mode ('b') returns bytes objects with no decoding — use it for images, audio, compressed files, or any non-text data. In text mode, the file's bytes are decoded using the specified encoding; in binary mode you receive the raw bytes unchanged.
# Binary read — get raw bytes, no decoding
with open("photo.jpg", "rb") as f:
data = f.read() # data is a bytes object
print(type(data)) # <class 'bytes'>
print(len(data), "bytes")
# Binary write — must write bytes, not str
with open("copy.jpg", "wb") as f:
f.write(data)
# Copy a binary file in chunks (memory-safe for large files)
with open("big.zip", "rb") as src, open("backup.zip", "wb") as dst:
while chunk := src.read(8192): # read 8 KB at a time
dst.write(chunk)
# Explicit encoding for non-UTF-8 text
with open("legacy.txt", "r", encoding="latin-1") as f:
text = f.read()
# Handle decoding errors gracefully
with open("messy.txt", "r", encoding="utf-8", errors="replace") as f:
text = f.read() # invalid bytes become the replacement charRandom access in files. After you read from a file, the position advances to where you left off. f.seek(0) resets to the beginning. This matters when you need to read a file more than once without reopening it, or when you need to jump to a specific byte position.
Seeking and the file position
A file object maintains a current position. tell() reports it; seek() moves it. This lets you re-read or jump within a file without reopening it.
with open("data.txt", "r", encoding="utf-8") as f:
print(f.tell()) # 0 — at the start
chunk = f.read(5) # read 5 characters
print(f.tell()) # position after 5 chars
f.seek(0) # jump back to the beginning
print(f.read()) # read the whole file again
# In binary mode, seek can use a reference point:
# 0 = start (default), 1 = current, 2 = end
with open("data.bin", "rb") as f:
f.seek(-10, 2) # 10 bytes before the end
tail = f.read()For building, joining, and inspecting file paths, Python's pathlib module is the modern, object-oriented approach — and Path objects work directly with open(). See the dedicated reference: pathlib in the Standard Library.
from pathlib import Path
# Path objects have convenient read/write shortcuts
p = Path("notes.txt")
text = p.read_text(encoding="utf-8") # read whole file
p.write_text("new content", encoding="utf-8") # write whole file
data = Path("photo.jpg").read_bytes() # read binary
# Path objects also work directly with open()
with open(Path("logs") / "today.txt", "w", encoding="utf-8") as f:
f.write("entry\n")'w' erases the file immediately. Opening an existing file in 'w' mode truncates it to zero length the moment open() is called — before you write anything. If you want to add to a file, use 'a' (append). To avoid accidental overwrites, use 'x', which raises FileExistsError if the file already exists.write() does not add newlines. Unlike print(), the file write() and writelines() methods write exactly what you give them. You must include \n yourself where you want line breaks.str, binary gives bytes. Mixing them raises TypeError — you cannot f.write("text") on a file opened with 'wb', nor write bytes to a text-mode file.✅ Intermediate tab complete — check your understanding
- I know that text mode returns str, binary mode returns bytes, and mixing them raises TypeError
- I can copy a binary file in chunks without loading it all into memory
- I know why CSV files need newline="" in the open() call
- I can use Path("file.txt").read_text(encoding="utf-8") as a one-liner
The io module's three-layer architecture
What you'll learn: How Python's io module is structured (FileIO → BufferedReader → TextIOWrapper). What universal newline translation does and when to disable it. In-memory file objects with io.StringIO and io.BytesIO.
How to read this tab: Read when you need to understand why file objects behave the way they do, or when using io.StringIO for testing.
What this section is: The internal architecture of Python file objects — three stacked layers each adding functionality. You don't need to interact with these layers directly in most code, but knowing they exist explains why type(open("f.txt")) returns TextIOWrapper, not a plain "file" object.
The io module and the three layers of file objects
The objects returned by open() are defined in the io module. There is a layered hierarchy: RawIOBase for raw unbuffered binary I/O, BufferedIOBase for buffered binary I/O, and TextIOBase for text I/O. When you open a file in text mode, open() returns a TextIOWrapper that wraps a BufferedReader/BufferedWriter, which in turn wraps a FileIO raw stream. This layering is why text mode can transparently decode bytes and translate newlines.
import io
# What open() actually returns
f = open("notes.txt", "r", encoding="utf-8")
print(type(f)) # <class '_io.TextIOWrapper'>
f.close()
f = open("notes.txt", "rb")
print(type(f)) # <class '_io.BufferedReader'>
f.close()
# In-memory file objects — same interface, no disk
text_stream = io.StringIO()
text_stream.write("in-memory text\n")
print(text_stream.getvalue()) # 'in-memory text\n'
bytes_stream = io.BytesIO()
bytes_stream.write(b"\x00\x01\x02")
print(bytes_stream.getvalue()) # b'\x00\x01\x02'
# Control buffering with the buffering argument
# 0 = unbuffered (binary only), 1 = line-buffered (text),
# >1 = buffer size in bytes
with open("log.txt", "w", encoding="utf-8", buffering=1) as f:
f.write("flushed each line\n")The CSV newline gotcha. In text mode, Python translates \r\n (Windows) to \n on reading. This is usually helpful — but the csv module handles line endings itself and gets confused by Python's translation. Always open CSV files with newline="" to disable the translation: open("data.csv", newline="").
Newline translation and universal newlines
In text mode, Python performs universal newline translation by default: on reading, any of \n, \r\n, or \r is translated to \n; on writing, \n is translated to the platform default line separator (os.linesep). The newline argument to open() controls this. Setting newline="" disables translation — which is exactly what the csv module requires to handle quoted fields containing embedded newlines correctly.
# Default: universal newlines translate \r\n -> \n on read
with open("windows_file.txt", "r", encoding="utf-8") as f:
text = f.read() # \r\n becomes \n automatically
# csv module requires newline="" to avoid double-translation
import csv
with open("data.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "city"])
writer.writerow(["Priya", "Mumbai"])
# Preserve original newlines exactly (no translation on read)
with open("exact.txt", "r", encoding="utf-8", newline="") as f:
raw = f.read() # \r\n stays \r\nWithout with (or an explicit try/finally), a file may stay open if an exception occurs before close(). Open file handles are a limited OS resource, and buffered writes are not guaranteed to reach disk until the file is flushed or closed. The with statement calls the file's __exit__, which closes and flushes it — even on error. This is the single most important habit in Python file handling.
✅ Expert tab complete
- I know the three io layers: raw binary → buffered binary → text
- I can use io.StringIO to create an in-memory file object for testing
- I understand what universal newline translation does and why csv needs newline=""