Insights Videos Blog Learning
PYTHON

Part 5: File Handling & Persistence

Give your Python scripts memory. Learn how to read, write, and append to files so your automation can remember what happened, even after the program ends.

In Part 4, we learned how to break up our code into reusable pieces with functions and how to defend it with try-except blocks. But all that logic still vanished the moment our script stopped running.

That changes now. In this part, your Python code will start to "remember", not just react. You'll learn how to:

  • Read from and write to files using open(), read(), readline(), readlines(), write(), and close()
  • Understand the difference between 'r', 'w', and 'a' modes
  • Avoid common bugs like forgetting to close files or accidentally overwriting data
  • Use with open() to safely manage file access

This is a major leap in automation power - giving your scripts long-term memory so they can log activity, store inputs, and resume where they left off. Whether you're building an agent-assist tool, keeping logs of escalations, or storing intermediate outputs, this is how your code graduates from temporary to persistent.


Try it out as you learn:
  • Click the menu icon (top-left of the editor below) to:
    • Reset to start over with fresh code
    • Download your script to reuse it later
    • Go fullscreen if you want more room to code

Intro & Recap

You’ve already built scripts that route calls using lists, store customer profiles in dictionaries, lock in metadata with tuples, and build resilient, modular logic using functions and error handling.

But there’s one thing your code still can’t do: remember. As soon as the program ends, all your carefully stored data, customer IDs, ticket reasons, even escalation logs, vanishes into thin air.

In the real world, that's a deal-breaker. Contact centers, bots, and CX tools need a memory. They must be able to log activity, retrieve past inputs, and store user history across sessions.

That’s where persistence comes in, the ability to save and load data from files.

This part teaches you how to:

  • Read from and write to files using open(), read(), and write()
  • Safely manage file operations using with open(), a Python best practice
  • Store logs, notes, and data across sessions using plain text files
  • Work with structured formats like JSON and CSV, the building blocks of modern automation and data exchange
Note: Your business works flows will need context, for example you won't want your bots to forget, and your codes shouldn’t either. Persistence is the foundation for logging, auditing, analytics, and external integration.

Once you know how to work with files, you're no longer building one-off scripts. You're building systems that remember, improve, and integrate. Let’s get into it.

What is File I/O in Python?

File I/O stands for Input and Output, reading from and writing to files. In the CX world, this might mean:

  • Reading an escalation log to understand what happened last shift
  • Writing a customer note during a live chat for later handoff
  • Appending a new ticket to a system log — without overwriting the old ones

In Python, this is done using the built-in open() function, like so:

file = open("notes.txt", "r")  # Open for reading
contents = file.read()
print(contents)
file.close()

Notice the flow: you open() the file, do your read() or write() operations, and then explicitly close the file.

Warning: If you forget to close() the file, you could lose unsaved data, leave system resources locked, or cause strange bugs, especially if multiple users or processes are involved.

Here’s a quick guide to the most common modes you’ll use with open():

Mode What It Does
"r" Read (default mode). File must exist.
"w" Write. Overwrites the file if it exists.
"a" Append. Adds to the end of the file if it exists.
"x" Create a new file. Fails if file already exists.
"b" Binary mode (e.g. "rb" for reading images/files)
Tip: You can combine modes like "w" and "b" to write binary data, or "a+" to append and read.

Let us now go ahead and use this knowledge to build a simple log viewer. Later, we’ll show a safer, more modern way to handle files with with open(), but this classic pattern is important to understand first.

Reading from a File

Let’s say you want to review escalation logs from earlier this week. If your Python script can read text files, it can summarize escalations, count issue types, or surface recurring customer pain points.

Let’s read from a file called escalations.txt using the manual approach.

try:
    file = open("escalations.txt", "r")
    contents = file.read()
    print(contents)
    file.close()
except FileNotFoundError:
    print("Log file not found. Try running the script that creates it first.")

That’s the full process: open the file in read mode ("r"), read it, then remember to close it when you’re done.

Other Ways to Read

Sometimes you don’t want the whole file at once. You want to process it line-by-line. Here are a few options:

# Read just the first line
file = open("escalations.txt", "r")
print(file.readline())
file.close()
# Read all lines as a list
file = open("escalations.txt", "r")
lines = file.readlines()
for line in lines:
    print(line)
file.close()
# Loop through file directly
file = open("escalations.txt", "r")
for line in file:
    print(line)
file.close()
Tip: Most lines end with a \n newline character. Use .strip() to remove them before displaying or processing: print(line.strip())

Right now you’re opening and closing files manually. That’s okay, but fragile. If your script crashes before file.close(), the file stays open!

That’s why in the next section, we’ll introduce a better, safer way: with open().

