
Python Course
File Handling in Python
Every variable in your program disappears the moment the program stops. If you want data to survive — to still be there tomorrow, next week, or when a different user opens the app — you need to save it to a file. File handling is how Python reads from and writes to files on your computer's disk.
Almost every real program uses files. Web apps store user data. Automation scripts write log files. Data pipelines read CSV files. Configuration files store app settings. Machine learning models are saved to disk and loaded back. Understanding file handling well is a core skill for any Python developer.
File Modes
When you open a file, you tell Python what you want to do with it using a mode — the second argument to open(). Choosing the wrong mode is one of the most common mistakes. Learn these modes before writing any file code.
| Mode | Meaning | What Happens |
|---|---|---|
"r" | Read | Opens existing file to read. Error if file does not exist. |
"w" | Write | Creates file. Erases existing content if file already exists. |
"a" | Append | Adds to end. Creates file if it does not exist. |
"x" | Create | Creates new file. Error if file already exists. |
"r+" | Read + Write | Opens existing file for both reading and writing. |
"rb" / "wb" | Binary | Read or write raw bytes — for images, PDFs, executables. |
Writing to a File — The with Statement
The safest way to work with any file is the with statement. It automatically closes the file when the block ends — even if an error occurs inside. You never need to call file.close() manually. If you forget to close a file, it can remain locked or data can be lost.
# "w" creates the file — overwrites if it already exists
with open("notes.txt", "w") as file:
file.write("Python File Handling\n") # \n = new line character
file.write("Lesson 19\n")
file.write("Dataplexa.com\n")
print("File written successfully.")
# Write multiple lines at once using writelines()
lines = ["First line\n", "Second line\n", "Third line\n"]
with open("lines.txt", "w") as file:
file.writelines(lines) # no separator added — each string needs its own \n
print("Multiple lines written.")
file.write()does not add a newline automatically — you must include\nat the end of each line.writelines()writes every string in a list without adding any separators — each string must already end with\nif you want separate lines.- The
withblock callsfile.__exit__()automatically when it ends — this flushes the buffer and closes the file handle even if an exception occurs mid-write.
Reading a File
Python gives you three ways to read a file. Choose based on how much of the file you need and how large it is.
# Method 1: read() — entire file as one string
with open("notes.txt", "r") as file:
content = file.read()
print("--- read() ---")
print(content)
# Method 2: readline() — one line per call
with open("notes.txt", "r") as file:
print("--- readline() ---")
print(file.readline().strip()) # first line
print(file.readline().strip()) # second line
# Method 3: readlines() — all lines as a list
with open("notes.txt", "r") as file:
lines = file.readlines()
print("--- readlines() ---")
print(lines)
# Strip newlines when processing
clean = [line.strip() for line in lines]
print("Cleaned:", clean)
read()loads the entire file into memory at once — fine for small files, but avoid on large files (gigabytes).readline()moves an internal cursor — each call reads the next line. The cursor remembers where it left off.readlines()returns a list including the\n— always use.strip()or a list comprehension to clean them before processing.
Looping Through a File Line by Line
The most memory-efficient way to process a large file is to loop through it directly. Python reads one line at a time, so a 10GB log file uses no more memory than a 1KB file.
# Efficient — reads one line at a time, no matter how big the file
with open("notes.txt", "r") as file:
for line_num, line in enumerate(file, start=1):
clean = line.strip()
print(f"Line {line_num}: {clean}")
# Practical: count lines, words, and characters in a file
line_count = 0
word_count = 0
char_count = 0
with open("notes.txt", "r") as file:
for line in file:
line_count += 1
word_count += len(line.split())
char_count += len(line.strip())
print(f"Lines: {line_count} | Words: {word_count} | Chars: {char_count}")
enumerate(file, start=1)gives both the line number and the line content — clean and readable.- The word count / char count pattern is a real Unix-style
wccommand implemented in Python.
Appending to a File
Mode "a" opens a file and adds content to the end without touching anything already there. This is used for log files, audit trails, and any record that grows over time.
from datetime import datetime
def write_log(message):
"""Appends a timestamped message to the log file."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open("app.log", "a") as log:
log.write(f"[{timestamp}] {message}\n")
# Simulate application events
write_log("Application started")
write_log("User 'alex' logged in")
write_log("File 'report.csv' uploaded")
write_log("User 'alex' logged out")
write_log("Application stopped")
# Read the full log
with open("app.log", "r") as log:
print(log.read())
- The
write_log()function is a reusable logging helper — this is the pattern used in real applications before they switch to Python's built-inloggingmodule. - Each call opens the file, appends one line, and closes it — safe for concurrent access in simple scripts.
Encoding — Handling Non-English Text
Files store raw bytes, not text. When Python reads a file, it must know which encoding to use to convert those bytes into characters. The default is often system-dependent, which causes bugs when files contain non-ASCII characters (accents, Hindi, Japanese, etc.). Always specify encoding="utf-8" explicitly — UTF-8 handles virtually every language on earth.
# Always specify encoding — especially for non-English content
data = "Hello World\nBonjour le monde\nHola Mundo\nनमस्ते दुनिया\n"
# Write with UTF-8 encoding
with open("multilingual.txt", "w", encoding="utf-8") as file:
file.write(data)
# Read back with the same encoding
with open("multilingual.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
# What happens if encoding is wrong:
# UnicodeDecodeError: 'charmap' codec can't decode byte 0x81...
# This is why always specifying encoding="utf-8" is best practice
print("File size:", end=" ")
import os
print(os.path.getsize("multilingual.txt"), "bytes")
- UTF-8 encodes ASCII characters in 1 byte, and non-ASCII (like Hindi or Chinese) in 2–4 bytes — it is backward-compatible and universal.
- The
UnicodeDecodeErroris one of the most common file handling errors in real programs — specifyingencoding="utf-8"prevents it. - When reading files from external sources (emails, web scrapes, CSVs from other systems), the encoding may not be UTF-8 — common alternatives are
"latin-1"and"cp1252".
Checking if a File Exists Before Opening
Opening a non-existent file in "r" mode raises a FileNotFoundError. Always check first, or use exception handling. Both approaches are professional — choose based on what you need to do in each case.
import os
filename = "config.txt"
# Approach 1: check before opening (LBYL — Look Before You Leap)
if os.path.exists(filename):
with open(filename, "r") as f:
print(f.read())
else:
print(f"'{filename}' not found. Creating defaults...")
with open(filename, "w") as f:
f.write("theme=dark\nlanguage=en\ntimeout=30\n")
# Approach 2: try/except (EAFP — Easier to Ask Forgiveness than Permission)
# This is the more Pythonic style
try:
with open("data.txt", "r") as f:
content = f.read()
print("Read", len(content), "characters")
except FileNotFoundError:
print("data.txt not found — skipping")
except PermissionError:
print("data.txt exists but you do not have permission to read it")
- LBYL (
os.path.exists()) is clear and readable — good when you need to do different things based on whether the file exists. - EAFP (
try/except) is the Pythonic style — it handles multiple error types cleanly and avoids a race condition (where the file could be deleted between the check and the open). - Always handle both
FileNotFoundErrorandPermissionErrorin production code — users on locked-down systems encounter permission errors regularly.
File Position — seek() and tell()
When you read from a file, Python tracks your position with an internal cursor. tell() returns the current byte position. seek() moves the cursor to any position. This lets you re-read sections of a file without closing and reopening it.
with open("notes.txt", "r") as file:
# Read the first line
first = file.readline()
print("First line:", first.strip())
print("Cursor at byte:", file.tell())
# Read the second line
second = file.readline()
print("Second line:", second.strip())
print("Cursor at byte:", file.tell())
# Go back to the very beginning (byte 0)
file.seek(0)
print("After seek(0), cursor at:", file.tell())
# Read from the beginning again
all_content = file.read()
print("Re-read entire file:")
print(all_content)
file.seek(0)rewinds the file to the beginning — useful when you need to read a file multiple times in the samewithblock.file.seek(0, 2)seeks to the end of the file — combine withtell()to get the file size without reading all of it.- In text mode, only
seek(0)and positions returned bytell()are reliable — do not calculate byte positions manually in text files.
Working with CSV Files
CSV (Comma-Separated Values) is the most common format for sharing tabular data. Python's built-in csv module handles the quoting and escaping rules correctly — do not parse CSV manually with split(",") because values can contain commas inside quotes.
import csv
# Write a CSV with csv.writer
employees = [
["Name", "Department", "Salary"],
["Alice", "Engineering", 95000 ],
["Bob", "Marketing", 72000 ],
["Carol", "Design", 80000 ],
["David", "Engineering", 105000 ]
]
with open("employees.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerows(employees) # writes header + all rows
print("CSV written.")
# Read with csv.reader — each row comes back as a list of strings
with open("employees.csv", "r", encoding="utf-8") as file:
reader = csv.reader(file)
header = next(reader) # skip the header row
print("Columns:", header)
for row in reader:
name, dept, salary = row
print(f" {name:<8} | {dept:<14} | Rs.{int(salary):,}")
newline=""prevents blank lines between rows on Windows — always include it when writing CSV files.next(reader)advances past the header row so your loop only processes data rows.- All values from CSV come back as strings — cast with
int()orfloat()before doing arithmetic.
CSV with DictReader and DictWriter
DictReader and DictWriter let you work with CSV rows as dictionaries — row["salary"] instead of row[2]. This is far more readable with real files that have many columns.
import csv
# Write with DictWriter — column names defined upfront
products = [
{"name": "Laptop", "price": 1200, "stock": 15},
{"name": "Monitor", "price": 450, "stock": 30},
{"name": "Keyboard", "price": 80, "stock": 100},
{"name": "Webcam", "price": 120, "stock": 50}
]
with open("products.csv", "w", newline="", encoding="utf-8") as file:
fields = ["name", "price", "stock"]
writer = csv.DictWriter(file, fieldnames=fields)
writer.writeheader() # writes the column name row
writer.writerows(products)
# Read with DictReader — each row is a dict
total_value = 0
with open("products.csv", "r", encoding="utf-8") as file:
reader = csv.DictReader(file)
print(f"{'Product':<12} {'Price':>8} {'Stock':>7} {'Value':>10}")
print("-" * 42)
for row in reader:
price = float(row["price"])
stock = int(row["stock"])
value = price * stock
total_value += value
print(f"{row['name']:<12} {price:>8.2f} {stock:>7} {value:>10.2f}")
print("-" * 42)
print(f"{'Total inventory value':>30}: {total_value:>10.2f}")
DictWriterwrites each dictionary as a row —writeheader()must be called first to write the column names.DictReadermakes long CSV files much easier to work with — column names document themselves in the code.- The inventory value calculation (price × stock) is a standard data processing task done on CSV data daily in business reporting.
The pathlib Module — Modern File Paths
pathlib is a modern, object-oriented way to work with file paths. Instead of building paths as strings with os.path.join(), you use Path objects that support intuitive operators and methods. It is the recommended approach in Python 3.4+.
from pathlib import Path
# Create a Path object
p = Path("reports") / "2025" / "sales.csv"
print("Path :", p)
print("Name :", p.name) # sales.csv
print("Stem :", p.stem) # sales (no extension)
print("Suffix:", p.suffix) # .csv
print("Parent:", p.parent) # reports/2025
print("Parts :", p.parts)
# Check if a file exists
config = Path("config.txt")
print("Exists:", config.exists())
# Read a file directly from a Path object
config = Path("notes.txt")
if config.exists():
text = config.read_text(encoding="utf-8")
print("Content:", text.strip())
# Write a file directly
output = Path("output.txt")
output.write_text("Written with pathlib!\n", encoding="utf-8")
print("Written:", output.read_text(encoding="utf-8"))
- The
/operator joins path components — more readable thanos.path.join("reports", "2025", "sales.csv"). Path.read_text()andPath.write_text()are convenient shortcuts for small files — internally they use the sameopen()mechanism.p.stemgives the filename without extension — useful for renaming or creating derived filenames likesales_processed.csv.
Safe File Operations — Copying and Deleting
Beyond reading and writing, real programs frequently need to copy, move, rename, and delete files. Python's shutil module handles these operations safely.
import shutil
import os
from pathlib import Path
# Copy a file
shutil.copy("notes.txt", "notes_backup.txt")
print("Backup created:", Path("notes_backup.txt").exists())
# Rename (or move) a file
os.rename("notes_backup.txt", "backup_v1.txt")
print("Renamed:", Path("backup_v1.txt").exists())
# Get file size
size = os.path.getsize("notes.txt")
print("File size:", size, "bytes")
# Delete a file safely
if os.path.exists("backup_v1.txt"):
os.remove("backup_v1.txt")
print("Deleted backup_v1.txt")
# Create a directory
Path("archive/2025").mkdir(parents=True, exist_ok=True)
print("Directory created:", Path("archive/2025").is_dir())
shutil.copy()copies a file —shutil.move()moves it (cuts and pastes to a new location).- Always check
os.path.exists()before deleting —os.remove()raisesFileNotFoundErrorif the file is gone. Path.mkdir(parents=True, exist_ok=True)creates the full directory tree —parents=Truecreates intermediate folders,exist_ok=Truedoes not error if the folder already exists.
Real World Example — Log File Analyser
This program writes a realistic application log, then reads it back to analyse activity — counting events by type, finding errors, and summarising the session. This is the kind of script a DevOps engineer or backend developer writes regularly.
import csv
from datetime import datetime, timedelta
from collections import Counter
# Step 1: Create a realistic log file
log_entries = [
{"time": "08:01", "level": "INFO", "message": "Server started"},
{"time": "08:05", "level": "INFO", "message": "User alice logged in"},
{"time": "08:12", "level": "INFO", "message": "File report.csv uploaded"},
{"time": "08:15", "level": "WARNING", "message": "Disk usage at 80%"},
{"time": "08:22", "level": "ERROR", "message": "Database connection timeout"},
{"time": "08:23", "level": "INFO", "message": "Database reconnected"},
{"time": "08:30", "level": "INFO", "message": "User bob logged in"},
{"time": "08:45", "level": "ERROR", "message": "Failed to send email: SMTP error"},
{"time": "08:50", "level": "INFO", "message": "User alice logged out"},
{"time": "09:00", "level": "WARNING", "message": "High memory usage detected"},
{"time": "09:05", "level": "INFO", "message": "Backup completed successfully"},
{"time": "09:10", "level": "INFO", "message": "Server shutdown"}
]
with open("server.log", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["time", "level", "message"])
writer.writeheader()
writer.writerows(log_entries)
# Step 2: Analyse the log
level_counts = Counter()
errors = []
warnings = []
with open("server.log", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
level_counts[row["level"]] += 1
if row["level"] == "ERROR":
errors.append(f" [{row['time']}] {row['message']}")
elif row["level"] == "WARNING":
warnings.append(f" [{row['time']}] {row['message']}")
# Step 3: Print the summary report
print("=" * 45)
print(" SERVER LOG ANALYSIS REPORT")
print("=" * 45)
print(f"Total log entries : {sum(level_counts.values())}")
for level, count in sorted(level_counts.items()):
print(f" {level:<10}: {count}")
if errors:
print("\nERRORS found:")
for e in errors:
print(e)
if warnings:
print("\nWARNINGS found:")
for w in warnings:
print(w)
print("=" * 45)
- The log is written as a structured CSV — far easier to parse than free-form text logs.
Counterfromcollections(Lesson 18) counts log levels in one line.- This three-step pattern — write → read → analyse — is used in monitoring systems, CI pipelines, and support tools everywhere.
Quick Reference Table
| Concept | Syntax | What It Does |
|---|---|---|
| Open safely | with open("f.txt", "r") as f: | Auto-closes file when done |
| Write text | f.write("text\n") | Writes a string to the file |
| Write list | f.writelines(["a\n","b\n"]) | Writes all strings in a list |
| Read all | f.read() | Returns entire file as a string |
| Read one line | f.readline() | Reads next line each call |
| Read to list | f.readlines() | Returns list of all lines (with \n) |
| Loop lines | for line in f: | Memory-efficient iteration |
| Append mode | open("f.txt", "a") | Adds to end without overwriting |
| Encoding | encoding="utf-8" | Handle all languages correctly |
| Cursor position | f.tell() / f.seek(0) | Get/set file cursor position |
| File exists | os.path.exists(name) | Returns True if file exists |
| Modern paths | from pathlib import Path | OOP path handling |
| Copy file | shutil.copy(src, dst) | Copy file to new location |
| CSV write | csv.writer / DictWriter | Write rows to CSV |
| CSV read | csv.reader / DictReader | Read rows from CSV |
Practice
Which file mode creates a new file and overwrites existing content?
Which file mode adds content to the end of a file without deleting anything?
What keyword opens a file safely so it closes automatically even if an error occurs?
Which string method removes the trailing \n newline from a line read from a file?
Which CSV class reads each row as a dictionary with column names as keys?
What encoding should you always specify when writing files containing non-English text?
Quick Quiz
What error is raised when you open a non-existent file in "r" mode?
What happens to an existing file when you open it with mode "w"?
Which method returns all lines of a file as a Python list?
What is the most memory-efficient way to read a very large file?
Which method moves the file cursor back to the beginning of the file?
What argument prevents blank lines between CSV rows on Windows?