Python: File Handling, Reading and Writing Text Files

Close a program and everything it computed vanishes with it: every variable, every user entry, gone. Python file handling is how a program remembers, by writing data to disk and reading it back on the next run. This guide covers reading, writing, and appending text files with the with statement, plus the mode and encoding choices that quietly decide whether your data survives.

“Make it work, make it right, make it fast.”

Kent Beck, TDD By Example

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 17 minutes

Every program you have written so far forgets everything the moment it ends. You run it, you read the output, you close the terminal, and it is all gone. Nothing was saved. Build a todo app this way and the user loses every task the second they quit. Python file handling is the fix: it lets your program write data to disk and read it back later, so your work survives past a single run.

Think of a file like a notebook on your desk. You can open it, read what is already there, jot down a new line, or flip to the last page and keep adding. The thing you must never forget is to close the notebook when you are done, otherwise you leave it lying open and someone else cannot use it. Python has a clean tool for exactly this: the built-in open() function paired with the with statement, which closes the notebook for you automatically, even if your code crashes halfway through.

The best part is there is nothing to install. File handling is baked into Python itself, so you can open a file in your very first line of code. This post walks through reading, writing, and appending text files, the file modes that decide what happens to your data, encoding (the thing that quietly breaks when you save an emoji), and the patterns that stop you from wiping out a file by accident. By the end you will handle files with confidence and dodge the mistakes that silently lose data.

Opening and Closing Files: The Old Way

‘x’: Exclusive CreateWrite onlyCreates new fileFileExistsError if existsSafe creation‘w+’: Write+ReadWrite and ReadCreates if missingTRUNCATES if exists!Cursor at start‘r+’: Read+WriteRead and WriteFile must existNo truncationCursor at start‘a’: AppendWrite onlyCreates if missingPreserves contentCursor at END‘w’: WriteWrite onlyCreates if missingTRUNCATES if exists!Cursor at start‘r’: ReadRead onlyFile must existCursor at startFileNotFoundError if missingPython File Modes: r, w, a, r+, w+, x Compared by Truncation and Cursor

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram compares Python’s file modes, read ('r'), write ('w'), and append ('a'), showing where the cursor starts and what happens to existing content. The detail that matters most is the difference between write and append. Write truncates, meaning it deletes everything first, while append positions the cursor at the end and keeps what is already there. Get this one right and you avoid the most common file handling mistake of all: erasing data by opening in write mode when you meant to append.

📄 manual_close.py: do not do this

# The dangerous way: if an error occurs, the file stays open
f = open("sample.txt", "w")
f.write("Hello, TechnoScripts!\n")
f.write("This is line 2.\n")
f.close()  # Must remember to close!

# What if an error happens between open() and close()?
# The file handle leaks, bad for long-running programs

Here is the catch. If your code crashes between open() and close(), that close() line never runs and the file handle is never released. It is like walking away from your notebook and forgetting to shut it. On a web server handling thousands of requests, those leaked handles pile up until the operating system refuses to open any more and the process falls over. There is a cleaner way that closes the file for you.

Context Managers: The Right Way (with statement)

The with statement is like the automatic door at a supermarket: you walk through and it closes behind you, whether you remembered it or not. In the example below, a developer named Rahul writes a few lines to a file, then reads them back to prove they landed on disk.

📄 context_manager.py: always use ‘with’ for files

# The right way: file is automatically closed, even if errors occur
with open("sample.txt", "w") as f:
    f.write("Hello, TechnoScripts!\n")
    f.write("This is line 2.\n")
    f.write("Written by Rahul's Python script.\n")

# File is closed here automatically, guaranteed
print("File written successfully!")

# Verify
with open("sample.txt", "r") as f:
    content = f.read()
    print(content)

▶ Output

File written successfully!
Hello, TechnoScripts!
This is line 2.
Written by Rahul's Python script.

What happened here: The with statement creates a context manager. The moment the indented block ends, whether it finished normally or blew up with an exception, Python calls f.close() for you. You never have to remember it yourself. This is the one habit to lock in early: in real Python code, you should almost always open files with with.

Reading Files: Three Approaches

