Skip to main content

Python Writing Files Practice Problems & Exercises

Practice: Writing Files

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Write and Read BackEasy
w modewrite()basic file I/O

Write a function write_greeting that opens a file in 'w' mode and writes "Hello, <name>!" (replacing the placeholder with the actual name parameter). Use encoding="utf-8".

This tests the most fundamental file writing pattern: open, write, close via a context manager.

Python
import os, tempfile

def write_greeting(filepath, name):
    with open(filepath, "w", encoding="utf-8") as f:
        f.write("Hello, " + name + "!")


# Test
path = os.path.join(tempfile.gettempdir(), "greeting.txt")
write_greeting(path, "Alice")

with open(path, "r", encoding="utf-8") as f:
    print(f.read())

os.remove(path)
Solution
def write_greeting(filepath, name):
with open(filepath, "w", encoding="utf-8") as f:
f.write("Hello, " + name + "!")

Key points:

  • 'w' mode creates the file if it does not exist, or truncates it if it does.
  • write() returns the number of characters written but does not add a newline.
  • Always specify encoding="utf-8" to avoid platform-dependent behavior.
import os, tempfile

def write_greeting(filepath, name):
  # TODO: Write "Hello, <name>!" to the file using 'w' mode
  # Include encoding="utf-8"
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "greeting.txt")
write_greeting(path, "Alice")

with open(path, "r", encoding="utf-8") as f:
  print(f.read())

os.remove(path)
Expected Output
Hello, Alice!
Hints

Hint 1: Open the file with open(filepath, 'w', encoding='utf-8') inside a with statement.

Hint 2: Use f.write() to write the greeting string. Remember write() does not add a newline automatically.

#2Append Log EntriesEasy
a modeappendnewlines

Write a function append_log that opens a file in 'a' mode and appends a single message followed by a newline.

Calling append_log three times should produce three lines in the file. This tests the difference between 'w' (truncate) and 'a' (append) mode.

Python
import os, tempfile

def append_log(filepath, message):
    with open(filepath, "a", encoding="utf-8") as f:
        f.write(message + "\n")


# Test
path = os.path.join(tempfile.gettempdir(), "app.log")

if os.path.exists(path):
    os.remove(path)

append_log(path, "Server started")
append_log(path, "Request received")
append_log(path, "Response sent")

with open(path, "r", encoding="utf-8") as f:
    print(f.read(), end="")

os.remove(path)
Solution
def append_log(filepath, message):
with open(filepath, "a", encoding="utf-8") as f:
f.write(message + "\n")

Key points:

  • 'a' mode preserves existing content and positions the cursor at the end.
  • Each call to append_log opens and closes the file independently — the content accumulates.
  • On POSIX systems, 'a' mode uses O_APPEND, making individual writes atomic at the OS level.
import os, tempfile

def append_log(filepath, message):
  # TODO: Append the message to the file followed by a newline
  # Use 'a' mode with encoding="utf-8"
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "app.log")

# Remove if exists from previous run
if os.path.exists(path):
  os.remove(path)

append_log(path, "Server started")
append_log(path, "Request received")
append_log(path, "Response sent")

with open(path, "r", encoding="utf-8") as f:
  print(f.read(), end="")

os.remove(path)
Expected Output
Server started
Request received
Response sent
Hints

Hint 1: Open the file with mode 'a' to append without truncating existing content.

Hint 2: Add a newline character after the message: f.write(message + '\n').

#3Write Multiple Lines with writelinesEasy
writelines()newlineslist writing

Write a function write_names that takes a filepath and a list of names, and writes each name on its own line using writelines().

The key insight: writelines() does not add newlines automatically — you must add them yourself.

Python
import os, tempfile

def write_names(filepath, names):
    with open(filepath, "w", encoding="utf-8") as f:
        f.writelines(name + "\n" for name in names)


# Test
path = os.path.join(tempfile.gettempdir(), "names.txt")
write_names(path, ["Alice", "Bob", "Charlie"])

with open(path, "r", encoding="utf-8") as f:
    print(f.read(), end="")

os.remove(path)
Solution
def write_names(filepath, names):
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(name + "\n" for name in names)

Key points:

  • writelines() accepts any iterable of strings. A generator expression avoids building a full list in memory.
  • The name is misleading: writelines() does not add line separators. It simply calls write() for each item.
  • Using a generator (name + "\n" for name in names) is memory-efficient for large lists.
