{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "l08-c000",
   "metadata": {},
   "source": [
    "# Lecture 10 - I/O 1: Reading and writing files\n",
    "\n",
    "**Authors:** Fiona McNeill, Matteo Degiacomi, Rayo Verweij, and Nigel Topham\n",
    "\n",
    "## Introduction\n",
    "\n",
    "So far, most of our programs have lost their data when the notebook kernel stops. Files let us save that data and read it again later. We'll start with the characters that separate lines in a text file, then write and read a few small files of our own.\n",
    "\n",
    "### Learning outcomes\n",
    "\n",
    "In this notebook, we'll discuss:\n",
    "\n",
    "- using escape sequences to include newlines, tabs, backslashes, and quotes in strings;\n",
    "- distinguishing relative and absolute file paths, and writing paths that work on Windows, Linux, and macOS;\n",
    "- opening and closing text files with `open()` and `.close()`, and more safely with `with open(...)`;\n",
    "- reading a whole file or processing it one line at a time; and\n",
    "- writing, appending, and choosing which whitespace to remove.\n",
    "\n",
    "### Using the notebook\n",
    "\n",
    "Remember, this notebook is _yours_! You can edit each cell, for example by adding new examples or changing values to see what happens. Feel free to experiment!\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "A common approach for learning how to read and write code in a structured way is called **PRIMM**:\n",
    "1. **Predict**: before running the code, look at it - do you understand each line? What do you think will happen?\n",
    "2. **Run**: now run the code and see what output you get.\n",
    "3. **Investigate**: did it go as you thought it would? If not, can you figure out why?\n",
    "4. **Modify**: now change the code and once again try to predict the output. If you got it wrong the first time, did you get it right now?\n",
    "5. **Make**: finally, try to write your own program from scratch using the skills you learned.\n",
    "\n",
    "You can apply steps 1-4 to each code cell in the notebook and many will have prediction prompts to help you get started. In addition, there will be challenges that ask you to put everything together yourself and execute step 5. There will often be multiple ways to solve these, but each challenge has a potential answer listed.\n",
    "</details>\n",
    "\n",
    "### Navigating the notebook\n",
    "\n",
    "A Jupyter notebook can be operated with your keyboard. Here is a **cheat sheet**:\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "- To run the selected cell and move to the next cell, press <kbd>&#x21E7; Shift</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To run the selected cell and keep focus there, press <kbd>⌃ Ctrl</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To create a cell under the one currently highlighted, press <kbd>B</kbd>;\n",
    "- To delete the currently highlighted cell, press <kbd>D</kbd> twice (be careful with this one!) or press <kbd>X</kbd> to cut it;\n",
    "- To get help for a specific function, place the cursor within the function's brackets, hold <kbd>&#x21E7; Shift</kbd>, and press <kbd>&#x21E5; Tab</kbd>.\n",
    "\n",
    "</details>\n",
    "\n",
    "Watch out: **code cells remember what happened in cells before**. So, especially in the more complicated notebooks later down the line, make sure to always run **every** cell from top to bottom, as one might rely on a piece of code that came before it! If your results become confusing, use **Kernel > Restart Kernel** and then rerun all the completed cells in order.\n",
    "\n",
    "### Further reading and practice\n",
    "\n",
    "- Official Python tutorial: [Reading and writing files](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files) - focus on text files and the `with` statement.\n",
    "- W3Schools: [Escape characters](https://www.w3schools.com/python/python_strings_escape.asp), [Reading files](https://www.w3schools.com/python/python_file_open.asp), and [Writing files](https://www.w3schools.com/python/python_file_write.asp).\n",
    "- Helsinki Python MOOC: [Reading files](https://programming-26.mooc.fi/part-6/1-reading-files) and [Writing files](https://programming-26.mooc.fi/part-6/2-writing-files).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a918b4a",
   "metadata": {},
   "source": [
    "## 1. Escape sequences"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "253eadc7",
   "metadata": {},
   "source": [
    "In Lecture 1, we used a backslash to put a quotation mark inside a string. A backslash starts an **escape sequence**: it changes how Python reads the character that follows it. In addition to representing characters, escape sequences can represent whitespace as well:\n",
    "\n",
    "| In your code | In the string |\n",
    "| --- | --- |\n",
    "| `\\n` | A newline: move to the next line |\n",
    "| `\\t` | A tab: move to the next tab position |\n",
    "| `\\\\` | One backslash |\n",
    "| `\\\"` | A double quote |\n",
    "| `\\'` | A single quote |\n",
    "\n",
    "Each of these pairs represents one character. How wide a tab is depends on the editor or console that you are using.\n",
    "\n",
    "**Predict:** How many lines will the first `print()` display? What does `len(\"\\n\")` return?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6759ccad",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"First line\\nSecond line\")\n",
    "print(\"Name\\tScore\")\n",
    "print(\"Maya\\t8\")\n",
    "print(\"A backslash: \\\\\")\n",
    "print(\"She said \\\"hello\\\".\")\n",
    "print('It\\'s Python!')\n",
    "print(len(\"\\n\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15da43d9",
   "metadata": {},
   "source": [
    "Sometimes it can be useful to show all characters, even escaped ones, in a string. The built-in `repr()` function returns a *representation* of a value. For strings, it includes quotation marks and shows escape sequences.\n",
    "\n",
    "**Predict:** Which of the two outputs below will fit on one line?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef53526d",
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"tea\\tsoup\\ncake\"\n",
    "print(message)\n",
    "print(repr(message))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfcb9361",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: a two-line timetable</b>\n",
    "\n",
    "Create one string containing `Monday`, a tab, and `10:00`, followed by a newline, then `Tuesday`, a tab, and `11:00`. Print the string and its `repr()`. How many newline characters does your string contain?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "698495df",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Create and inspect your timetable string\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3de22e8d",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "The string itself contains one newline. `print()` adds another line ending after displaying the string.\n",
    "\n",
    "```python\n",
    "timetable = \"Monday\\t10:00\\nTuesday\\t11:00\"\n",
    "print(timetable)\n",
    "print(repr(timetable))\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1dcf75fc",
   "metadata": {},
   "source": [
    "## 2. Paths for files"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c001",
   "metadata": {},
   "source": [
    "A **path** is an address that shows where on a system a file is stored. An **absolute path** gives a complete location from the **root**, the most top-level folder, of a system. This usually looks something like `/home/student/data/notes.txt` on Linux or `C:\\Users\\student\\data\\notes.txt` on Windows, but will be different depending on how someone has set up their computer.\n",
    "\n",
    "By contrast, a **relative path** starts from the program's current working directory. For example, if I try to access `notes.txt` from a Python file, it would look for a file by that name *in the working directory*, which is usually the folder you ran the script from. The relative path `data/notes.txt` would first look for a `data` directory in the working directory, and then for a `notes.txt` file within that folder.\n",
    "\n",
    "When writing relative paths, `.` is shorthand for the current working directory, and `..` for the parent directory.\n",
    "\n",
    "Say that I have a folder structure like this:\n",
    "\n",
    "- 📁 project\n",
    "  - 📁 images\n",
    "    - 🖼️ img1.jpg\n",
    "\t- 🖼️ img2.jpg\n",
    "  - 📁 scripts\n",
    "    - 📁 data\n",
    "\t  - 📄 notes.txt\n",
    "    - 🐍 main.py\n",
    "\t- 🐍 sidequest.py\n",
    "\n",
    "Working from `main.py`, run from inside the `scripts` folder, how would you access `sidequest.py`, `notes.txt`, and `img1.jpg` using relative paths?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f70b844a",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for the solution</b></summary>\n",
    "\n",
    "You can access `sidequest.py` using either `./sidequest.py` or simply `sidequest.py`; `notes.txt` using `data/notes.txt`; and `img1.jpg` using `../images/img1.jpg`.\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-sep-01",
   "metadata": {},
   "source": [
    "### Windows vs Unix paths\n",
    "\n",
    "The folders in a path are separated by a **path separator**, and operating systems disagree about which character to use:\n",
    "\n",
    "| System | Separator | Example of an absolute path |\n",
    "| --- | --- | --- |\n",
    "| Linux and macOS | `/` | `/home/student/data/notes.txt` |\n",
    "| Windows | `\\` | `C:\\Users\\student\\data\\notes.txt` |\n",
    "\n",
    "This is a common source of confusion, and not only because the slashes lean in different directions. In section 1, we saw that a backslash in a Python string starts an escape sequence.\n",
    "\n",
    "**Predict:** What will this print?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-sep-02",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"C:\\new\\table.txt\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-sep-03",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-warning\">\n",
    "<b>Watch out: backslashes in Windows paths</b>\n",
    "\n",
    "Python reads `\\n` as a newline and `\\t` as a tab, so the path was silently changed. Some Windows paths don't run at all: `\"C:\\Users\\student\\notes.txt\"` raises a `SyntaxError`, because `\\U` starts another kind of escape sequence.\n",
    "\n",
    "Inside Python, the simplest fix is to write Windows paths with forward slashes instead, such as `\"C:/Users/student/notes.txt\"`, as **Windows will accept them too**. Writing each backslash twice, as in `\"C:\\\\Users\\\\student\\\\notes.txt\"`, also works.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d2bd0fe",
   "metadata": {},
   "source": [
    "## 3. Opening text files"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c008",
   "metadata": {},
   "source": [
    "Now that we know how to construct sound paths, we can create our own files!\n",
    "\n",
    "Python's built-in `open()` function opens files to read from and write to. It gives us a **file object**, also called a **file handle**, which we store in a variable that we can then use to work with the file.\n",
    "\n",
    "When we are done with the file, we have to call its `.close()` method to close the connection.\n",
    "\n",
    "If we try to `open()` a file that does not exist yet, Python automatically creates it for us. The following cell creates `ip26_l10_sample.txt` using `open()` with the mode `\"w\"`, for **write**. If that file had already existed, this would have replaced its contents.\n",
    "\n",
    "Then, the file object's `.write()` method writes the supplied string to the file. Unlike `print()`, `.write()` does not add newlines for us.\n",
    "\n",
    "Finally, we have to supply `open()` with an **encoding** argument. A text encoding specifies how characters are stored as bytes. UTF-8 is the most commonly used universal encoding, and can store any Unicode character, including special characters such as `é` or `£` (see also Lecture 3). Always specify that you are using UTF-8 encoding, or your program might behave differently on different devices.\n",
    "\n",
    "**Predict:** What will the contents of our new file be?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c009",
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_path = \"ip26_l10_sample.txt\"\n",
    "\n",
    "file = open(sample_path, \"w\", encoding=\"utf-8\")\n",
    "file.write(\"First line\\nSecond line\\nCafé: £2.50\\n\")\n",
    "file.close()\n",
    "\n",
    "print(\"Wrote to\", sample_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f3a0637",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "<b>Note: character encoding</b>\n",
    "\n",
    "UTF-8 is so commonly used that from Python version 3.15, released in 2026, UTF-8 has become the default value for the `encoding` parameter of `open()`. This means that when you write programs using Python 3.15, you no longer need to specify `encoding=\"utf-8\"`. However, Python 3.15 was released so recently that almost no-one has upgraded yet, including Noteable where you are probably using this notebook. For now, then, remember to always add the `encoding` argument when opening files.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92f9b8b7",
   "metadata": {},
   "source": [
    "### Reading whole files\n",
    "\n",
    "To read the file, open it with mode `\"r\"`, for **read**. The `.read()` method then returns its contents as one string.\n",
    "\n",
    "**Note:** From now on, and in the notebooks that follow, cells will often rely on previous cells to execute correctly. Make sure you have run the cell above, or the next one will not work!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fbaf126a",
   "metadata": {},
   "outputs": [],
   "source": [
    "file = open(sample_path, \"r\", encoding=\"utf-8\")\n",
    "contents = file.read()\n",
    "file.close()\n",
    "\n",
    "print(contents)\n",
    "print(type(contents))\n",
    "print(repr(contents))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acd70471",
   "metadata": {},
   "source": [
    "If the file doesn't exist, `open(..., \"r\")` raises a `FileNotFoundError`. Check the spelling of the filename, the working directory, and whether you have run the cell that creates the file.\n",
    "\n",
    "A file object also *remembers how far you have read*. A second `.read()` on the same open file (before calling `.close()`) returns an empty string if you have already reached the end. Reopening it would start reading from the beginning again."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0d67fdc",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: read and measure</b>\n",
    "\n",
    "Read `sample_path`, then print the number of characters in its contents and whether the text contains the string `Café`.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9c37bee",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Read from sample_path and inspect the resulting string\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d6b2acd",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for the solution</b></summary>\n",
    "\n",
    "If you wrote the file per the cells above, it would contain 35 characters and `\"Café\" in contents` would evaluate to `True`.\n",
    "\n",
    "```python\n",
    "file = open(sample_path, \"r\", encoding=\"utf-8\")\n",
    "contents = file.read()\n",
    "file.close()\n",
    "\n",
    "print(len(contents))\n",
    "print(\"Café\" in contents)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-01",
   "metadata": {},
   "source": [
    "### Closing files\n",
    "\n",
    "Closing a file is easy to forget, and forgetting has consequences! Until a file is closed, text we have written may still be waiting in the computer's memory, rather than saved in the file.\n",
    "\n",
    "**Predict:** The cell below writes to a new file, then reads it back twice: once before closing it and once after. What will the two `print()` calls show?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-02",
   "metadata": {},
   "outputs": [],
   "source": [
    "draft_path = \"ip26_l10_draft.txt\"\n",
    "\n",
    "file = open(draft_path, \"w\", encoding=\"utf-8\")\n",
    "file.write(\"Remember to close me\\n\")\n",
    "\n",
    "reader = open(draft_path, \"r\", encoding=\"utf-8\")\n",
    "print(repr(reader.read()))\n",
    "reader.close()\n",
    "\n",
    "file.close()\n",
    "\n",
    "reader = open(draft_path, \"r\", encoding=\"utf-8\")\n",
    "print(repr(reader.read()))\n",
    "reader.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-03",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for the explanation</b></summary>\n",
    "\n",
    "Before `file.close()`, the file was still empty: Python had kept the text in memory, to save it later in one go. Closing the file saved it. If a program stops before closing a file, for example because it crashes, text it wrote can be lost.\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-04",
   "metadata": {},
   "source": [
    "Even when we remember `.close()`, it might never run. If an error happens between `open()` and `.close()`, Python stops the cell straight away, and the line that closes the file is skipped.\n",
    "\n",
    "You can check whether a file is open or closed using the `.closed` attribute.\n",
    "\n",
    "**Predict:** `.write()` only accepts strings. What happens when the next cell runs? Is `file` closed afterwards?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-05",
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [],
   "source": [
    "file = open(\"ip26_l10_numbers.txt\", \"w\", encoding=\"utf-8\")\n",
    "file.write(42)\n",
    "file.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-06",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(file.closed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-07",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for the explanation</b></summary>\n",
    "\n",
    "`file.write(42)` raises a `TypeError`, so the cell stops before `file.close()` runs, and `file.closed` is `False` as the file was left open.\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-08",
   "metadata": {},
   "source": [
    "### A better way: `with`\n",
    "\n",
    "To better handle these types of situations, we can use a `with` statement. The `with` statement opens the file, keeps it open while the indented block runs, then closes it for us. This also happens if the block stops because of an error, so we no longer need to remember `.close()`.\n",
    "\n",
    "In the header of a `with` statement, we call `open()` and then assign the file object to a variable using the `as` keyword. Inside the statement's body, we can then use this variable for any operation that we want.\n",
    "\n",
    "**Predict:** We've made the same mistake as above, but this time inside `with`. Is `file` closed afterwards?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-09",
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [],
   "source": [
    "with open(\"ip26_l10_numbers.txt\", \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(42)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(file.closed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-close-11",
   "metadata": {},
   "source": [
    "The error still happens, but this time the file is closed.\n",
    "\n",
    "From now on, we'll always open files with `with`, as it is a safer way to code. Here is the sample file written that way: `with open(...) as file:` replaces both `open()` and `.close()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-close-12",
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(sample_path, \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"First line\\nSecond line\\nCafé: £2.50\\n\")\n",
    "\n",
    "print(\"Wrote to\", sample_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "502839e7",
   "metadata": {},
   "source": [
    "## 4. Reading one line at a time"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c016",
   "metadata": {},
   "source": [
    "A file object is *iterable*: we can loop over it, receiving one line as a string on each iteration. This lets us process a long file without first storing all its contents in one massive string.\n",
    "\n",
    "A line usually includes its ending, `\\n`. The final line might have no newline if the file ends straight after its text.\n",
    "\n",
    "**Predict:** What will `repr()` show at the end of each line from our sample file?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c017",
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(sample_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    for line in file:\n",
    "        print(repr(line))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c018",
   "metadata": {},
   "source": [
    "You already know `.strip()`: it removes whitespace from both ends of a string. That includes spaces, tabs, and newlines. It leaves whitespace in the middle alone.\n",
    "\n",
    "What if we only wanted to clean newlines from the right side of a string, such as the ones in the example above? Python's `.rstrip()` strips whitespace from only the right side of a string, and we can pass the characters we want to strip as arguments, like this: `.rstrip(\"\\n\")`.\n",
    "\n",
    "**Predict:** Which output keeps the spaces around `Portobello Beach`?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c019",
   "metadata": {},
   "outputs": [],
   "source": [
    "raw = \"   Portobello Beach   \\n\"\n",
    "print(repr(raw.strip()))\n",
    "print(repr(raw.rstrip(\"\\n\")))\n",
    "\n",
    "with open(sample_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    for line in file:\n",
    "        cleaned_line = line.strip()\n",
    "        print(f\"[{cleaned_line}]\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c020",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: count non-empty lines</b>\n",
    "\n",
    "Create a text file containing three non-empty lines and one blank line. Read it line by line and count only lines for which `line.strip()` is not empty. Then try a line containing only spaces: should it count?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c021",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "lines_path = \"ip26_l10_lines.txt\"\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c022",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "This prints `3`. A line containing only spaces also becomes an empty string after `.strip()`, so it is not counted.\n",
    "\n",
    "```python\n",
    "lines_path = \"ip26_l10_lines.txt\"\n",
    "with open(lines_path, \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"alpha\\n\\nbeta\\ngamma\\n\")\n",
    "\n",
    "count = 0\n",
    "with open(lines_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    for line in file:\n",
    "        if line.strip() != \"\":\n",
    "            count += 1\n",
    "print(count)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ab1c62a",
   "metadata": {},
   "source": [
    "## 5. Writing and appending"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c023",
   "metadata": {},
   "source": [
    "Opening an existing file with `\"w\"` erases all of its contents straight away, before anything is written. To keep the existing text and add more at the end, use `\"a\"`, for **append**. Both modes create a file if it does not exist.\n",
    "\n",
    "| Mode | Purpose | If the file already exists | If the file is missing |\n",
    "| --- | --- | --- | --- |\n",
    "| `\"r\"` | Read | Read its contents | Raise `FileNotFoundError` |\n",
    "| `\"w\"` | Write | Replace its contents | Create and write to it |\n",
    "| `\"a\"` | Append | Add at the end | Create and write to it |\n",
    "\n",
    "**Predict:** How many entries appear when you run the next cell? If you rerun the whole cell, will that number change?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c024",
   "metadata": {},
   "outputs": [],
   "source": [
    "log_path = \"ip26_l10_study_log.txt\"\n",
    "\n",
    "with open(log_path, \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"Started loops practice\\n\")\n",
    "    file.write(\"Completed three exercises\\n\")\n",
    "\n",
    "with open(log_path, \"a\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"Reviewed collections\\n\")\n",
    "\n",
    "with open(log_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    print(file.read())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c025",
   "metadata": {},
   "source": [
    "There are three entries each time: the first block replaces the file before the second block appends. If you reran only the append block, another entry would be added.\n",
    "\n",
    "Before using `\"w\"` in your own code, check that you are either creating a new file or that you mean to replace one that already exists!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c026",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: maintain a journal</b>\n",
    "\n",
    "Write two entries to `ip26_l10_journal.txt`, append a third, then read and print all three. Include `\\n` after each entry. Try appending a fourth entry in a separate cell.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c027",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "journal_path = \"ip26_l10_journal.txt\"\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c028",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "journal_path = \"ip26_l10_journal.txt\"\n",
    "\n",
    "with open(journal_path, \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"Predicted a loop trace\\n\")\n",
    "    file.write(\"Fixed an off-by-one error\\n\")\n",
    "\n",
    "with open(journal_path, \"a\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"Explained the fix\\n\")\n",
    "\n",
    "with open(journal_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    print(file.read())\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d28dbe4f",
   "metadata": {},
   "source": [
    "## 6. Processing data from a file"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-c029",
   "metadata": {},
   "source": [
    "Text files store text (as strings), even when that text looks like numbers. We need `int()` or `float()` before doing arithmetic with the values we read.\n",
    "\n",
    "**Predict:** What are the type and value of `line` on the first iteration? What are the type and value of `value`? Work out the final total before running the cell.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-c030",
   "metadata": {},
   "outputs": [],
   "source": [
    "readings_path = \"ip26_l10_readings.txt\"\n",
    "with open(readings_path, \"w\", encoding=\"utf-8\") as file:\n",
    "    file.write(\"12\\n15\\n9\\n\")\n",
    "\n",
    "total = 0\n",
    "count = 0\n",
    "with open(readings_path, \"r\", encoding=\"utf-8\") as file:\n",
    "    for line in file:\n",
    "        value = int(line.strip())\n",
    "        total += value\n",
    "        count += 1\n",
    "\n",
    "print(f\"Read {count} values; total = {total}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-consolidation-prompt",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "## Consolidation challenge\n",
    "\n",
    "Let's finish with one more challenge to bring everything together.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: save and reload a task list</b>\n",
    "\n",
    "Write `save_lines(path, lines)` to write each string in a list on its own line. Then write `load_nonempty_lines(path)` to return a list of stripped, non-empty lines from a file.\n",
    "\n",
    "Use the functions to save `[\"read\", \"\", \"  practise  \", \"review\"]` to `ip26_l10_tasks.txt`, then read it back. Use an assertion, from Lecture 9, to check that the result is `[\"read\", \"practise\", \"review\"]`. Also check an empty list.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l08-consolidation-work",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your solution here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-consolidation-solution",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "def save_lines(path, lines):\n",
    "    \"\"\"Writes a list of lines to a file, separated by newlines\"\"\"\n",
    "    with open(path, \"w\", encoding=\"utf-8\") as file:\n",
    "        for line in lines:\n",
    "            file.write(line + \"\\n\")\n",
    "\n",
    "def load_nonempty_lines(path):\n",
    "    \"\"\"Reads individual lines from a file and saves them in a list, ignoring empty lines\"\"\"\n",
    "    result = []\n",
    "    with open(path, \"r\", encoding=\"utf-8\") as file:\n",
    "        for line in file:\n",
    "            cleaned = line.strip()\n",
    "            if cleaned != \"\":\n",
    "                result.append(cleaned)\n",
    "    return result\n",
    "\n",
    "task_path = \"ip26_l10_tasks.txt\"\n",
    "save_lines(task_path, [\"read\", \"\", \"  practise  \", \"review\"])\n",
    "assert load_nonempty_lines(task_path) == [\"read\", \"practise\", \"review\"]\n",
    "save_lines(task_path, [])\n",
    "assert load_nonempty_lines(task_path) == []\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l08-consolidation-summary",
   "metadata": {
    "tags": [
     "summary"
    ]
   },
   "source": [
    "## Summary\n",
    "\n",
    "Well done! You can now create and read your own files. Our programs are rapidly becoming more powerful, and will level up even further next time when we look at modules that others have written.\n",
    "\n",
    "### Key terms\n",
    "\n",
    "<dl>\n",
    "\t<dt><b>Escape sequence</b></dt>\n",
    "\t<dd>A backslash followed by a character that represents something such as a newline or a quote in a string.</dd>\n",
    "\t<dt><b>Path</b></dt>\n",
    "\t<dd>A location in a filesystem.</dd>\n",
    "\t<dt><b>Absolute path</b></dt>\n",
    "\t<dd>A path starting from the root of a filesystem.</dd>\n",
    "\t<dt><b>Relative path</b></dt>\n",
    "\t<dd>A path starting from a particular folder.</dd>\n",
    "\t<dt><b>Path separator</b></dt>\n",
    "\t<dd>The character between folder names in a path: <code>/</code> on Linux and macOS, <code>\\</code> on Windows.</dd>\n",
    "\t<dt><b>Working directory</b></dt>\n",
    "\t<dd>The directory from which relative paths are interpreted.</dd>\n",
    "\t<dt><b>File object</b></dt>\n",
    "\t<dd>An object through which a program reads or writes a file, also called a file handle.</dd>\n",
    "\t<dt><b>Text encoding</b></dt>\n",
    "\t<dd>A rule for representing characters as bytes. UTF-8 is a widely used encoding for Unicode text.</dd>\n",
    "</dl>\n",
    "\n",
    "### New syntax\n",
    "\n",
    "<dl>\n",
    "\t<dt><code>\\n, \\t, \\\\, \\&quot; and \\&#39;</code></dt>\n",
    "\t<dd>Escape sequences for a newline, a tab, a backslash, a double quote, and a single quote.</dd>\n",
    "\t<dt><code>repr(value)</code></dt>\n",
    "\t<dd>Returns a representation of <code>value</code>. For strings this shows quotes and escape sequences.</dd>\n",
    "\t<dt><code>file = open(path, mode, encoding=\"utf-8\")</code> and <code>file.close()</code></dt>\n",
    "\t<dd>Opens a text file and returns a file object; closing it saves any text still waiting to be written. Less safe than using <code>with</code>.</dd>\n",
    "\t<dt><code>with open(path, mode, encoding=\"utf-8\") as file:</code></dt>\n",
    "\t<dd>Opens a text file for the indented block, then closes it.</dd>\n",
    "\t<dt><code>\"r\", \"w\", \"a\"</code></dt>\n",
    "\t<dd>File modes for reading, replacing contents, and appending, respectively.</dd>\n",
    "\t<dt><code>file.read()</code></dt>\n",
    "\t<dd>Returns the remaining contents of an open text file as a string.</dd>\n",
    "\t<dt><code>file.write(text)</code></dt>\n",
    "\t<dd>Writes a string without adding a newline. Returns the number of characters written.</dd>\n",
    "\t<dt><code>file.closed</code></dt>\n",
    "\t<dd>Returns <code>True</code> or <code>False</code> depending on whether the file is closed or not.</dd>\n",
    "\t<dt><code>for line in file:</code></dt>\n",
    "\t<dd>Reads an open text file one line at a time.</dd>\n",
    "\t<dt><code>text.rstrip(\"\\n\")</code></dt>\n",
    "\t<dd>Returns the text with newline characters removed from its right-hand end.</dd>\n",
    "\t<dt><code>FileNotFoundError</code></dt>\n",
    "\t<dd>A type of error raised when a requested file or directory cannot be found.</dd>\n",
    "</dl>\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
