Typorithm · medium

Replacing a report without exposing a partial file

pythonapi-clientsautomationindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Replacing a report without exposing a partial file

0%

Concept and contract

Write a complete temporary report beside its target, then replace the visible file and clean up a failed attempt.

A report reader should not observe half of a newly generated JSON document. Writing directly over the visible file exposes partial contents if the writer fails during the operation.

This lesson writes a temporary file in the target directory and then replaces the target once the complete payload is ready. The output directory is a trusted disposable workspace chosen by the caller, not an arbitrary untrusted path sandbox.

Mechanism and worked example

The function validates identifiers and serializes the complete payload before creating a temporary sibling.

The function validates identifiers and serializes the complete payload before creating a temporary sibling. It writes UTF-8 text, flushes Python buffers, requests a file sync and closes the handle before os.replace.

A finally block removes a remaining temporary path after either success or failure. The temporary file shares the target directory to avoid a cross-filesystem rename assumption. Existing directory creation and authorization belong to the caller’s separate setup policy.

Complete worked example

python

import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile

def write_report(path, ids):
    if not isinstance(ids, list) or any(not isinstance(item, str) for item in ids):
        raise ValueError("identifier list required")
    payload = json.dumps({"ids": ids}, ensure_ascii=False, sort_keys=True)
    target, temporary = Path(path), None
    try:
        with NamedTemporaryFile("w", encoding="utf-8", newline="\n", dir=target.parent, prefix="." + target.name + ".", delete=False) as handle:
            temporary = Path(handle.name)
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, target)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)
    return len(ids)
Reasoning
Write a complete temporary report beside its target, then replace the visible file and clean up a failed attempt. Compare the stated values and failure boundary before changing the implementation.

Counterexamples and common mistakes

The counterexample writes the visible target directly instead of using the final replacement boundary.

The counterexample writes the visible target directly instead of using the final replacement boundary. It can produce the correct bytes in ordinary cases while destroying the old report during a failed publication.

The lab injects a replacement failure to expose that distinction. Atomic visibility is not a universal power-loss durability guarantee: filesystem behavior and syncing the containing directory need separate consideration when a crash-durable commit is required.

Counterexample: a runnable policy defect

python

import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile

def write_report(path, ids):
    if not isinstance(ids, list) or any(not isinstance(item, str) for item in ids):
        raise ValueError("identifier list required")
    payload = json.dumps({"ids": ids}, ensure_ascii=False, sort_keys=True)
    target, temporary = Path(path), None
    try:
        with NamedTemporaryFile("w", encoding="utf-8", newline="\n", dir=target.parent, prefix="." + target.name + ".", delete=False) as handle:
            temporary = Path(handle.name)
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        target.write_text(payload, encoding="utf-8")
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)
    return len(ids)
Reasoning
This runnable counterexample violates the declared contract; the companion lab requires it to fail.

Independent practice: API client task

Add a single trailing newline to the published JSON report while preserving the temporary-file and replacement sequence.

Add a single trailing newline to the published JSON report while preserving the temporary-file and replacement sequence. Start with an existing fictional report, publish an empty identifier list and compare its exact bytes.

Then run the injected failure check. This task changes output formatting only; it must not weaken preservation of the previous report when the replacement operation fails.

Step 1: Append exactly one newline to the serialized report. Step 2: Keep replacement failure from changing the previous visible file.

Practice starter: solve before reading the solution

python

import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile

def write_report(path, ids):
    # Remove this placeholder, then adapt the retained baseline:
    # Append exactly one newline to the serialized report.
    raise NotImplementedError
    if not isinstance(ids, list) or any(not isinstance(item, str) for item in ids):
        raise ValueError("identifier list required")
    payload = json.dumps({"ids": ids}, ensure_ascii=False, sort_keys=True)
    target, temporary = Path(path), None
    try:
        with NamedTemporaryFile("w", encoding="utf-8", newline="\n", dir=target.parent, prefix="." + target.name + ".", delete=False) as handle:
            temporary = Path(handle.name)
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, target)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)
    return len(ids)
Reasoning
Append exactly one newline to the serialized report. Keep replacement failure from changing the previous visible file.
  • Hint 1: Adjust payload before creating and writing the temporary sibling.
  • Hint 2: Run both exact-byte cases and the injected replacement-failure probe.

Solution, tests and trade-offs

The normal cases inspect returned item count and the actual UTF-8 file contents, including a non-ASCII identifier.

The normal cases inspect returned item count and the actual UTF-8 file contents, including a non-ASCII identifier. The special probe begins with a saved report, makes os.replace fail, verifies the old bytes remain and checks that no temporary sibling is left behind.

The practice repeats those checks with the newline policy. All paths live inside the checker’s temporary directory, and no production report or user document participates.

Practice solution and acceptance checks

python

import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile

def write_report(path, ids):
    if not isinstance(ids, list) or any(not isinstance(item, str) for item in ids):
        raise ValueError("identifier list required")
    payload = json.dumps({"ids": ids}, ensure_ascii=False, sort_keys=True)
    payload += "\n"
    target, temporary = Path(path), None
    try:
        with NamedTemporaryFile("w", encoding="utf-8", newline="\n", dir=target.parent, prefix="." + target.name + ".", delete=False) as handle:
            temporary = Path(handle.name)
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, target)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)
    return len(ids)
Reasoning
Adjust payload before creating and writing the temporary sibling. Run both exact-byte cases and the injected replacement-failure probe.

Interview reasoning and next step

Explain which operation makes new contents visible to a reader and what remains true if it raises an error.

Explain which operation makes new contents visible to a reader and what remains true if it raises an error. Keep that publication boundary separate from path authorization, database transactions and application-level version identity.

A multi-file report needs an additional manifest or directory-switch protocol. This single-file example prepares the next persistence lesson, where the corresponding all-or-nothing boundary is a transaction rather than a rename.

Optional stretch — after the core task works: Design a parent-directory sync policy for a declared crash-durability requirement.

Computer practice: https://applydjinn.com/typorithm/python-automation-lab-20261105.zip. Run verify.py using your selected Python interpreter, then adapt the retained exercise baseline and check it with --practice followed by the lesson id. The standard-library lab uses owned temporary 127.0.0.1 loopback servers, fictional pages and disposable files; it never scans production feeds or opens the working ApplyDjinn database.

Write a complete temporary report beside its target, then replace the visible file and clean up a failed attempt.