import os, tempfile

def write_names(filepath, names):
  # TODO: Write each name on its own line using writelines()
  # Remember: writelines() does NOT add newlines for you
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "names.txt")
write_names(path, ["Alice", "Bob", "Charlie"])

with open(path, "r", encoding="utf-8") as f:
  print(f.read(), end="")

os.remove(path)
Expected Output
Alice
Bob
Charlie
Hints

Hint 1: writelines() writes each element of the iterable as-is, without adding newlines.

Hint 2: You need to add '\n' to each name before passing to writelines. A generator expression works well.

#4Exclusive Create GuardEasy
x modeFileExistsErrorexclusive create

Write a function safe_create that writes content to a file using 'x' (exclusive create) mode. If the file already exists, it should print "File already exists: <filepath>" and return False. If the file was created successfully, return True.

This tests the race-condition-free file creation pattern that avoids TOCTOU bugs.

Python
import os, tempfile

def safe_create(filepath, content):
    try:
        with open(filepath, "x", encoding="utf-8") as f:
            f.write(content)
        return True
    except FileExistsError:
        print("File already exists: " + filepath)
        return False


# Test
path = os.path.join(tempfile.gettempdir(), "unique_file.txt")

if os.path.exists(path):
    os.remove(path)

result1 = safe_create(path, "original data")
print(result1)

result2 = safe_create(path, "overwrite attempt")
print(result2)

with open(path, "r", encoding="utf-8") as f:
    print(f.read())

os.remove(path)
Solution
def safe_create(filepath, content):
try:
with open(filepath, "x", encoding="utf-8") as f:
f.write(content)
return True
except FileExistsError:
print("File already exists: " + filepath)
return False

Key points:

  • 'x' mode maps to O_CREAT | O_EXCL at the OS level — a single atomic system call.
  • This is safer than if not os.path.exists(path): open(path, 'w') which has a TOCTOU race condition.
  • FileExistsError is a subclass of OSError and is specific to this exact scenario.
import os, tempfile

def safe_create(filepath, content):
  # TODO: Write content to filepath using 'x' mode
  # If file already exists, print "File already exists: <filepath>"
  # and do NOT overwrite it
  # Return True if created, False if it already existed
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "unique_file.txt")

# Clean up from previous run
if os.path.exists(path):
  os.remove(path)

result1 = safe_create(path, "original data")
print(result1)

result2 = safe_create(path, "overwrite attempt")
print(result2)

with open(path, "r", encoding="utf-8") as f:
  print(f.read())

os.remove(path)
Expected Output
True
File already exists: /tmp/unique_file.txt
False
original data
Hints

Hint 1: Use 'x' mode in the open() call — it raises FileExistsError if the file exists.

Hint 2: Wrap the open() in a try/except FileExistsError block.


Medium

#5Truncation Trap DetectorMedium
w modetruncationwrite behavior

Write a function demonstrate_truncation that:

  1. Writes "AAAAAAAAAA" (10 A characters) to the file in 'w' mode
  2. Opens the same file again in 'w' mode and writes "BB" (2 B characters)
  3. Reads and returns the file contents

The returned string should be 'BB' (length 2), not 'BBAAAAAAAA'. This demonstrates that 'w' mode truncates the file to zero length at open() time.

Python
import os, tempfile

def demonstrate_truncation(filepath):
    with open(filepath, "w", encoding="utf-8") as f:
        f.write("AAAAAAAAAA")

    with open(filepath, "w", encoding="utf-8") as f:
        f.write("BB")

    with open(filepath, "r", encoding="utf-8") as f:
        return f.read()


# Test
path = os.path.join(tempfile.gettempdir(), "trunc_test.txt")
result = demonstrate_truncation(path)
print(repr(result))
print(len(result))

os.remove(path)
Solution
def demonstrate_truncation(filepath):
with open(filepath, "w", encoding="utf-8") as f:
f.write("AAAAAAAAAA")

with open(filepath, "w", encoding="utf-8") as f:
f.write("BB")

with open(filepath, "r", encoding="utf-8") as f:
return f.read()

Key points:

  • 'w' mode truncates the file immediately at open() — before write() is called.
  • The file becomes empty at the moment you call open(path, "w"). If a crash happens before the new write(), you lose all data.
  • This is why production systems use the atomic write pattern (write to temp, then os.replace()).
import os, tempfile