Reading a file is like reading a book: you can photocopy the whole thing in one go, or turn one page at a time. Which approach you pick matters once files get big. Say you keep a small roster of your project teammates, Rahul, Niranjan, Viraj, and Pravin, in a text file. Here are all the ways to read it back.

📄 reading_methods.py: read(), readline(), readlines()

# First, create a test file
with open("team.txt", "w") as f:
    f.write("Rahul Mahadik - Backend\n")
    f.write("Niranjan Raut - Frontend\n")
    f.write("Viraj Patil - DevOps\n")
    f.write("Pravin Sharma - Testing\n")

# Method 1: read() reads the entire file as one string
with open("team.txt", "r") as f:
    content = f.read()
    print("=== read() ===")
    print(repr(content))  # repr shows \n characters
    print()

# Method 2: readline() reads one line at a time
with open("team.txt", "r") as f:
    print("=== readline() ===")
    line1 = f.readline()
    line2 = f.readline()
    print(f"Line 1: {line1.strip()}")
    print(f"Line 2: {line2.strip()}")
    print()

# Method 3: readlines() returns a list of all lines
with open("team.txt", "r") as f:
    lines = f.readlines()
    print("=== readlines() ===")
    print(f"Type: {type(lines)}, Count: {len(lines)}")
    print(f"Lines: {lines}")
    print()

# Method 4 (BEST): iterate directly, memory efficient
with open("team.txt", "r") as f:
    print("=== for line in file ===")
    for line_num, line in enumerate(f, 1):
        print(f"  {line_num}: {line.strip()}")

▶ Output

=== read() ===
'Rahul Mahadik - Backend\nNiranjan Raut - Frontend\nViraj Patil - DevOps\nPravin Sharma - Testing\n'

=== readline() ===
Line 1: Rahul Mahadik - Backend
Line 2: Niranjan Raut - Frontend

=== readlines() ===
Type: <class 'list'>, Count: 4
Lines: ['Rahul Mahadik - Backend\n', 'Niranjan Raut - Frontend\n', 'Viraj Patil - DevOps\n', 'Pravin Sharma - Testing\n']

=== for line in file ===
  1: Rahul Mahadik - Backend
  2: Niranjan Raut - Frontend
  3: Viraj Patil - DevOps
  4: Pravin Sharma - Testing

What happened here: read() pulls the whole file into memory at once, which is fine for a small file and a disaster for a gigabyte log. readline() grabs a single line each time you call it. readlines() hands you a list with every line in it. The winner, though, is looping straight over the file with for line in f:. It reads one line at a time, sips memory no matter how big the file is, and it is the way seasoned Python developers do it.

Writing Files

Writing with mode "w" is like wiping a whiteboard clean before you write on it: whatever was there before is gone, and only your new text remains. Suppose two students named Anvi and Aditi just got their test scores and you want to save a small report file. Python gives you three ways to put text into a file.

📄 writing.py: write() and writelines()

# write() writes a string
with open("output.txt", "w") as f:
    f.write("Line 1: Hello\n")
    f.write("Line 2: World\n")
    # write() does NOT add newlines, you must add \n yourself

# writelines() writes a list of strings
lines = ["Anvay: 92\n", "Aviraj: 88\n", "Vinay: 75\n"]
with open("scores.txt", "w") as f:
    f.writelines(lines)  # Does NOT add newlines between items!

# print() to a file adds newlines automatically
with open("report.txt", "w") as f:
    print("Student Report", file=f)
    print("=" * 30, file=f)
    for name, score in [("Anvi", 95), ("Aditi", 88)]:
        print(f"{name}: {score}", file=f)

# Verify
with open("report.txt") as f:
    print(f.read())

▶ Output

Student Report
==============================
Anvi: 95
Aditi: 88

What happened here: Two catches hide in this snippet. First, write() never adds a newline, so if you forget the \n, all your text runs together on one line. Second, writelines() sounds like it should put each item on its own line, but it does not add anything between items either, you have to bake the \n into each string yourself. The print(..., file=f) trick sidesteps both: it adds the newline for you and supports every format you already use with a normal print(). For quick output to a file, it is often the cleanest of the three.

Appending to Files

Appending is like writing in a diary: today’s entry goes below yesterday’s, and you never tear out the earlier pages. In the example below, an app records events as two users named Pravin and Viraj log in, and every new entry lands at the end of the log file.

