Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Files, Paths & Encodings  ›  Lesson

Reading & Writing, Text and Binary

Reading and Writing Files 22 minOpening a file in the right mode, and knowing what the mode changes
You're building a piece ofQuarry — the yard
This piece — read_source(): Opens one file and reports what is really on disk: bytes, lines and line endings.
Scenario A log from a Windows server and a CSV from a Mac land in the same yard. Quarry needs to know what is actually on disk before it trusts either one.
Your task
Build read_source(files, name). The plant() helper writes the files byte for byte. Open the named file in binary mode to get its size and whether it contains Windows line endings (b"\r\n"), then open it in text mode with encoding="utf-8" to count its lines and keep the first one without its line ending. Return {"bytes", "lines", "first", "crlf"}. Example: read_source({"a.log": "one\r\ntwo\r\n"}, "a.log") → {"bytes": 10, "lines": 2, "first": "one", "crlf": True}.

Reading & Writing, Text and Binary

In short: open() returns a file object, and the mode you pass decides everything: "r" reads, "w" empties the file first, "a" appends, "x" refuses to clobber an existing file. The bigger choice is text versus binary. Text mode does two jobs on every read — decoding bytes to characters, and newline translation, collapsing \r\n and \r into \n — so \r\n on disk becomes one \n character, not two. Binary mode ("rb"/"wb") does neither: you get the raw bytes, exactly as they sit on disk. Always open inside a with block, so the file closes whether or not the body raises, and always name encoding="utf-8" in text mode.

A 4-byte file a, CR, LF, b on disk; the top row shows it read in binary mode (all 4 bytes untouched), the bottom row shows it read in text mode (3 characters, with CR LF collapsed into one \n)

Read a Windows log without guessing

A log shipped from a Windows server uses \r\n line endings. Put exactly four bytes on disk — a, carriage return, line feed, b — and read them both ways:

What does this print?
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "w.txt"
    path.write_bytes(b"a\r\nb")
    with open(path, encoding="utf-8") as f:
        print(len(f.read()))

"How big is this file, and does it use Windows endings?" is a question about bytes — ask it in "rb" mode, where b"\r\n" in raw is still visible. "What does line 3 say?" is a question about text — ask it with encoding="utf-8", where the loop below never sees a stray \r:

python

Looping the file object reads one line at a time and holds only that line in memory — read() and readlines() pull the whole file into memory, which is fine for a config file and fatal for a 20 GB log. Strip the ending with rstrip("\n"), never strip(), which would also eat meaningful leading spaces.

"w" is the dangerous mode: opening for writing empties the file immediately, before you've written a single byte. If the program crashes between open and write, the old contents are already gone — lesson 03 fixes that properly. "x" is the polite alternative, for an output that must never overwrite a previous run's:

What happens when this runs?
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "report.txt"
    path.write_text("first run\n", encoding="utf-8")
    with open(path, "x", encoding="utf-8") as f:
        f.write("second run\n")
    print("written")

Stream a 20 GB telemetry dump in flat memory

Quarry reads files far too big to hold in memory at once, in fixed-size chunks, using read(size):

python

read(n) returns at most n bytes, and the last chunk is often shorter. The only reliable stop signal is an empty result — a short chunk doesn't mean the end in general, only here, at the real end of the file. Writing follows the same shape in reverse: write() returns how much it accepted, which in text mode is a count of characters, not bytes — only after encoding do they become bytes on disk.

Replay a buffer with StringIO

io.StringIO and io.BytesIO behave like files but live entirely in memory — useful for testing file-handling code without touching a disk, and they make a file object's one hidden rule visible: there is a single cursor, shared by reading and writing. This prints an empty line; fix it with one call:

broken — fix it

Run it — it prints nothing. Explain to yourself where the cursor is, then fix it.

That same shared cursor exists on a real file opened "r+" or "w+". tell() reports its position, seek(n) moves it. Step through a chunked read and watch the position march forward four bytes at a time:

step through it
1import io
2 
3f = io.BytesIO(b"QUARRY-YARD")
4chunks = []
5while True:
6 pos = f.tell()
7 chunk = f.read(4)
8 if not chunk:
9 break
10 chunks.append((pos, chunk))
11print(chunks)

The trace shows the same rule the telemetry dump relied on: read(4) returns at most four bytes, the last chunk is shorter, and the loop stops on an empty result, never on "the chunk was short".

One warning about tell(): only on a binary file is its position a byte offset you can do arithmetic on. On a text file it's an opaque cookie you may only hand back to seek(), because decoding and newline translation mean "character 7" and "byte 7" aren't the same place. When Quarry needs real offsets, it opens in "rb".

Quick practice

What does this print?
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "a.log"
    with open(path, "w", encoding="utf-8") as f:
        f.write("first\n")
    with open(path, "a", encoding="utf-8") as f:
        f.write("second\n")
    print(repr(path.read_text(encoding="utf-8")))
What does this print?
line = "   indented value\n"
print(repr(line.strip()), repr(line.rstrip("\n")))
What does this print?
import io

buf = io.StringIO("abcdef")
buf.seek(2)
first = buf.read(2)
buf.write("XY")
buf.seek(0)
print(first, buf.read())
broken — fix it

Run it. The size may read 0. Close the file (or use `with`) before measuring it.


🎯 Your turn

Build read_source(files, name):

  • read_source({"a.log": "one\r\ntwo\r\n"}, "a.log") → {"bytes": 10, "lines": 2, "first": "one", "crlf": True}
  • read_source({"c.txt": "café\nnaïve\n"}, "c.txt") → {"bytes": 13, "lines": 2, "first": "café", "crlf": False}

Hint — read once in "rb" for size and endings, once in text mode with encoding="utf-8" for lines.

Then press ▶ Run and hit ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow big is it, really?

Write bytes_on_disk(text): write text to a file in a temporary folder with encoding="utf-8", then return the file's size in bytes (path.stat().st_size). bytes_on_disk("é") → 2.

file-iotext-mode
DrillThe last few lines

Write tail(text, n): plant text as a file, read it back in text mode, and return its last n lines without their line endings. tail("a\nb\nc\n", 2) → ["b", "c"].

file-iotext-mode
BuildReading in chunks

Big files are read a chunk at a time. Write chunk_sizes(text, size): plant text, open it in binary mode, call f.read(size) until it returns empty bytes, and return the list of chunk lengths. chunk_sizes("abcdefghij", 4) → [4, 4, 2].

file-ioseek-tell
BossOne line ending to rule them

A yard mixes Windows (\r\n), old Mac (\r) and Unix (\n) endings. Write to_unix(text): plant text, read it back in text mode (which translates all three to \n), write the result to a second file with newline="\n", then read that second file in BINARY mode and return its bytes decoded as UTF-8. to_unix("a\r\nb\rc\n") → "a\nb\nc\n".

file-ionewline-translation
CapstoneAn index into a big file

To jump straight to line 50,000 later, Quarry records where every line starts. Write line_offsets(text): plant text, open it in BINARY mode, and return [[offset, line], ...] where offset is f.tell() before each readline() and line is that line decoded as UTF-8 without its ending (\r\n or \n). Then prove the index works: for the LAST entry, seek to its offset and check a fresh readline() gives the same line — return [] if it doesn't.

file-ioseek-tell
ExplainWhy every open() names its encoding

Quarry's code review rule is: every text-mode open() passes encoding= explicitly. A colleague says the default has always worked on their laptop. Explain what the default actually is, how the same code can break on another machine, and why the failure is especially nasty.

read_source.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
read_source(files, name) → dictSize in bytes, line count, first line and whether the file uses CRLF endings.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.