Appending Data to Files

Append mode is the right tool whenever you want to add to a file without destroying what is already there. Logs, event streams, running tallies, daily exports — each of these grows over time and should never be truncated by mistake. Opening a file with "a" positions the stream at the current end and every subsequent f.write() adds to that tail.

On POSIX systems, writes opened with O_APPEND (the underlying flag for "a") are atomic per write up to the size of a pipe buffer. That means several processes can append to the same log file without interleaving each other's writes inside a single line. On Windows the guarantee is weaker but still safe for line-level writes from a single process.

The most common append mistake is forgetting the trailing newline. f.write("hello") does not add one; successive calls will run into each other: "hellohello". Either append "\n" yourself or use print(value, file=f), which adds the OS-appropriate line separator automatically.

For anything that looks like a log, consider the logging module instead of hand-rolled file I/O. logging.basicConfig(filename="app.log") gives you thread-safe append, timestamps, log levels, and rotation with zero extra work. Rolling your own append is fine for small scripts and for structured event data where you want full control over the format.

Append vs write, and the end-of-file cursor

Opening with "w" truncates the file to zero bytes immediately, even before you call write. Opening with "a" does not truncate: it leaves the existing content intact and places the cursor at EOF. Inside an append-mode file, f.seek has no effect on writes — they always land at the end.

For binary logs, use "ab" the same way you would use "wb". Text files need encoding="utf-8" on both sides (read and append) so mixed runs don't corrupt the file.

Appending structured data

For CSV logs, open once with "a" and use csv.writer. Don't rewrite the header; check whether the file is empty first and call writeheader() only in that case. For JSONL (one JSON object per line) just f.write(json.dumps(obj) + "\n"). JSONL is a popular log format precisely because each line is independently parseable.

If you need rotation (“keep the last 7 days”), logging.handlers.RotatingFileHandler and TimedRotatingFileHandler do it safely. Do not try to rotate yourself while other processes are still writing.

The tools for adding to existing files.

ToolPurpose
open(p, 'a')
built-in
Opens a text file at EOF for appending.
open(p, 'ab')
built-in
Appends binary data.
print(value, file=f)
built-in
Writes + newline, platform-appropriate.
csv.writer(f)
class
Appends CSV rows to the open file.
json.dumps(obj)
function
Serializes one object for a JSONL line.
logging
module
Thread-safe append with timestamps and levels.
RotatingFileHandler
class
Log rotation by file size.
Path.stat().st_size
attribute
File size in bytes (useful for 'first write').

Appending Data to Files code example

The script below treats a file as a simple event log, appends JSONL records, and reads the file back afterwards.

# Lesson: Appending Data to Files
import json
from datetime import datetime, timezone
from pathlib import Path
from tempfile import gettempdir

log = Path(gettempdir()) / "events.log"
if log.exists():
    log.unlink()


def append_event(path: Path, **fields) -> None:
    """JSONL line with automatic timestamp."""
    fields.setdefault("ts", datetime.now(timezone.utc).isoformat())
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(fields) + "\n")


append_event(log, kind="login", user="ana")
append_event(log, kind="view",  user="ana", page="/home")
append_event(log, kind="logout", user="ana")

print("size:", log.stat().st_size, "bytes")
print("lines:")
with open(log, "r", encoding="utf-8") as f:
    for line in f:
        print(" ", json.loads(line))

# Appending never truncates: re-opening keeps history
append_event(log, kind="audit", user="ana", note="still here")
print("lines after re-append:", len(log.read_text(encoding="utf-8").splitlines()))

log.unlink()

Each block illustrates one append idea:

1) Mode 'a' opens at EOF; existing content is preserved.
2) JSONL is the easiest append-friendly structured format.
3) `f.write(json.dumps(obj) + '\n')` keeps lines parseable independently.
4) Re-opening the file later appends at the new EOF without re-reading anything.

Try a simple tally and a CSV append.

from pathlib import Path
from tempfile import gettempdir
import csv

log = Path(gettempdir()) / "count.log"
log.write_text("", encoding="utf-8")

# Example A: running tally using print(..., file=f)
with open(log, "a", encoding="utf-8") as f:
    for i in range(3):
        print(f"tick {i}", file=f)
print(log.read_text(encoding="utf-8"))

# Example B: CSV append with header only on first write
csv_path = Path(gettempdir()) / "tally.csv"
first = not csv_path.exists() or csv_path.stat().st_size == 0
with open(csv_path, "a", encoding="utf-8", newline="") as f:
    w = csv.writer(f)
    if first:
        w.writerow(["ts", "count"])
    w.writerow(["2025-01-01", 7])
print(csv_path.read_text(encoding="utf-8"))
log.unlink(); csv_path.unlink()

Simple logic checks you can reason about on paper.

import json
assert json.loads(json.dumps({"a": 1})) == {"a": 1}
lines = ["a\n", "b\n"]
joined = "".join(lines)
assert joined.count("\n") == 2
assert joined.splitlines() == ["a", "b"]

Running the script prints:

size: 240 bytes
lines:
  {'ts': '2025-01-01T10:00:00+00:00', 'kind': 'login', 'user': 'ana'}
  {'ts': '2025-01-01T10:00:05+00:00', 'kind': 'view', 'user': 'ana', 'page': '/home'}
  {'ts': '2025-01-01T10:00:10+00:00', 'kind': 'logout', 'user': 'ana'}
lines after re-append: 4