def demonstrate_truncation(filepath):
  # Step 1: Write "AAAAAAAAAA" (10 A's) to the file in 'w' mode
  # Step 2: Open the SAME file in 'w' mode and write "BB" (2 B's)
  # Step 3: Read and return the file contents
  # TODO: implement all three steps
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "trunc_test.txt")
result = demonstrate_truncation(path)
print(repr(result))
print(len(result))

os.remove(path)
Expected Output
'BB'
2
Hints

Hint 1: Opening a file in 'w' mode truncates it to zero length BEFORE any write happens.

Hint 2: After step 2, the file contains only 'BB' — the 10 A's are completely gone.

#6Numbered Line WriterMedium
write()writelines()string formatting

Write a function write_numbered_lines that takes a filepath and a list of lines, and writes each line prefixed with its 1-based line number in the format "N: line text\n".

Use writelines() with a generator expression and enumerate().

Python
import os, tempfile

def write_numbered_lines(filepath, lines):
    with open(filepath, "w", encoding="utf-8") as f:
        f.writelines(
            str(i) + ": " + line + "\n"
            for i, line in enumerate(lines, start=1)
        )


# Test
path = os.path.join(tempfile.gettempdir(), "numbered.txt")
write_numbered_lines(path, ["Buy groceries", "Walk the dog", "Write code"])

with open(path, "r", encoding="utf-8") as f:
    print(f.read(), end="")

os.remove(path)
Solution
def write_numbered_lines(filepath, lines):
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(
str(i) + ": " + line + "\n"
for i, line in enumerate(lines, start=1)
)

Key points:

  • enumerate(lines, start=1) yields (1, line), (2, line), ... tuples.
  • The generator expression builds each formatted line lazily — no intermediate list is created in memory.
  • writelines() iterates through the generator and calls write() for each string.
import os, tempfile

def write_numbered_lines(filepath, lines):
  # TODO: Write each line prefixed with its 1-based line number
  # Format: "1: first line
2: second line
" etc.
  # Use writelines() with a generator expression
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "numbered.txt")
write_numbered_lines(path, ["Buy groceries", "Walk the dog", "Write code"])

with open(path, "r", encoding="utf-8") as f:
  print(f.read(), end="")

os.remove(path)
Expected Output
1: Buy groceries
2: Walk the dog
3: Write code
Hints

Hint 1: Use enumerate(lines, start=1) to get 1-based indices.

Hint 2: Build each formatted line with a newline at the end, then pass the generator to writelines().

#7Flush vs. Sync DemonstratorMedium
flush()os.fsync()bufferingdurability

Write a function write_with_levels that demonstrates the three layers of file writing:

  1. f.write(data) — data is in Python's internal buffer
  2. f.flush() — data is moved to the OS page cache
  3. os.fsync(f.fileno()) — data is forced to physical disk

Return a tuple (True, True, True) after all three steps complete successfully, and also ensure the file contains the data.

Python
import os, tempfile

def write_with_levels(filepath, data):
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(data)
        python_buffered = True

        f.flush()
        os_buffered = True

        os.fsync(f.fileno())
        on_disk = True

    return (python_buffered, os_buffered, on_disk)


# Test
path = os.path.join(tempfile.gettempdir(), "sync_demo.txt")
result = write_with_levels(path, "critical transaction data")
print(result)

with open(path, "r", encoding="utf-8") as f:
    print(f.read())

os.remove(path)
Solution
def write_with_levels(filepath, data):
with open(filepath, "w", encoding="utf-8") as f:
f.write(data)
python_buffered = True

f.flush()
os_buffered = True

os.fsync(f.fileno())
on_disk = True

return (python_buffered, os_buffered, on_disk)

Key points:

  • f.write() puts data into Python's BufferedWriter buffer (8 KB by default). It may not reach the OS yet.
  • f.flush() pushes the Python buffer to the OS kernel's page cache. Other processes can now read it, but a power failure can lose it.
  • os.fsync(f.fileno()) issues a system call that forces the OS to write its page cache to the physical disk and waits for confirmation. Only after this is data truly durable.
  • f.fileno() returns the OS-level file descriptor (an integer) needed by os.fsync().
import os, tempfile

def write_with_levels(filepath, data):
  # TODO:
  # 1. Open the file in 'w' mode
  # 2. Write the data string
  # 3. Call flush() — this moves data from Python buffer to OS page cache
  # 4. Call os.fsync() on the file descriptor — this forces data to disk
  # 5. Return a tuple: (python_buffered, os_buffered, on_disk)
  #    where each value is True after that step completes
  # The function should track these three states
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "sync_demo.txt")
result = write_with_levels(path, "critical transaction data")
print(result)