Writing & Appending to a File

Just like reading helps you review old interactions, writing lets you create new records, like logging an escalation, saving a chat transcript, or outputting a customer summary to a file.

Here’s how to write to a new file. If the file exists, it’ll be replaced.

file = open("escalations.txt", "w")
file.write("Customer ID: 2392 | Reason: Escalated due to system outage\n")
file.close()

That wrote one line to the file. If you run it again, it’ll overwrite the previous contents.

Note: The "w" mode erases existing contents. Don’t use it unless you intend to start fresh.

To append a new line to an existing file instead, use mode "a":

file = open("escalations.txt", "a")
file.write("Customer ID: 7154 | Reason: Escalated for billing confusion\n")
file.close()

That adds the new line without wiping out what was already there. Very useful for ongoing logs or journals.

Quick Review of Modes

  • "w" — write (overwrite mode)
  • "a" — append (add to end of file)
Tip: A log file is just a smart text file. Append one line per event, and it becomes your workflow's memory or an audit trail.

Next up, we’ll introduce with open() so you don’t have to remember to call close() every time.

Using with open() — The Safe Way

Remember how we had to manually call file.close() every time? That works, but it’s risky. What if your script crashes before it gets to close()? The file might stay locked, or worse, your data might not be saved.

Note: If you forget close() or your script errors out, you risk corrupting files or leaving them in a half-written state.

That’s why most production Python code uses the with open() pattern, also called a context manager. It opens the file, lets you work inside a block, and automatically closes it afterward, even if an error happens.

Analogy: It’s like booking a meeting room. You use it for a set time, and the lights turn off and the doors lock when you leave, no intervention required.

Rewriting Our Previous Example

Here’s how to write to a file the safer way:

with open("escalations.txt", "a") as file:
    file.write("Customer ID: 9135 | Reason: Escalated for tone mismatch\n")

That’s it. The file is closed automatically when the block ends. You don’t have to worry about forgetting anything.

Reading is just as clean:

with open("escalations.txt", "r") as file:
    for line in file:
        print(line.strip())
Best Practice: Always use with open() when dealing with files. It’s cleaner, safer, and prevents bugs you’ll regret later.

From this point on, we’ll use with open() exclusively. It’s how you build production-grade scripts, especially in day to day automation workflows where logs and notes matter.

Reading and Writing with JSON

In the real world, your automations won't just deal with plain text. You'll often interact with structured data from APIs, error logs, databases, or platform configs. The most common format? JSON, short for JavaScript Object Notation.

JSON is the standard format for storing structured information like customer records, entities for bots, or escalation templates. And Python gives you everything you need to read and write JSON with the json module.

Let’s say you have a config file like this:

{
  "escalation_categories": ["billing", "technical", "complaint"],
  "default_priority": "medium",
  "max_notes_per_ticket": 3
}

You can load and use this in Python like this:

import json

# Read JSON config from file
with open("config.json", "r") as file:
    config = json.load(file)

print(config["escalation_categories"])
print("Default priority:", config["default_priority"])

Writing JSON is just as easy:

# Update something
config["default_priority"] = "high"

# Save it back to file
with open("config.json", "w") as file:
    json.dump(config, file, indent=2)
Tip: When writing JSON, always pass indent=2 to make it human-readable. Without it, the file becomes one long line.

JSON is your go-to for saving structured settings or customer data in a readable, portable format. You’ll use it all the time when working with webhooks, CPaaS, or bot configurations.

Common JSON Pitfalls
  • Keys must be strings — {"name": "John"} is valid, but {name: "John"} is not.
  • json.load() reads from a file. json.loads() reads from a string.
  • json.dump() writes to a file. json.dumps() returns a string.

In our upcoming mini project, we’ll use JSON to simulate a config file that defines escalation priorities and categories. That’s how real systems scale, they read from structured data, not hardcoded lists.

Working with CSV Files

While JSON is great for configs and APIs, a lot of data you’ll work with, especially logs, reports, or exported records, comes in CSV format. CSV stands for Comma-Separated Values, and it's the universal way of storing rows and columns of data in plain text.

For example, an exported escalation log might look like this:

customer_id,reason,agent,timestamp
2345,Complaint escalation,Ash,2024-07-11 09:20
1888,Billing dispute,Lee,2024-07-11 10:15

Python has a built-in csv module that makes it easy to read and write this format.

Reading a CSV File

import csv

with open("escalation_log.csv", "r") as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

Each row is a list. If your file has a header row, you can skip it manually or use csv.DictReader to treat each row as a dictionary:

with open("escalation_log.csv", "r") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print("Customer:", row["customer_id"], "Agent:", row["agent"])

Writing to a CSV File