📄 append.py: add to a file without destroying existing content

# WARNING: "w" mode DESTROYS existing content!
# Use "a" mode to append

# Create initial log
with open("app.log", "w") as f:
    f.write("[2026-06-21 10:00] App started\n")

# Append new entries (file is NOT overwritten)
with open("app.log", "a") as f:
    f.write("[2026-06-21 10:05] User Pravin logged in\n")
    f.write("[2026-06-21 10:12] User Viraj logged in\n")

# Append more
with open("app.log", "a") as f:
    f.write("[2026-06-21 10:30] Processing complete\n")

# Read the full log
with open("app.log") as f:
    print(f.read())

▶ Output

[2026-06-21 10:00] App started
[2026-06-21 10:05] User Pravin logged in
[2026-06-21 10:12] User Viraj logged in
[2026-06-21 10:30] Processing complete

What happened here: Mode "a" opens the file for appending. If the file is not there yet, Python creates it. If it already exists, your text lands at the very end and nothing already in the file is touched. Notice we opened app.log three separate times and each batch of lines simply stacked onto the previous ones. This is exactly how log files grow, one entry at a time, which is why every logger you will ever use opens its file in append mode.

File Modes Cheat Sheet

ModeRead?Write?Creates?Truncates?Cursor
"r"YesNoNo (error)NoStart
"w"NoYesYesYes!Start
"a"NoYesYesNoEnd
"r+"YesYesNo (error)NoStart
"w+"YesYesYesYes!Start
"a+"YesYesYesNoEnd
"x"NoYesYes (error if exists)NoStart

Add "b" for binary mode: "rb", "wb", "ab". Binary mode is for images, PDFs, and other non-text files.

Working with File Paths using pathlib

A file path is just an address, like a postal address for a file: the folder is the street, the file name is the house, and the extension is the pin code. pathlib understands each part instead of treating the whole thing as one long string, and it writes the address correctly for whichever operating system it runs on.

📄 pathlib_example.py: modern path handling

from pathlib import Path

# Create paths (works on Windows, Mac, Linux)
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)  # Create directory if it doesn't exist

file_path = data_dir / "config.txt"  # / operator joins paths!

# Write using pathlib
file_path.write_text("app_name=TechnoScripts\nversion=2.0\ndebug=false\n")

# Read using pathlib
content = file_path.read_text()
print(f"Content:\n{content}")

# Useful path operations
print(f"Name: {file_path.name}")
print(f"Stem: {file_path.stem}")
print(f"Suffix: {file_path.suffix}")
print(f"Parent: {file_path.parent}")
print(f"Exists: {file_path.exists()}")
print(f"Is file: {file_path.is_file()}")
print(f"Absolute: {file_path.resolve()}")

▶ Output

Content:
app_name=TechnoScripts
version=2.0
debug=false

Name: config.txt
Stem: config
Suffix: .txt
Parent: data
Exists: True
Is file: True
Absolute: /home/user/project/data/config.txt

What happened here: pathlib.Path is the modern replacement for the older os.path functions. The headline feature is that / operator: data_dir / "config.txt" joins paths in a way that works the same on Windows, Mac, and Linux, so you can stop typing os.path.join(). The write_text() and read_text() shortcuts open and close the file for you in a single call, perfect for quick one-shot reads and writes. One thing to expect: the last line, the absolute path from resolve(), will look different on your machine because it spells out your real folder. On Windows it might start with C:\Users\... instead. That part is meant to vary, the rest of the output stays the same.

Real-World Pattern: Log File Processor

This pattern works like straining tea: pour everything through the strainer and keep only what you want in the cup. Here we pour a server log through a filter and keep only the error lines.

📄 log_processor.py: read, process, write

# Create a sample log file
log_data = """[ERROR] 2026-06-21 10:05:23 - Database connection failed
[INFO] 2026-06-21 10:05:25 - Retrying connection...
[INFO] 2026-06-21 10:05:26 - Connected to database
[WARNING] 2026-06-21 10:10:45 - Slow query detected (3.2s)
[ERROR] 2026-06-21 10:15:00 - API timeout: /users endpoint
[INFO] 2026-06-21 10:15:01 - Request retried successfully
[ERROR] 2026-06-21 10:20:33 - File not found: config.yaml
"""

