Working with files is how a program stops being a toy. The moment you read a configuration from disk, write a log line, or save a result for later, your code is interacting with the outside world and has to deal with encoding, open/close semantics, and error cases. Python's file handling APIs are deliberately small: one open() built-in, a handful of methods on the returned object, and a with statement that makes cleanup automatic.
A file in Python is an object returned by open(path, mode, encoding=...). The mode string chooses read vs write and text vs binary: "r" reads text, "w" writes text (truncating first), "a" appends text, and each of those has a "b" variant for binary. The encoding argument controls how bytes become str; always pass encoding="utf-8" explicitly for text files to avoid cross-platform surprises.
The golden rule is: always use with open(...) as f:. The with block guarantees that the file is closed, even if an error interrupts your code. Without with, a missing f.close() on an error path leaks the underlying OS handle and, on Windows, can prevent a later attempt to delete or overwrite the file.
File paths themselves are better managed with pathlib.Path than with raw strings. Path lets you join with the / operator (root / "data" / "in.txt"), check for existence (p.exists()), read a whole file in one line (p.read_text(encoding="utf-8")), and enumerate directories. Mixing pathlib for paths with open() for large/streaming reads is the modern, pythonic combination.
Modes, encodings and the with statement
The most common mistake when reading text is to forget encoding="utf-8". On Windows the platform default is often cp1252, so a file with non-ASCII characters will decode incorrectly. For binary data (images, pickles, parquet), use "rb"/"wb" and skip the encoding argument entirely.
The with statement wraps the open/close lifecycle. You can open several files at once with a single with: with open(a) as src, open(b, "w") as dst:. On exit — normal or exceptional — both files are flushed and closed in reverse order.
Paths the modern way
pathlib.Path abstracts away the differences between Windows backslashes and POSIX slashes. p.parent, p.name, p.suffix and p.stem give you the directory, filename, extension and base name in readable form. p.with_suffix(".bak") returns a new path with the extension replaced — perfect for building backup filenames.
For small files, p.read_text()/p.write_text() read or write the entire contents in one call. For anything bigger than a few megabytes, prefer with open(p, ...) as f and stream line by line.
The minimal set of tools for working with files.
| Tool | Purpose |
|---|---|
open(path, mode, encoding=...)built-in | Opens a file and returns a file object. |
pathlib.Pathclass | Filesystem paths with methods and operators. |
Path.read_text(encoding=...)method | Reads an entire text file in one call. |
Path.write_text(data)method | Writes a whole text file. |
with ...statement | Ensures the file is closed even on error. |
iomodule | Buffered and text wrappers underlying open(). |
os.fspath(p)function | Returns a string path from any path-like object. |
os.pathmodule | Legacy path utilities; pathlib preferred for new code. |
Introduction to File Handling code example
The script below creates a small temp file, writes to it, reads it back and cleans up — the full life cycle in one go.
# Lesson: Introduction to File Handling
from pathlib import Path
from tempfile import gettempdir
# Pick a platform-friendly temp path
path = Path(gettempdir()) / "pythondeck_demo.txt"
print("path:", path)
# One-shot write (small file) with explicit utf-8
path.write_text("line 1\nline 2\né accent\n", encoding="utf-8")
# One-shot read
text = path.read_text(encoding="utf-8")
print("length:", len(text), "chars")
print("first 20:", repr(text[:20]))
# Streaming read, line by line, with automatic close
with open(path, "r", encoding="utf-8") as f:
for i, line in enumerate(f, start=1):
print(f" L{i}: {line.rstrip()}")
# Path parts
print("parent:", path.parent)
print("name: ", path.name)
print("suffix:", path.suffix)
# Clean up
path.unlink()
print("exists after unlink:", path.exists())
Notice these four anchors:
1) Always pass `encoding='utf-8'` for text files.
2) `write_text`/`read_text` handle open/close automatically for small files.
3) `with open(...) as f` streams large files safely.
4) `pathlib` gives you parent/name/suffix and /-based joining for free.
Try both snippets to feel the two styles of reading.
from pathlib import Path
from tempfile import gettempdir
p = Path(gettempdir()) / "notes.txt"
p.write_text("a\nb\nc\n", encoding="utf-8")
# Example A: read all, then split
lines = p.read_text(encoding="utf-8").splitlines()
print(lines)
# Example B: stream for large files
with open(p, "r", encoding="utf-8") as f:
for line in f:
print(line.rstrip())
p.unlink()
Sanity checks that do not touch disk.
from pathlib import Path
p = Path("data") / "a.txt"
assert p.parent.name == "data"
assert p.suffix == ".txt"
assert p.with_suffix(".bak").suffix == ".bak"
assert str(Path("/")) in ("/", "\\")
Running the script prints something like:
path: /tmp/pythondeck_demo.txt
length: 18 chars
first 20: 'line 1\nline 2\né acce'
L1: line 1
L2: line 2
L3: é accent
parent: /tmp
name: pythondeck_demo.txt
suffix: .txt
exists after unlink: False