# Add a new escalation row
new_row = ["3901", "Product malfunction", "Zara", "2024-07-11 11:45"]

with open("escalation_log.csv", "a", newline="") as file:
    writer = csv.writer(file)
    writer.writerow(new_row)
Tip: Always use newline="" when opening CSV files to avoid extra blank lines, especially on Windows.

CSVs are ideal for creating daily reports or integrating with tools that expect spreadsheet-style data.

When to Use JSON vs. CSV
  • Use JSON for nested or structured data (e.g., config files, inputs to webhooks, etc).
  • Use CSV for flat, table-style data (e.g., reports, logs, dashboards).

In our mini project, we'll extend the escalation logger to offer optional CSV export, perfect for team reporting and operational dashboards.

Mini Project: Escalation Log with Memory & Export

It’s time to tie it all together, input handling, persistence, dictionaries, JSON, and CSV into one cohesive project. Think of this like a smart agent-assist tool that logs escalations with customer metadata and stores it both for immediate reference and future analysis.

Here's what the program will do:

  • Use a fake customer database (dictionary with nested fields).
  • Let an agent enter a Customer ID and the reason for escalation.
  • Lookup the customer profile and confirm it (demoing tuples, sets, etc.).
  • Store the log in a JSON file for structured history.
  • Support viewing the JSON escalation history anytime.
  • Export the structured data to a CSV file for dashboards.
  • All input is validated with get_valid_input() and all file operations are robust and wrapped in error handling.

Here's the full script. You can paste and run this in any Python environment.

import json
import csv
import os
import random

def get_valid_input(prompt, expected_type=str):
    while True:
        try:
            value = input(prompt).strip()
            if expected_type == int:
                return int(value)
            elif expected_type == float:
                return float(value)
            elif expected_type == str:
                if value == "":
                    raise ValueError("Input cannot be empty.")
                return value
        except ValueError as e:
            print(f"Error: {e}. Please try again.")

# Sample database (from Part 3 style)
customer_db = {
    "C001": {
        "name": "Alice",
        "tags": {"VIP", "Singapore"},
        "registered": ("2021-05-21", "agentA"),
    },
    "C002": {
        "name": "Bob",
        "tags": {"Trial", "Philippines"},
        "registered": ("2022-01-10", "agentB"),
    },
}

def fetch_customer_profile(customer_id):
    profile = customer_db.get(customer_id)
    if profile:
        print(f"\nCustomer: {profile['name']}")
        print(f"Tags: {', '.join(profile['tags'])}")
        print(f"Registered: {profile['registered'][0]} (by {profile['registered'][1]})\n")
    else:
        print("No customer profile found.\n")
    return profile

def log_customer_query(customer_id, reason):
    log_entry = {
        "id": customer_id,
        "reason": reason,
        "agent": "system_user",
    }
    try:
        # Simulate potential sync failure (API down, etc.)
        if random.random() < 0.2:
            raise ConnectionError("Remote logging failed. Will retry later.")
        print("Query successfully logged to central system.\n")
    except Exception as e:
        print(f"{e}\n")
        log_entry["status"] = "PENDING SYNC"
    finally:
        print("Log attempt complete.\n")
        return log_entry

def save_to_json(data, filename="escalation_log.json"):
    try:
        if os.path.exists(filename):
            with open(filename, "r") as f:
                existing = json.load(f)
        else:
            existing = []

        existing.append(data)
        with open(filename, "w") as f:
            json.dump(existing, f, indent=2)
    except Exception as e:
        print(f"Error writing to JSON: {e}")

def summarize_queries(filename="escalation_log.json"):
    try:
        with open(filename, "r") as f:
            logs = json.load(f)
        print(f"\n--- Escalation Summary ({len(logs)} records) ---")
        for entry in logs:
            print(f"ID: {entry['id']}, Reason: {entry['reason']}, Status: {entry.get('status', 'OK')}")
        print()
    except Exception as e:
        print(f"Error reading log file: {e}\n")

def export_to_csv(filename_json="escalation_log.json", filename_csv="escalation_export.csv"):
    try:
        with open(filename_json, "r") as f:
            logs = json.load(f)

        with open(filename_csv, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=["id", "reason", "agent", "status"])
            writer.writeheader()
            for entry in logs:
                writer.writerow(entry)
        print(f"CSV export complete: {filename_csv}\n")
    except Exception as e:
        print(f"Export failed: {e}\n")

def show_menu():
    print("1. Log a customer query")
    print("2. Review past escalations")
    print("3. Export to CSV")
    print("4. Exit")

