PYTHON / FILE HANDLING
Writing and appending to text files
Create, overwrite, and extend text files with write(), writelines(), and append mode, and control exactly when bytes reach disk.
What you will learn
- Choose between 'w' (truncate at open) and 'a' (writes pinned to end of file)
- Use write() knowing it adds no newline and returns the character count
- Feed writelines() strings that already end in \n, or add them yourself
- Force data to disk with flush() or by letting the with block close the file
Understanding Writing and appending to text files
A file object opened for writing is a cursor plus a buffer. write() takes one str, copies it into that buffer, and returns how many characters it accepted; it never adds a separator, so "a" followed by "b" produces the two-character file ab, not two lines. If you want newlines, you put them in the string yourself, or use print("text", file=f), which applies its usual end="\n".
The difference between 'w' and 'a' is decided at open time, not at write time. Opening with 'w' truncates the file to zero bytes immediately, before your first write() call and even if you never write at all, which is why a mistyped mode can erase a file a script only meant to extend. Opening with 'a' sets the operating system's append flag, so every write lands at the current end of file; seeking backwards first does not change that, and two processes appending to the same log will not overwrite each other's lines.
Writes are buffered, typically about 8 KB for a text file, so what you wrote is not on disk yet when write() returns. Closing the file flushes the buffer, which is the practical reason a write should live inside a with block: if you open a file, write, and read it back in the same script without closing, you can legitimately read nothing. Call flush() when you need the data visible right now, such as a log you are tailing in another terminal.
with open("notes.txt", "w", encoding="utf-8") as f:
n = f.write("first line\n")
print("characters written:", n)
f.write("second line\n")
with open("notes.txt", "a", encoding="utf-8") as f:
f.write("appended line\n")
with open("notes.txt", encoding="utf-8") as f:
print(f.read(), end="")Truncation and append positioning are fixed by the mode you pass to open(), while the actual bytes only reach disk when the buffer is flushed or the file is closed.
Worked examples
writelines does not add newlines
Shows that writelines concatenates strings verbatim, and how print(file=...) differs.
rows = ["alpha", "beta", "gamma"]
with open("out.txt", "w", encoding="utf-8") as f:
f.writelines(rows)
with open("out.txt", encoding="utf-8") as f:
print(repr(f.read()))
with open("out.txt", "w", encoding="utf-8") as f:
f.writelines(line + "\n" for line in rows)
print("done", file=f)
with open("out.txt", encoding="utf-8") as f:
print(repr(f.read()))Example explained
Line 1f.writelines(rows) writes the three strings back to back; the name promises lines but the method only concatenates.
Line 2repr() is used instead of print(text) so the missing and present \n characters are visible.
Line 3The generator line + "\n" for line in rows supplies the terminators, and writelines accepts any iterable of strings.
Line 4print("done", file=f) writes to the open file and contributes its own trailing newline.
'w' truncates at open, 'a' ignores seek
Demonstrates that the file is emptied by open() itself and that append mode always writes at the end.
import os
with open("log.txt", "w", encoding="utf-8") as f:
f.write("original content\n")
print("size before:", os.path.getsize("log.txt"))
f = open("log.txt", "w", encoding="utf-8")
print("size after opening in w:", os.path.getsize("log.txt"))
f.close()
with open("log.txt", "a", encoding="utf-8") as f:
f.seek(0)
f.write("appended anyway\n")
with open("log.txt", encoding="utf-8") as f:
print(repr(f.read()))Example explained
Line 1The second open() call has not written anything, yet getsize reports 0: truncation is part of opening in 'w'.
Line 2f.seek(0) moves the cursor to the start, but the append flag makes the kernel place the write at the end regardless.
Line 3The file was empty at that point, so end of file is offset 0 and the result is a single line.
Line 4os.path.getsize reads the real file on disk, which is why it is a reliable probe here.
Nothing is on disk until you flush
Reads a file from a second handle to prove the written text is still sitting in the buffer.
f = open("buf.txt", "w", encoding="utf-8")
f.write("pending\n")
with open("buf.txt", encoding="utf-8") as check:
print("visible before flush:", repr(check.read()))
f.flush()
with open("buf.txt", encoding="utf-8") as check:
print("visible after flush:", repr(check.read()))
f.close()Example explained
Line 1"pending\n" is far smaller than the default text buffer, so write() keeps it in memory.
Line 2The independent handle named check sees an empty file, which is what any other program would see too.
Line 3f.flush() pushes the buffer to the operating system, and the same read now returns the text.
Line 4f.close() would have flushed as well; a with block around f makes that automatic.
Building a log with append mode in a loop
Shows the common pattern of opening once in 'a' and writing several records, plus counting written characters.
events = [("start", 0), ("load", 12), ("stop", 30)]
total = 0
with open("events.log", "a", encoding="utf-8") as log:
for name, ms in events:
total += log.write(f"{ms:>4} {name}\n")
print("characters appended:", total)
with open("events.log", encoding="utf-8") as log:
for line in log:
print(repr(line))Example explained
Line 1Mode 'a' creates events.log if it does not exist, so no separate setup step is needed.
Line 2log.write returns the length of each written string, so summing the return values counts characters.
Line 3The f-string with {ms:>4} does the alignment; write itself never formats anything.
Line 4Re-running this script would add three more lines instead of replacing the file.
Important notes
In text mode on Windows, each "\n" you write becomes "\r\n" on disk; pass newline="" to open() when you need the exact bytes preserved.
Mode 'a' cannot be used to overwrite earlier parts of a file; if you need in-place edits, read the whole text, change it in memory, and rewrite with 'w'.
Common mistakes
Reaching for 'w' when the intent was to add to an existing file: the previous contents are gone the moment open() returns, before any write.
Assuming write() behaves like print() and adds a line break, which produces one long run-together line such as alphabetagamma.
Reading the file back while the writing handle is still open and unflushed, then concluding the write failed because the read returned an empty string.
Passing a non-string to write(), so f.write(42) raises TypeError instead of writing 42; convert with str() or use an f-string.
Try it yourself
Change, predict, then run
Write a script that creates shopping.txt in 'w' mode with the lines bread, milk, eggs, then reopens it in 'a' mode and adds coffee. Print the final contents with repr() and confirm every line ends in \n.
Open the Python workspaceCheck your understanding
A script opens report.txt with mode 'w' and then crashes on the next line, before calling write(). What state is report.txt in?
- Empty, because opening in 'w' truncates the file as part of open()
- Unchanged, since nothing was ever written to the file object
- Deleted from disk, because the interpreter exited without closing it
- Unchanged until close() runs, which is what would have applied the truncation
Show answer
Mode 'w' truncates at open time, so the old contents are lost the instant open() succeeds, whether or not a write follows. The tempting answer is that nothing was written so nothing changed, but truncation is an effect of opening rather than of writing, and it is not deferred until close().