with open(path, "r", encoding="utf-8") as f:
  print(f.read())

os.remove(path)
Expected Output
(True, True, True)
critical transaction data
Hints

Hint 1: After f.write(), data is in Python's internal buffer (python_buffered = True).

Hint 2: After f.flush(), data is in the OS page cache (os_buffered = True). After os.fsync(f.fileno()), it is on disk (on_disk = True).

#8Line-Buffered LoggerMedium
bufferingline-bufferedflush behavior

Write a function create_line_buffered_log that opens a file with line buffering (buffering=1) and writes each entry followed by a newline. After each write, record the file size using os.path.getsize().

With line buffering, each newline triggers an automatic flush, so the file size grows after every line.

Python
import os, tempfile

def create_line_buffered_log(filepath, entries):
    sizes = []
    with open(filepath, "w", encoding="utf-8", buffering=1) as f:
        for entry in entries:
            f.write(entry + "\n")
            sizes.append(os.path.getsize(filepath))
    return sizes


# Test
path = os.path.join(tempfile.gettempdir(), "line_buf.log")
sizes = create_line_buffered_log(path, ["INFO: start", "WARN: low memory", "ERROR: crash"])
print(sizes)

with open(path, "r", encoding="utf-8") as f:
    print(f.read(), end="")

os.remove(path)
Solution
def create_line_buffered_log(filepath, entries):
sizes = []
with open(filepath, "w", encoding="utf-8", buffering=1) as f:
for entry in entries:
f.write(entry + "\n")
sizes.append(os.path.getsize(filepath))
return sizes

Key points:

  • buffering=1 in text mode means line-buffered: the internal buffer is flushed each time a \n is written.
  • This is useful for log files where you want real-time visibility — a tail -f on the file sees each line immediately.
  • Without line buffering (default 8 KB buffer), data might not appear on disk until the buffer is full or the file is closed.
  • Line buffering only works in text mode. In binary mode, buffering=1 means a 1-byte buffer (not line-buffered).
import os, tempfile

def create_line_buffered_log(filepath, entries):
  # TODO:
  # 1. Open the file in 'w' mode with line buffering (buffering=1)
  # 2. For each entry, write it followed by a newline
  # 3. After writing each entry, verify it is visible by reading
  #    the file size with os.path.getsize()
  # 4. Return a list of file sizes after each write
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "line_buf.log")
sizes = create_line_buffered_log(path, ["INFO: start", "WARN: low memory", "ERROR: crash"])
print(sizes)

with open(path, "r", encoding="utf-8") as f:
  print(f.read(), end="")

os.remove(path)
Expected Output
[12, 29, 43]
INFO: start
WARN: low memory
ERROR: crash
Hints

Hint 1: Use buffering=1 in the open() call — this enables line buffering in text mode.

Hint 2: Line buffering means the buffer is flushed automatically whenever a newline character is written.


Hard

#9Atomic File WriterHard
atomic writetempfileos.replace()os.fsync()

Implement atomic_write(filepath, content) using the write-to-temp-then-rename pattern:

  1. Create a temp file in the same directory as the target (required for atomicity)
  2. Write content, flush, and fsync
  3. Use os.replace() to atomically swap the temp file into place
  4. On failure, clean up the temp file

This is the production pattern for safely updating critical files without risk of corruption.

Python
import os, tempfile, json

def atomic_write(filepath, content):
    dir_path = os.path.dirname(os.path.abspath(filepath))
    fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")

    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
            f.flush()
            os.fsync(f.fileno())

        os.replace(tmp_path, filepath)

    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise


# Test
path = os.path.join(tempfile.gettempdir(), "atomic_test.json")

atomic_write(path, json.dumps({"version": 1}, indent=2))

with open(path, "r", encoding="utf-8") as f:
    print(f.read())

atomic_write(path, json.dumps({"version": 2, "updated": True}, indent=2))

with open(path, "r", encoding="utf-8") as f:
    print(f.read())

os.remove(path)
Solution
def atomic_write(filepath, content):
dir_path = os.path.dirname(os.path.abspath(filepath))
fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")

try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
f.flush()
os.fsync(f.fileno())

os.replace(tmp_path, filepath)

except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise

Key points:

  • The temp file must be in the same directory (same filesystem) as the target. Cross-filesystem renames are not atomic.
  • os.fdopen(fd, ...) wraps the raw OS file descriptor from mkstemp() into a Python file object.
  • f.flush() then os.fsync() ensures data is physically on disk before the rename.
  • os.replace() is atomic on POSIX — readers always see either the complete old file or the complete new file.
  • The except block cleans up the temp file to avoid leaving orphaned .tmp files.
import os, tempfile, json

def atomic_write(filepath, content):
  # TODO: Implement the atomic write pattern:
  # 1. Create a temp file in the SAME directory as filepath
  #    using tempfile.mkstemp(dir=..., suffix=".tmp")
  # 2. Write content to the temp file using os.fdopen()
  # 3. Flush and fsync the temp file
  # 4. Use os.replace() to atomically swap it into place
  # 5. If anything fails, clean up the temp file
  pass


# Test
path = os.path.join(tempfile.gettempdir(), "atomic_test.json")

# Write initial content
atomic_write(path, json.dumps({"version": 1}, indent=2))

with open(path, "r", encoding="utf-8") as f:
  print(f.read())

# Overwrite atomically
atomic_write(path, json.dumps({"version": 2, "updated": True}, indent=2))

with open(path, "r", encoding="utf-8") as f:
  print(f.read())

os.remove(path)
Expected Output
{
"version": 1
}
{
"updated": true,
"version": 2
}
Hints

Hint 1: Use os.path.dirname(os.path.abspath(filepath)) to get the directory for the temp file.

Hint 2: tempfile.mkstemp() returns (fd, tmp_path). Use os.fdopen(fd, 'w', encoding='utf-8') to get a file object.

Hint 3: Wrap the write in try/except — in the except block, delete the temp file with os.unlink(tmp_path).

#10NamedTemporaryFile PipelineHard
tempfileNamedTemporaryFilepipelineio.StringIO

Build a transform_pipeline that passes text through a chain of transformations using temporary files at each stage:

  1. Write input lines to a temp file
  2. For each transformation: read the file, apply the transform to each line, write to a new temp file, delete the old one
  3. Return the final transformed text

This tests NamedTemporaryFile with delete=False, file lifecycle management, and multi-stage processing.

Python
import os, tempfile, io

def transform_pipeline(input_lines, transformations):
    # Write initial input to a temp file
    tmp = tempfile.NamedTemporaryFile(
        mode="w", suffix=".txt", delete=False, encoding="utf-8"
    )
    tmp.writelines(line + "\n" for line in input_lines)
    tmp.close()
    current_path = tmp.name

    for transform in transformations:
        # Read current file
        with open(current_path, "r", encoding="utf-8") as f:
            lines = f.read().splitlines()

        # Write transformed lines to new temp file
        new_tmp = tempfile.NamedTemporaryFile(
            mode="w", suffix=".txt", delete=False, encoding="utf-8"
        )
        new_tmp.writelines(transform(line) + "\n" for line in lines)
        new_tmp.close()

        # Delete old, advance to new
        os.unlink(current_path)
        current_path = new_tmp.name

    # Read final result
    with open(current_path, "r", encoding="utf-8") as f:
        result = f.read()

    os.unlink(current_path)
    return result


# Test
lines = ["hello world", "foo bar baz", "python is great"]
transforms = [
    str.upper,
    str.title,
    lambda line: line.replace(" ", "_"),
]

result = transform_pipeline(lines, transforms)
print(result, end="")
Solution
def transform_pipeline(input_lines, transformations):
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
)
tmp.writelines(line + "\n" for line in input_lines)
tmp.close()
current_path = tmp.name

for transform in transformations:
with open(current_path, "r", encoding="utf-8") as f:
lines = f.read().splitlines()

new_tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
)
new_tmp.writelines(transform(line) + "\n" for line in lines)
new_tmp.close()

os.unlink(current_path)
current_path = new_tmp.name

with open(current_path, "r", encoding="utf-8") as f:
result = f.read()

os.unlink(current_path)
return result

Key points:

  • delete=False is essential — without it, the file is deleted when closed, and you cannot reopen it for the next stage.
  • Each stage reads from the previous temp file and writes to a new one, then deletes the old one.
  • splitlines() removes the trailing newlines, so each transformation receives clean strings.
  • Always clean up temp files with os.unlink()mkstemp() and NamedTemporaryFile(delete=False) do not auto-delete.
