File I/O, Pathlib, and Large Files
1 · The lesson
readYou already know with open(...) as f, the read/write modes, and that for line in f is better than f.read() for anything but small files. This lesson shows how file code looks in professional Python: pathlib instead of paths built from strings, streaming for huge files, atomic writes that survive crashes, and the encoding problem that sooner or later affects every program that runs on more than one operating system.
If you still use os.path.join, this lesson shows you the modern replacement.
1. Pathlib: Stop Treating Paths as Strings
pathlib.Path is the object-oriented path API. A Path knows its parent, its suffix and its parts. It can also read and write the file, search with glob patterns, and get file information (stat). You will rarely need raw strings or os.path again.
from pathlib import Path p = Path("data") / "logs" / "app.log" # the / operator builds paths print(p) # data/logs/app.log (or data\logs\app.log on Windows) print(p.name) # app.log print(p.stem) # app print(p.suffix) # .log print(p.parent) # data/logs print(p.parts) # ('data', 'logs', 'app.log')
Path defines its own meaning for the / operator. It uses the correct separator on every platform, so you never need os.sep.
2. The Five Pathlib Methods You'll Use Daily
from pathlib import Path root = Path("project") root.mkdir(parents=True, exist_ok=True) # creates parent folders too, no error if it exists (root / "config.yaml").exists() # True/False (root / "config.yaml").is_file() # True only if it exists AND is a file (root / "src").is_dir() # the same check, for a directory # Read or write in one call. Short files need no `with` block. (root / "README.md").write_text("# Hi\n", encoding="utf-8") content = (root / "README.md").read_text(encoding="utf-8") # Binary equivalents (root / "thumb.png").write_bytes(b"\x89PNG...") blob = (root / "thumb.png").read_bytes()
A few more that are very useful:
# Globbing: find every file that matches a pattern for csv in Path("data").glob("*.csv"): print(csv) # Recursive glob: ** also looks inside subfolders for py in Path("src").rglob("*.py"): print(py) # Change the file extension, common when converting files src = Path("notes.md") out = src.with_suffix(".html") # notes.html # Loop over the items directly inside a folder for child in Path(".").iterdir(): print(child, "(dir)" if child.is_dir() else "(file)")
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Path(*_a, **_kw): print('-> Path() called') return _AutoMock('Path()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from pathlib import Path except Exception: pass _pm_earlier = [ 'from pathlib import Path\n\np = Path("data") / "logs" / "app.log" # the / operator builds paths\nprint(p) # data/logs/app.log (or data\\logs\\app.log on Windows)\n\nprint(p.name) # app.log\nprint(p.stem) # app\nprint(p.suffix) # .log\nprint(p.parent) # data/logs\nprint(p.parts) # (\'data\', \'logs\', \'app.log\')', 'from pathlib import Path\n\nroot = Path("project")\n\nroot.mkdir(parents=True, exist_ok=True) # creates parent folders too, no error if it exists\n(root / "config.yaml").exists() # True/False\n(root / "config.yaml").is_file() # True only if it exists AND is a file\n(root / "src").is_dir() # the same check, for a directory\n\n# Read or write in one call. Short files need no `with` block.\n(root / "README.md").write_text("# Hi\\n", encoding="utf-8")\ncontent = (root / "README.md").read_text(encoding="utf-8")\n\n# Binary equivalents\n(root / "thumb.png").write_bytes(b"\\x89PNG...")\nblob = (root / "thumb.png").read_bytes()', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
write_text and read_text are fine for small files, when you really want the whole file in memory. For anything that could grow, keep using with open(...) and stream it (see Section 5).
3. Text vs Binary Mode: When Bytes Matter
Text mode ("r", "w", "a") decodes bytes to str on read and encodes on write. Binary mode ("rb", "wb", "ab") gives you raw bytes, with no decoding and no newline translation.
Use binary mode for anything that isn't human-readable text:
# Hashing a file: read bytes only, never decode them import hashlib def file_sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): h.update(chunk) return h.hexdigest()
Common binary cases: images, audio, video, pickle files, executables, PDFs, and anything that gave you a UnicodeDecodeError when you read it as text. If you read a JPEG in text mode, Python tries to decode the bytes with a text encoding, usually fails on some byte, and raises an error.
On Windows, text mode also translates \n to \r\n on write. You don't notice this most of the time, but it gives wrong results when you compute a hash or compare file sizes. When in doubt, use binary mode.
4. Encoding: The "Works on My Machine" Problem
Text files are bytes plus an encoding. The world has standardised on UTF-8. Most operating systems default to UTF-8. On Windows, Python's open() has long used the system's locale encoding by default, which is often cp1252: a Western European code page that can't represent most non-ASCII text.
# Danger: the encoding defaults to the operating system's choice with open("notes.txt", "w") as f: f.write("café — 北京 — 🐍") # crashes on Windows cp1252 # Correct: always give the encoding, every time with open("notes.txt", "w", encoding="utf-8") as f: f.write("café — 北京 — 🐍")
This bug is hard to catch. The code works on your Mac and in CI on Linux. Then a Windows user runs it and gets UnicodeEncodeError: 'charmap' codec can't encode character. Or, worse, the program writes garbled text (called mojibake) without any error.
Rules:
- Reading text? Pass
encoding="utf-8". - Writing text? Pass
encoding="utf-8". - Reading text from a source that really uses a different encoding (legacy logs, a CSV from a Windows export)? Pass that encoding explicitly.
- Never rely on the default. Writing the encoding costs you a few extra characters.
When the source might contain a few invalid bytes, you can replace them instead of failing:
with open("legacy.txt", "r", encoding="utf-8", errors="replace") as f: text = f.read() # bad bytes become � instead of crashing
5. Reading Huge Files: Iterate, Don't Load Everything
A very common bug with large files is loading a 4 GB log into memory all at once. The fix is the iteration pattern you already know, but it helps to see both versions side by side.
# Bad: loads the entire file into a list of strings with open("server.log", encoding="utf-8") as f: lines = f.readlines() # a 4 GB file needs more than 4 GB of RAM for line in lines: process(line) # Good: reads one line at a time, so memory use stays small with open("server.log", encoding="utf-8") as f: for line in f: # file objects are iterators process(line)
setup added so this can run · defines process
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def process(*_a, **_kw): print('-> process() called') return _AutoMock('process()')
The file object is its own iterator. Each next() call reads up to the next \n and returns the line (including the trailing \n). Memory usage stays at one buffer's worth, no matter how big the file is.
Need only the first line? f.readline(). Need only the last ten? Use collections.deque with a max length. It keeps only the most recent items as you read:
from collections import deque with open("server.log", encoding="utf-8") as f: last_ten = deque(f, maxlen=10) # reads every line but keeps only the last 10 for line in last_ten: print(line, end="")
6. Streaming Binary Chunks
For binary files, you read in fixed-size chunks instead of lines. The standard loop uses the walrus operator (:=), Python's assignment expression:
with open("video.mp4", "rb") as f: while chunk := f.read(8192): # read 8 KB at a time, stop when read returns b"" process(chunk)
setup added so this can run · defines process, chunk
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def process(*_a, **_kw): print('-> process() called') return _AutoMock('process()') chunk = _AutoMock('chunk')
chunk := f.read(8192) assigns the value and returns it. The loop ends when f.read() returns b"" (empty bytes), which is the EOF signal in binary mode. 8 KB is a fine default, but 64 KB to 1 MB is better for high-throughput work on SSDs.
The version without the walrus operator is longer, but it also works on Python 3.7 and earlier:
with open("video.mp4", "rb") as f: for chunk in iter(lambda: f.read(8192), b""): process(chunk)
setup added so this can run · defines process
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def process(*_a, **_kw): print('-> process() called') return _AutoMock('process()')
iter(callable, sentinel) keeps calling callable() until it returns the sentinel. Both forms are idiomatic.
7. Atomic Writes: Surviving Crashes
With the simple pattern, there is a moment when the file on disk is only half-written:
# Bad: if Python crashes while writing, config.json is left broken with open("config.json", "w", encoding="utf-8") as f: f.write(serialize(big_object)) # a crash here leaves an empty or half-written file
setup added so this can run · defines serialize, big_object
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def serialize(*_a, **_kw): print('-> serialize() called') return _AutoMock('serialize()') big_object = _AutoMock('big_object')
The professional pattern is to write to a temp file, then rename it:
import os from pathlib import Path def atomic_write_text(path, text, encoding="utf-8"): """Write text to `path` atomically. Readers see either the old file or the full new one.""" path = Path(path) tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(text, encoding=encoding) os.replace(tmp, path) # atomic on POSIX (see below)
On POSIX systems (Linux, macOS), the Python docs guarantee that os.replace() is atomic: the rename either fully happens or doesn't happen at all. Readers of the target file never see a half-written file. On Windows the docs don't promise this, but the pattern is still much safer than writing the file in place. Configuration files, sqlite-style databases, and anything that other processes might read while you're writing should use this pattern.
For extra safety against a power failure or an OS crash, call tmp_fh.flush() and os.fsync(tmp_fh.fileno()) before closing the temp file. That makes the OS write the bytes to disk, not just keep them in its memory cache. Most apps don't need this, but databases do.
8. Temporary Files and Directories
The tempfile module gives you safe, OS-chosen temp paths that get cleaned up automatically.
import tempfile from pathlib import Path # A one-off temp FILE, deleted when the block exits (delete_on_close needs Python 3.12+) with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".json", delete_on_close=False) as tmp: tmp.write('{"hello": "world"}') tmp.flush() print("Temp path:", tmp.name) # use tmp.name as a real path here, for example give it to another tool # the file is deleted here # A whole temp DIRECTORY, removed with all its contents when the block exits with tempfile.TemporaryDirectory() as tmpdir: workdir = Path(tmpdir) (workdir / "input.txt").write_text("hi", encoding="utf-8") # ... do work in workdir ... # the folder and everything in it are gone
Use NamedTemporaryFile when you need a real file path to give to a subprocess or another library. Use TemporaryDirectory for test fixtures, build sandboxes, and any case where you need to write many files and then delete them all.
Both clean up when the with block ends. The Advanced path shows how to build your own context managers.
9. shutil Highlights
shutil is the standard library's module for high-level file operations. These are the ones you'll use most:
import shutil from pathlib import Path shutil.copy("src.txt", "dst.txt") # copy one file (shutil.copy2 also copies metadata) shutil.copytree("src_dir", "dst_dir") # copy a directory tree shutil.move("from.txt", "to.txt") # rename or move, even to another drive shutil.rmtree("scratch_dir") # delete a folder and all its contents. No undo, no Trash. usage = shutil.disk_usage(Path.home()) print(f"{usage.free / 1e9:.1f} GB free")
shutil.rmtree deletes everything under the path, and there's no undo. Check the path twice, and keep the default ignore_errors=False so you see an error when something fails.
10. CSV and JSON: A Short Introduction
You'll often want structured text rather than raw lines. The standard library has both:
import csv, json # CSV: read the rows as dicts with open("people.csv", encoding="utf-8", newline="") as f: for row in csv.DictReader(f): print(row["name"], row["email"]) # JSON: read a whole file into a Python object with open("config.json", encoding="utf-8") as f: config = json.load(f) # JSON: write a Python object to a file with open("config.json", "w", encoding="utf-8") as f: json.dump(config, f, indent=2)
Always pass the newline="" argument for CSV files. The csv module handles newlines itself, and the default text-mode translation can break quoted fields that contain newlines. Both modules have a full lesson's worth of options (dialects, custom encoders, streaming). The CSV & JSON How-To covers them.
11. Common Mistakes
1. Forgetting encoding=
The code works on your machine and breaks on other machines. Adding encoding="utf-8" is the cheapest way to make your code more reliable. Every text open() should have it.
2. Holding file handles open longer than needed
Use with for every file you open. A file that stays open longer than needed uses up OS file descriptors, locks the file on Windows, and risks data loss if your process is killed before the buffer flushes.
3. Reading the whole file when you only need part
Calling f.read() on a 4 GB file when you only wanted the first line is a beginner's mistake. Use f.readline() for the first line, collections.deque(f, maxlen=N) for the last N, and for line in f for everything in between.
4. Comparing paths as strings"data/log.txt" == "data\\log.txt" is False. Path("data/log.txt") == Path("data\\log.txt") is True on Windows. Once you have Path objects, compare and hash them as paths. Don't convert them with str() just to use ==.
5. The check-then-act race
# Race condition: another process could create the file between these two lines if not path.exists(): path.write_text("default")
setup added so this can run · defines path
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) path = _AutoMock('path')
Between the
exists() and the write_text(), another process or thread could create the file. Either use exclusive-create mode (open(path, "x") raises FileExistsError if it exists), use Path.write_text(...) unconditionally if you're happy to overwrite, or catch the exception:
try: with open(path, "x", encoding="utf-8") as f: f.write("default") except FileExistsError: pass # another process created it first, which is fine
setup added so this can run · defines path
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) path = _AutoMock('path')
This is the EAFP principle: "easier to ask forgiveness than permission". We'll cover it more in Exceptions.
🎯 Your Turn: Remove Duplicate Lines From a Huge File
Write dedupe_lines(input_path, output_path) that reads a (potentially huge) text file and writes the unique lines, in first-seen order, to the output file. The constraint: memory must be O(unique line count), not O(file size). You can't load the whole file with readlines().
# Input file (one line per row): # alpha # beta # alpha # gamma # beta # delta # Output file: # alpha # beta # gamma # delta
Skeleton:
from pathlib import Path def dedupe_lines(input_path, output_path): # TODO 1: open input for reading and output for writing, both as UTF-8 # TODO 2: keep a set of lines you've already seen # TODO 3: stream input line-by-line; if a line is new, write it and add to the set ...
Hint 1: Read line by line
Iterate the file object directly:for line in f:. Each iteration reads one line. Don't call readlines(), because that loads everything.
Hint 2: Memory-efficient deduplication
Aset gives you O(1) membership checks. You store each unique line in it the first time you see it. The set grows with the number of distinct lines, not the total number of lines. That is what the task asks for.
Show full solution
from pathlib import Path def dedupe_lines(input_path, output_path): """Copy each line only the first time it appears. Memory grows with unique lines.""" seen = set() in_path = Path(input_path) out_path = Path(output_path) with in_path.open("r", encoding="utf-8") as src, \ out_path.open("w", encoding="utf-8") as dst: for line in src: if line not in seen: seen.add(line) dst.write(line) # Demo import tempfile from pathlib import Path with tempfile.TemporaryDirectory() as tmpdir: tmp = Path(tmpdir) src = tmp / "in.txt" dst = tmp / "out.txt" src.write_text("alpha\nbeta\nalpha\ngamma\nbeta\ndelta\n", encoding="utf-8") dedupe_lines(src, dst) print(dst.read_text(encoding="utf-8")) # alpha # beta # gamma # delta
What you did:
- Streamed the input:
for line in srcreads one line at a time and never loads the whole file. - Used a
setfor membership: O(1) check, O(unique) storage. - Wrote each new line straight to the output, with no intermediate list.
- Wrapped both files in one
withso they close even if writing fails halfway.
If the input had 10 million lines but only 100 unique, your memory usage is 100 entries, not 10 million. That's the difference between a script that finishes and a script that the system stops because it ran out of memory.
For huge inputs where even the set of unique lines won't fit in memory, you need a method that uses the disk (sort + uniq), or a Bloom filter, which removes duplicates with a small chance of error. That's outside this lesson, but those methods also build on the streaming pattern.
What You Learned
- Pathlib replaces
os.path. Build paths with/, check them with.exists()/.is_file()/.glob(), change the extension with.with_suffix(), create with.mkdir(parents=True, exist_ok=True). - Binary mode (
"rb","wb") for anything that isn't human-readable text: hashes, images, pickles. Text mode can fail to decode such data, and it changes newlines. - Always pass
encoding="utf-8". The default encoding causes bugs on Windows. - Stream, don't load the whole file.
for line in ffor text,while chunk := f.read(8192)for binary. O(1) memory. - Atomic writes: write to
*.tmp, thenos.replace(tmp, final). This survives crashes. tempfile.NamedTemporaryFileandTemporaryDirectoryclean up after themselves.shutilfor copy/move/rmtree/disk_usage.rmtreeis permanent, so check the path twice.csvandjsonfor structured text, covered in full in the CSV & JSON How-To.- Avoid check-then-act races: use
open(path, "x")or catch the exception (EAFP).
Next: Exceptions: Patterns Beyond the Basics: custom exception hierarchies, chaining, ExceptionGroup, and the patterns used in production code.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.