# Main loop
while True:
    show_menu()
    choice = get_valid_input("Choose an option (1–4): ", expected_type=str)

    if choice == "1":
        customer_id = get_valid_input("Customer ID: ")
        profile = fetch_customer_profile(customer_id)
        if profile:
            reason = get_valid_input("Escalation reason: ")
            log = log_customer_query(customer_id, reason)
            save_to_json(log)
    elif choice == "2":
        summarize_queries()
    elif choice == "3":
        export_to_csv()
    elif choice == "4":
        print("Goodbye!")
        break
    else:
        print("Invalid option.\n")
Builder Tip:
Try changing the customer_db with your own test users, or hook it up to a CRM API. The fallback [PENDING SYNC] pattern is useful when working with intermittent cloud logging.
Challenge:
Add a new menu option to filter and show only VIP escalations, using the tags in customer_db. Bonus: color the output differently if you’re using a rich console library!

Other Patterns You’ll Start Seeing

Now that you’re handling persistent files, especially ones with structured data, you'll notice some new patterns showing up often.

  • strip() – Used to clean extra spaces or newlines from file content.
    Why? When you read from a file, every line ends in \n. To avoid messy formatting or logic bugs, clean it first:
    line = line.strip()
  • split() – Used to break strings into parts (words, values, etc.).
    Why? If you're parsing plain-text logs, you'll often need to extract data:
    entry = "Customer ID: 1234 | Reason: Billing issue"
    parts = entry.split("|")
    id_part = parts[0].strip()
    reason_part = parts[1].strip()
  • json.load() and json.dump() – The right way to read and write structured JSON files.
    Why? JSON is everywhere — APIs, chatbots, analytics tools. You’ll use this to convert between Python and structured text:
    with open("data.json", "r") as f:
        data = json.load(f)  # loads JSON into Python list or dict
    
    with open("data.json", "w") as f:
        json.dump(data, f, indent=2)  # writes Python data to JSON file
  • try/except – More than just input handling.
    Why? You'll often wrap file reads in try blocks to gracefully handle missing files or bad formats:
    try:
        with open("missing.txt", "r") as file:
            contents = file.read()
    except FileNotFoundError:
        print("File not found. Please check the name.")
Good to Know: These aren’t just one-off tricks, they’re the building blocks of every real-world automation. Clean input. Split intelligently. Read and write structured formats. Catch problems gracefully.

Gotchas and How to Avoid Them

Persistence is powerful, but it also opens the door to new problems. Here are common mistakes you’ll run into (and how to sidestep them):

  • Forgetting to close files
    This used to be a big issue when using open() and close() manually. If the script crashes or exits early, your file might remain locked or unsaved.
    Fix: Always use with open(), it auto-closes for you.
  • Overwriting your data accidentally
    Opening a file in 'w' mode deletes everything inside it.
    Fix: Use 'a' (append) to add new lines without erasing the old ones, or confirm with the user before overwriting.
  • Assuming files will always exist
    If you try to read a file that’s not there, Python throws a FileNotFoundError.
    Fix: Use try/except to catch and handle it gracefully.
  • Mixing up string and numeric types
    Everything read from a file is a string. You can’t do math until you convert it.
    Fix: Use int() or float() as needed:
    amount_str = "49.99"
    amount = float(amount_str)
  • Messy formatting in plain-text logs
    If you don’t use strip() and split(), you’ll end up with extra spaces, broken logic, or unreadable outputs.
    Fix: Clean each line when reading:
    for line in file:
        cleaned = line.strip().split("|")
  • Corrupting your JSON file
    If you write partial or invalid JSON (e.g., during a crash), you might not be able to load it again.
    Fix: Always json.dump() in one go inside a with open() block. Wrap in try/except if reliability matters.

A Note Before You Go

You’ve officially crossed the threshold, from learning how to write code, to building scripts that can remember, persist, and interact with the world.

In this part alone, you’ve learned to:

  • Read from and write to files using open(), read(), write(), and file modes
  • Use with open() to handle files safely and cleanly
  • Store structured logs in plain text, CSV, and JSON formats
  • Build a complete, interactive script that logs and retrieves data like a real CX tool
  • Spot and avoid file I/O bugs that could break or corrupt your workflows

The Mini Project you just completed isn’t just an example, it’s a real-world pattern. If you come from the CX world, you've mostly seen these scenarios. I know there are purpose built solutions that can be deployed on call to address these problems, but hey what better way to learn. You’ve built a system that validates input, writes structured data to disk, and retrieves it on demand. That’s not “beginner code.” That’s a pattern you’ll see in real agent-assist systems, escalation trackers, and backend CX tooling.

We are actually wrapping up this series here, I personally think this is enough grounding of basics to get going. There's a part 6 post to this series, but that is more of a post script and shows you the way forward. Till then, happy building!