with open("app_server.log", "w") as f:
    f.write(log_data)

# Process: extract only errors
errors = []
with open("app_server.log") as f:
    for line in f:
        if line.startswith("[ERROR]"):
            errors.append(line.strip())

# Write error report
with open("error_report.txt", "w") as f:
    f.write(f"Error Report: {len(errors)} errors found\n")
    f.write("=" * 50 + "\n\n")
    for i, error in enumerate(errors, 1):
        f.write(f"{i}. {error}\n")

# Display the report
with open("error_report.txt") as f:
    print(f.read())

▶ Output

Error Report: 3 errors found
==================================================

1. [ERROR] 2026-06-21 10:05:23 - Database connection failed
2. [ERROR] 2026-06-21 10:15:00 - API timeout: /users endpoint
3. [ERROR] 2026-06-21 10:20:33 - File not found: config.yaml

What happened here: This is the read, process, write loop you will reach for again and again on the job. We opened the raw log and looped over it line by line, kept only the lines that start with [ERROR], and wrote a tidy report to a brand new file. The original log was never modified, we only read from it. That separation is the safe pattern: read your source, build the result in memory or in a separate file, and never overwrite the thing you are still reading. Swap the keyword and you have a tool that pulls warnings, or payments, or any line you care about.

Encoding: Why UTF-8 Matters

📄 encoding.py: always specify encoding

# Write with explicit encoding (best practice)
with open("greetings.txt", "w", encoding="utf-8") as f:
    f.write("Hello! 👋\n")
    f.write("नमस्ते! 🙏\n")    # Hindi
    f.write("こんにちは! 🎌\n")  # Japanese