import os, tempfile, io

def transform_pipeline(input_lines, transformations):
  # TODO:
  # 1. Write input_lines to a NamedTemporaryFile (delete=False, mode="w")
  # 2. For each transformation function in the list:
  #    a. Read the current temp file contents
  #    b. Apply the transformation to each line
  #    c. Write the transformed lines to a NEW NamedTemporaryFile
  #    d. Delete the old temp file
  #    e. The new temp file becomes the current one
  # 3. Read and return the final contents as a string
  # 4. Clean up the final temp file
  pass


# Test
lines = ["hello world", "foo bar baz", "python is great"]
transforms = [
  str.upper,
  str.title,
  lambda line: line.replace(" ", "_"),
]

result = transform_pipeline(lines, transforms)
print(result, end="")
Expected Output
Hello_World
Foo_Bar_Baz
Python_Is_Great
Hints

Hint 1: Use tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') for each stage.

Hint 2: After writing, close the file, then reopen it for reading. NamedTemporaryFile with delete=False lets you control cleanup.

Hint 3: Track the current file path and delete old temp files with os.unlink() after each transformation stage.

#11StringIO Report CaptureHard
io.StringIOin-memory filetestinggetvalue()

Write two functions:

  1. generate_table(output, headers, rows) — writes a formatted table to any file-like object
  2. capture_table(headers, rows) — uses io.StringIO to capture the table in memory and return it as a string

This tests the pattern of writing code that works with both real files and in-memory buffers — essential for testable I/O code.

Python
import io

def generate_table(output, headers, rows):
    header_line = " | ".join(headers)
    output.write(header_line + "\n")
    output.write("-" * len(header_line) + "\n")
    for row in rows:
        output.write(" | ".join(row) + "\n")


def capture_table(headers, rows):
    buffer = io.StringIO()
    generate_table(buffer, headers, rows)
    return buffer.getvalue()


# Test
headers = ["Name", "Age", "City"]
rows = [
    ["Alice", "30", "New York"],
    ["Bob", "25", "London"],
    ["Charlie", "35", "Tokyo"],
]

table_str = capture_table(headers, rows)
print(table_str, end="")
print("---")
print("Captured", len(table_str), "characters")
Solution
def generate_table(output, headers, rows):
header_line = " | ".join(headers)
output.write(header_line + "\n")
output.write("-" * len(header_line) + "\n")
for row in rows:
output.write(" | ".join(row) + "\n")


def capture_table(headers, rows):
buffer = io.StringIO()
generate_table(buffer, headers, rows)
return buffer.getvalue()

Key points:

  • generate_table accepts any file-like object — a real file, sys.stdout, or io.StringIO. This makes the function testable without disk I/O.
  • io.StringIO implements the same interface as a text file: write(), read(), seek(), getvalue().
  • getvalue() returns the entire buffer contents without needing to seek(0) first — unlike read() which depends on the current position.
  • This dependency-injection pattern (pass the output target in) is a hallmark of well-designed I/O code.
import io

def generate_table(output, headers, rows):
  # TODO: Write a formatted table to the output file object
  # 1. Write column headers separated by " | " with a newline
  # 2. Write a separator line of dashes (length matches header line)
  # 3. Write each row with values separated by " | " with a newline
  # The function should accept ANY file-like object (real file or StringIO)
  pass


def capture_table(headers, rows):
  # TODO: Use io.StringIO to capture the table output in memory
  # 1. Create a StringIO buffer
  # 2. Call generate_table with the buffer
  # 3. Return the captured string using getvalue()
  pass


# Test
headers = ["Name", "Age", "City"]
rows = [
  ["Alice", "30", "New York"],
  ["Bob", "25", "London"],
  ["Charlie", "35", "Tokyo"],
]

# Capture in memory
table_str = capture_table(headers, rows)
print(table_str, end="")
print("---")
print("Captured", len(table_str), "characters")
Expected Output
Name | Age | City
-----------------
Alice | 30 | New York
Bob | 25 | London
Charlie | 35 | Tokyo
---
Captured 73 characters
Hints

Hint 1: Join headers with ' | ' and add a newline. Do the same for each row.

Hint 2: The separator length should match the header line (without the newline). Use '-' * len(header_line).

Hint 3: In capture_table, create an io.StringIO(), pass it to generate_table, then return buffer.getvalue().

© 2026 EngineersOfAI. All rights reserved.