File Handling in Python | Python Course | Dataplexa

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.

ModeMeaningWhat Happens
"r"ReadOpens existing file to read. Error if file does not exist.
"w"WriteCreates file. Erases existing content if file already exists.
"a"AppendAdds to end. Creates file if it does not exist.
"x"CreateCreates new file. Error if file already exists.
"r+"Read + WriteOpens existing file for both reading and writing.
"rb" / "wb"BinaryRead 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 written successfully. Multiple lines written.
  • file.write() does not add a newline automatically — you must include \n at the end of each line.
  • writelines() writes every string in a list without adding any separators — each string must already end with \n if you want separate lines.
  • The with block calls file.__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() --- Python File Handling Lesson 19 Dataplexa.com --- readline() --- Python File Handling Lesson 19 --- readlines() --- ['Python File Handling\n', 'Lesson 19\n', 'Dataplexa.com\n'] Cleaned: ['Python File Handling', 'Lesson 19', 'Dataplexa.com']
  • 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}")
Line 1: Python File Handling Line 2: Lesson 19 Line 3: Dataplexa.com Lines: 3 | Words: 6 | Chars: 37
  • 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 wc command 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())
[2025-06-15 14:22:05] Application started [2025-06-15 14:22:05] User 'alex' logged in [2025-06-15 14:22:05] File 'report.csv' uploaded [2025-06-15 14:22:05] User 'alex' logged out [2025-06-15 14:22:05] Application stopped
  • The write_log() function is a reusable logging helper — this is the pattern used in real applications before they switch to Python's built-in logging module.
  • 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")
Hello World Bonjour le monde Hola Mundo नमस्ते दुनिया File size: 69 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 UnicodeDecodeError is one of the most common file handling errors in real programs — specifying encoding="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")
'config.txt' not found. Creating defaults... data.txt not found — skipping
  • 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 FileNotFoundError and PermissionError in 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)
First line: Python File Handling Cursor at byte: 21 Second line: Lesson 19 Cursor at byte: 31 After seek(0), cursor at: 0 Re-read entire file: Python File Handling Lesson 19 Dataplexa.com
  • file.seek(0) rewinds the file to the beginning — useful when you need to read a file multiple times in the same with block.
  • file.seek(0, 2) seeks to the end of the file — combine with tell() to get the file size without reading all of it.
  • In text mode, only seek(0) and positions returned by tell() 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):,}")
CSV written. Columns: ['Name', 'Department', 'Salary'] Alice | Engineering | Rs.95,000 Bob | Marketing | Rs.72,000 Carol | Design | Rs.80,000 David | Engineering | Rs.1,05,000
  • 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() or float() 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}")
Product Price Stock Value ------------------------------------------ Laptop 1200.00 15 18000.00 Monitor 450.00 30 13500.00 Keyboard 80.00 100 8000.00 Webcam 120.00 50 6000.00 ------------------------------------------ Total inventory value: 45500.00
  • DictWriter writes each dictionary as a row — writeheader() must be called first to write the column names.
  • DictReader makes 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"))
Path : reports/2025/sales.csv Name : sales.csv Stem : sales Suffix: .csv Parent: reports/2025 Parts : ('reports', '2025', 'sales.csv') Exists: True Content: Python File Handling Lesson 19 Dataplexa.com Written: Written with pathlib!
  • The / operator joins path components — more readable than os.path.join("reports", "2025", "sales.csv").
  • Path.read_text() and Path.write_text() are convenient shortcuts for small files — internally they use the same open() mechanism.
  • p.stem gives the filename without extension — useful for renaming or creating derived filenames like sales_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())
Backup created: True Renamed: True File size: 43 bytes Deleted backup_v1.txt Directory created: True
  • 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() raises FileNotFoundError if the file is gone.
  • Path.mkdir(parents=True, exist_ok=True) creates the full directory tree — parents=True creates intermediate folders, exist_ok=True does 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)
============================================= SERVER LOG ANALYSIS REPORT ============================================= Total log entries : 12 ERROR : 2 INFO : 8 WARNING : 2 ERRORS found: [08:22] Database connection timeout [08:45] Failed to send email: SMTP error WARNINGS found: [08:15] Disk usage at 80% [09:00] High memory usage detected =============================================
  • The log is written as a structured CSV — far easier to parse than free-form text logs.
  • Counter from collections (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

ConceptSyntaxWhat It Does
Open safelywith open("f.txt", "r") as f:Auto-closes file when done
Write textf.write("text\n")Writes a string to the file
Write listf.writelines(["a\n","b\n"])Writes all strings in a list
Read allf.read()Returns entire file as a string
Read one linef.readline()Reads next line each call
Read to listf.readlines()Returns list of all lines (with \n)
Loop linesfor line in f:Memory-efficient iteration
Append modeopen("f.txt", "a")Adds to end without overwriting
Encodingencoding="utf-8"Handle all languages correctly
Cursor positionf.tell() / f.seek(0)Get/set file cursor position
File existsos.path.exists(name)Returns True if file exists
Modern pathsfrom pathlib import PathOOP path handling
Copy fileshutil.copy(src, dst)Copy file to new location
CSV writecsv.writer / DictWriterWrite rows to CSV
CSV readcsv.reader / DictReaderRead 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?





NEXT UP
Exception Handling in Python
Learn how to handle errors gracefully with try/except/finally — preventing crashes and writing programs that recover cleanly from unexpected situations.