# Read with same encoding
with open("greetings.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())

▶ Output

Hello! 👋
नमस्ते! 🙏
こんにちは! 🎌

What happened here: Always pass encoding="utf-8" when you open a text file. Leave it out and Python falls back to your system default, which is not the same everywhere. On Linux and Mac that default is usually UTF-8 (Unicode Transformation Format), so things just work. On Windows it is often cp1252, a small Western-European set that has no idea what an emoji or a Devanagari letter is, and your read or write blows up with a UnicodeEncodeError.

Think of encoding like the language a document is written in: save it in one language and read it back in another, and you get garbage. UTF-8 is the one encoding that speaks every language, so make it your default and you never think about this again.

Windows console note: the file above is written and read back correctly because we passed encoding="utf-8" to both open() calls. The output shown is what you see in a UTF-8 terminal such as VS Code, the macOS Terminal, or Linux. The default Windows console can still choke when print() tries to put an emoji on screen, raising UnicodeEncodeError at the print line, not at the file read. Python 3.15 is set to make UTF-8 mode the default (PEP 686, a Python Enhancement Proposal); until then, run your script with py -X utf8 your_script.py or set the environment variable PYTHONUTF8=1 and the emoji print correctly on Windows too.

Common Mistakes

Mistake 1: Using “w” when you meant “a”

Mode "w" destroys existing content. If you want to add to a file, use "a" (append). This is the single most common file handling bug.

Mistake 2: Not using with statement

Always use with open(...) as f:. Manual open()/close() risks file handle leaks when exceptions occur.

Mistake 3: Reading a huge file with read()

📄 mistake_memory.py

# BAD: loads the entire file into memory
# with open("huge_file.log") as f:
#     data = f.read()  # 5GB file means 5GB in RAM!

# GOOD: process line by line
# with open("huge_file.log") as f:
#     for line in f:  # One line in memory at a time
#         process(line)

Best Practices

  • DO always use with open() as f: so the file closes itself, never manual open/close
  • DO always specify encoding="utf-8" for text files
  • DO iterate over files line by line for large files
  • DO use pathlib.Path for path operations instead of string concatenation
  • DON’T use "w" mode unless you intentionally want to overwrite
  • DON’T call read() on files that could be large, iterate line by line instead

Conclusion

Python file handling is how your programs hold on to data after they finish running. Lock in a few habits and you are set: always open files with the with statement so they close themselves, always pass encoding="utf-8", and pick the right mode on purpose, "r" to read, "w" to overwrite, "a" to append. Reach for pathlib.Path when you build paths, and loop line by line when a file might be large. These same fundamentals carry straight into every file format you will meet, including the structured CSV (Comma-Separated Values) and JSON (JavaScript Object Notation) files we tackle next.

Next up: Working with CSV and JSON Files, the structured data formats that power real applications. And if you want to revisit earlier topics or see everything this series covers, browse the full index at the Python + AI/ML tutorial series home.

Practice Exercises

  1. Exercise 1: Create a 5-line file, then read and print each line.
  2. Exercise 2: Read a CSV and calculate average of a numeric column.
  3. Exercise 3: Build a log analyzer counting entries by severity.

Frequently Asked Questions

How do I read a file in Python?

Python file handling starts with open(). Use with open('file.txt', 'r', encoding='utf-8') as f: then either f.read() for the entire file, or iterate with for line in f: for line-by-line reading (recommended for large files).

What is the difference between write and append mode?

Mode 'w' overwrites the file, so existing content is deleted. Mode 'a' appends to the end, so existing content is preserved. Use 'a' for log files and 'w' when you want a fresh file.

Why should I use the with statement for files?

The with statement guarantees the file is closed when the block ends, even if an exception occurs. Without it, a crash between open() and close() leaves the file handle leaked.

What encoding should I use for text files?

Always use encoding='utf-8'. It handles all languages, emoji, and special characters. Without specifying encoding, Python uses the system default, which varies by OS and can cause encoding errors.

How do I read a large file without running out of memory?

Iterate line by line: for line in f: reads one line at a time, using minimal memory regardless of file size. Avoid f.read() or f.readlines() which load the entire file into memory.

What is the difference between pathlib and os.path?

pathlib.Path is the modern, object-oriented approach to file paths. It uses the / operator for joining paths, has methods like .read_text() and .exists(), and is more readable than os.path.join() chains.

Interview Questions on Python File Handling

Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.

Q: What happens if you open a file in “r” mode and the file does not exist?

Python raises FileNotFoundError immediately at the open() call, because read mode never creates a file. In real code you either wrap the call in try/except FileNotFoundError or check first with Path("file.txt").exists() from pathlib. Modes "w" and "a" do not have this problem since they create the file when it is missing.

Q: Your script writes a report file, but when the process is killed midway, the file on disk is sometimes empty or missing the last lines. What is going on?

Python buffers writes in memory and only pushes them to disk when the buffer fills up or the file is closed. If the process dies before the close happens, whatever was sitting in the buffer is lost. Using with open(...) guarantees the flush and close on both normal exits and exceptions, and for critical data you can call f.flush() after each write. A hard kill such as a power cut can still lose the final buffer, which is why databases go further with special sync calls.

Q: You open a config file with open("config.txt", "w+") planning to read it first and then update it, but f.read() returns an empty string. Why?

Mode "w+" truncates the file the moment it opens, so by the time read() runs the content is already gone. If you need to read and then write the same file without wiping it, "r+" is the mode that opens with the content intact. Even simpler: read the file fully in one with block, build the new content, then write it out in a second with block using "w".

Q: When would you use mode “x” instead of “w”?

Mode "x" is exclusive creation: it creates the file only if it does not already exist, and raises FileExistsError otherwise. That makes it the safer choice whenever overwriting would be a bug, for example generating one output file per job run where a duplicate name means something went wrong. "w" would silently wipe the existing file, while "x" turns the same mistake into a loud, catchable error.

Q: What is the difference between f.write() and print(..., file=f)?

f.write() takes exactly one string and adds nothing to it, so you must append \n yourself and convert numbers with str() first. print(..., file=f) behaves like a normal print: it converts every argument to a string, joins them with spaces, and adds a newline at the end. For quick human-readable output, print to a file is often cleaner, while write() gives you exact control over every character.

Q: A file object has a cursor. What do tell() and seek() do with it?

The cursor is the position where the next read or write happens. f.tell() reports the current position, and f.seek(0) moves it back to the start, which lets you read the same file twice inside one open(). This is also why file modes matter: "r" and "w" start the cursor at position 0, while "a" starts it at the end so new writes cannot damage existing content.

Reference: the complete, always-current details live in the official Python documentation.

Previous: Python: Higher-Order Functions, map(), filter(), reduce()

Next: Python: Working with CSV and JSON Files

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *