Python File Handling
Understand how Python reads, writes, and manages files so you can persist data safely and efficiently.
Why File Handling Matters
Python applications often need to read configuration files, write logs, persist user input, or exchange data across processes. The built-in open() function and file object methods unlock these capabilities with a consistent API across operating systems.
- Interact with text and binary files on disk.
- Stream large datasets without loading everything into memory.
- Integrate with serialization formats like JSON, CSV, and custom protocols.
File Paths and Working Directories
File paths can be absolute or relative to the current working directory. Use the pathlib module for cross-platform path manipulation.
from pathlib import Path
cwd = Path.cwd()
config_path = cwd / "config" / "settings.ini"
print(config_path)
print(config_path.exists())Normalize paths with Path.resolve() to remove . and .. segments, and prefer forward slashes when displaying paths to users.
Opening Files
Use open(file, mode, encoding) to access files. The most common modes are:
'r': Read (default); fails if the file does not exist.'w': Write; creates the file or truncates existing content.'a': Append; creates the file if needed.'b': Binary mode modifier (for example'rb').'x': Exclusive creation; fails if the file exists.'+': Update (read and write) modifier.
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)The with statement (context manager) ensures that the file closes automatically, even when exceptions occur.
Reading Strategies
Choose the reading method that matches your needs and file size.
file.read(): Returns the entire contents as a string (or bytes in binary mode).file.readline(): Returns one line at a time, including the newline character.file.readlines(): Returns a list of lines; avoid on very large files.- Iterate directly over the file object to stream line by line.
with open("data.csv", "r", encoding="utf-8") as file:
for line in file:
process(line.strip())Writing Strategies
Writing replaces or appends content depending on the mode. Remember to include newline characters when writing multi-line text.
numbers = [1, 2, 3, 4]
with open("numbers.txt", "w", encoding="utf-8") as file:
for number in numbers:
file.write(f"{number}\n")For complex data, serialize with json.dump(), csv.writer, or third-party libraries.
File Object Attributes
File objects expose helpful properties:
file.name: Original file path.file.mode: Mode string used to open the file.file.closed: Boolean indicating whether the file is closed.file.encoding: Encoding for text mode files.
with open("report.log", "a", encoding="utf-8") as file:
file.write("INFO: Task completed\n")
print(file.name, file.mode, file.closed)
print(file.closed) # TrueError Handling
Wrap file operations in try/except blocks to catch FileNotFoundError, PermissionError, or IsADirectoryError. Always log meaningful messages to diagnose issues.
from pathlib import Path
path = Path("/secure/data.txt")
try:
with path.open("r", encoding="utf-8") as file:
print(file.read())
except FileNotFoundError:
print("File does not exist.")
except PermissionError:
print("Insufficient permissions to read the file.")Best Practices
- Always specify an encoding (UTF-8) for text files.
- Use
pathlib.Pathfor portable path manipulation. - Close files promptly with context managers or
file.close(). - Validate user-provided paths to prevent directory traversal vulnerabilities.
- Avoid loading huge files entirely into memory; stream instead.
Next Steps
Continue to the reading and writing chapters to explore specific techniques, then learn how to safely delete files.
Always Use with (Context Manager)
Opening a file with with guarantees it is closed automatically, even if an error occurs — the safe, standard pattern.
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
# file is closed here, automatically
print(content)Always pass encoding="utf-8". Relying on the platform default causes garbled text and cross-machine bugs.
File Modes
| Mode | Meaning | If file missing |
|---|---|---|
"r" | Read (default) | Error |
"w" | Write (truncates!) | Creates it |
"a" | Append | Creates it |
"x" | Create new only | Creates; errors if exists |
"rb"/"wb" | Binary read/write | — |
"w" erases existing contents immediately. Use "a" to add without losing data.
Try It Yourself
Exercise 1: Write "line 1" and "line 2" to a file, then read it back.
Show solution
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("line 1\n")
f.write("line 2\n")
with open("demo.txt", encoding="utf-8") as f:
print(f.read())Exercise 2: Append a new line to demo.txt without erasing it.
Show solution
with open("demo.txt", "a", encoding="utf-8") as f:
f.write("line 3\n")Key Takeaways
- Open files with
with open(...)so they always close. - Choose the right mode:
r,w(truncates),a,x. - Always set
encoding="utf-8".
📘 Real-World Deep Dive
File I/O is the input layer of nearly every useful Python program. Knowing the open modes, the <code>with</code> statement, and the <code>pathlib</code> API makes file-handling code shorter, faster, and safer.
Real-Life Scenario
A multi-file log scrubber that reads compressed or uncompressed logs based on the file suffix, drops empty lines, and writes a clean, timestamped version of each to an output folder.
Real-Life Example
import gzip, json, time
from pathlib import Path
def open_maybe_gz(path: Path):
if path.suffix == ".gz":
return gzip.open(path, "rt", encoding="utf-8")
return path.open("r", encoding="utf-8")
def scrub(src_dir: Path, out_dir: Path) -> None:
out_dir.mkdir(parents=True, exist_ok=True)
seen = 0
for src in sorted(src_dir.glob("*.log*")):
cleaned = []
with open_maybe_gz(src) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
cleaned.append(line)
out = out_dir / (src.stem.replace(".log", "") + ".cleaned.log")
out.write_text("\n".join(cleaned))
seen += len(cleaned)
print(f"{src.name}: {len(cleaned):>5} lines -> {out.name}")
print("total scrubbed lines:", seen)
scrub(Path("logs-in"), Path("logs-out"))Expected Output
app.log: 187 lines -> app.cleaned.log
audit.log.gz: 1420 lines -> audit.cleaned.log
system.log: 41 lines -> system.cleaned.log
total scrubbed lines: 1648Common mistakes
- Forgetting
encoding="utf-8"makes your script platform-dependent (Windows defaults to cp1252). - Reading a file without
newline=""mixes newline conventions when usingcsv.reader. - Forgetting to close on exception paths —
with open(...)never leaks file handles.
🚀 Performance & Best Practices
- Read in chunks:
for chunk in iter(lambda: f.read(64*1024), ""):— uniform across huge files. - For append-heavy workloads, prefer opening with
"a"and writing newline-terminated strings. io.BufferedReader+io.TextIOWrappergives you explicit control over buffering on slow disks.
🧪 Try It Yourself
- Add a tail-follow mode that streams new lines as they arrive (
f.seek(0, 2); while True: time.sleep(1); ...). - Compute a checksum (
hashlib.sha256) while scrubbing so you can prove the same output twice. - Refactor scrub to be async (
aiofiles) for huge directories.