Introduction
In our previous lesson, you mastered pathlib.Path, the modern, object-oriented way to handle file paths. You learned how to construct, manipulate, and query paths in a clean, cross-platform manner, moving away from the older, string-based os.path module.
Now that you can confidently point to any file on the system, it's time to interact with its contents. This lesson focuses on the fundamental I/O operations for text files, which are the backbone of countless applications, from reading configuration files to processing logs.
The learning outcome for this lesson is to: Read and write text files safely using with open(...) and explicitly specifying encoding='utf-8'.
We will cover two critical concepts that are non-negotiable for writing robust, professional Python code:
- The
withstatement: A construct for safely managing resources like files, ensuring they are always closed properly. - File encoding: The rulebook for converting text to bytes and back, and why getting it wrong leads to bugs and data corruption.
1. Safe Resource Management: The with Statement
In languages like Java, you might be familiar with try-with-resources blocks to ensure resources are closed. Python has an even more concise and idiomatic construct: the with statement.
The "un-Pythonic" way to open a file involves manually calling .close():
# ANTI-PATTERN: Manual file management
f = open('my_file.txt', 'r')
data = f.read()
# What if an error happens here? The file is never closed.
f.close()
This approach is fragile. If an error occurs before f.close() is called, the file object is left open, potentially leaking system resources. The Pythonic solution is to use a context manager, which the with statement provides.
Python Tutorial: File Objects - Reading and Writing to Files
To understand the with statement's role, let's look at a detailed explanation from the 'Python Tutorial' by Corey Schafer. He first shows the manual open/close method and then introduces the with statement, explaining why it's the best practice.
Watch from 03:06 to 05:41. Pay close attention to how the with statement automatically handles closing the file, even if exceptions occur. This is the single most important pattern for file I/O in Python.
As the video demonstrates, the with open(...) as f: syntax guarantees that the file f will be closed as soon as the code block is exited, for any reason—whether it finishes successfully or raises an error. This makes your code safer and cleaner.
File Handling Best Practices in Python
The PythonForAll guide also emphasizes this as the 'golden rule' of file handling. It's worth reading their quick take on it.
Read the first section titled '1. The Golden Rule: Always Use the with Statement'. It reinforces the concept and explicitly calls out the manual file.close() method as an anti-pattern.
2. The Crucial Detail: Text vs. Bytes and Encoding
Before we can read or write text, we must understand how a computer stores it. This is a common source of bugs that trip up even experienced developers.
str(Text): A Pythonstrobject is a sequence of characters (or Unicode code points). These are abstract concepts like 'A', '€', or '😊'. They have semantic meaning to humans.bytes(Bytes): Abytesobject is a sequence of raw 8-bit numbers (from 0 to 255). This is how data is actually stored in memory or on disk. Bytes have no inherent meaning.
Encoding is the set of rules that translates characters into bytes (encode) and bytes back into characters (decode).

This distinction is fundamental. The mCoding video 'str vs bytes in Python' provides one of the clearest explanations of this concept available.
Watch from 01:59 to 05:56. The video uses a smiley face emoji to demonstrate how a single character can be represented by multiple bytes. This segment clearly explains why a sequence of bytes is meaningless without knowing the encoding used to create it.
3. The Mandate: Always Specify encoding='utf-8'
If encoding is the rulebook, what happens if you don't specify which one to use?
# DANGEROUS: Relies on a system-dependent default encoding
with open('data.txt', 'w') as f:
f.write('résumé')
When encoding is omitted, Python uses a system default. On modern macOS and Linux, this is usually UTF-8. On many Windows systems, it might be cp1252. If you write a file with non-ASCII characters (like 'é') on a Windows machine and then try to read it on a Linux server, your program will likely crash with a UnicodeDecodeError or, worse, silently corrupt the data.
The unbreakable rule is to always be explicit. For nearly all modern applications, UTF-8 is the correct choice as it can represent virtually any character.
# SAFE and PORTABLE: Explicitly specify the encoding
with open('data.txt', 'w', encoding='utf-8') as f:
f.write('résumé')
with open('data.txt', 'r', encoding='utf-8') as f:
content = f.read()
print(content) # -> résumé
Let's see the consequences of ignoring encoding. The next segment of the mCoding video demonstrates this exact problem and the simple fix.
Watch from 05:56 to 08:06. This section shows a UnicodeEncodeError when writing and silent data corruption when reading with the wrong encoding. It directly demonstrates why encoding='utf-8' is not optional for robust code.
The key takeaway from both DigitalOcean's and PythonForAll's guides is identical: be explicit to prevent errors.
Python open() Function Explained and File Encoding
For a textual reinforcement of this rule, quickly review these two resources.
Skim the section 'File Encoding and Handling Non-UTF-8 Files' and the FAQ '5. How to open a file in a different encoding?'. They confirm that explicitly setting encoding='utf-8' is the standard way to avoid UnicodeDecodeError.
4. Reading and Writing in Practice
Now let's combine these concepts to perform common file operations.
File Modes
When you open a file, you specify a mode. The most common text modes are:
'r': Read (default). Fails if the file doesn't exist.'w': Write. Creates the file if it doesn't exist; erases the file if it does.'a': Append. Creates the file if it doesn't exist; adds new data to the end.

Reading Files
There are several ways to read a file, but one is most idiomatic for processing line-by-line.
Python Tutorial: File Objects - Reading and Writing to Files
Corey Schafer's tutorial covers the primary methods for reading file content. Pay attention to the distinction between reading the whole file at once versus iterating over it line by line.
Watch from 05:50 to 12:28. This long segment covers: f.read(): Reads the entire file into a single string. f.readlines(): Reads the entire file into a list of strings. for line in f:: This is the Pythonic way. It reads the file line-by-line, which is memory-efficient for large files. Reading in fixed-size chunks, which is less common for text files but useful to know.
Example: The Idiomatic Way
from pathlib import Path
# We'll use the pathlib object from the last lesson
file_path = Path("my_log.txt")
# Let's assume my_log.txt contains:
# line 1
# line 2
# line 3
try:
with open(file_path, 'r', encoding='utf-8') as f:
# This loop is memory-efficient and clean
for line in f:
# .strip() removes leading/trailing whitespace, including the newline character
print(line.strip())
except FileNotFoundError:
print(f"Error: {file_path} was not found.")
Writing Files
Writing is straightforward with the .write() method.
Python Tutorial: File Objects - Reading and Writing to Files
Next, let's look at writing. The same video explains the 'write' and 'append' modes.
Watch from 14:38 to 16:08. This section clearly explains the difference between 'w' (overwrite) and 'a' (append).
Example: Writing and Appending
from pathlib import Path
report_path = Path("report.txt")
# Using 'w' will create or overwrite the file
with open(report_path, 'w', encoding='utf-8') as f:
f.write("Annual Report\n")
f.write("="*13 + "\n")
# Using 'a' will add to the end of the file
with open(report_path, 'a', encoding='utf-8') as f:
f.write("- Q1 Results: Strong\n")
A Note on Binary Files
When you need to work with non-text files like images, executables, or compressed archives, you use binary mode.
- Add
'b'to the mode (e.g.,'rb','wb'). - Do not specify an encoding. You are working directly with bytes.
# Example of copying an image file
with open('source.jpg', 'rb') as f_in, open('destination.jpg', 'wb') as f_out:
chunk = f_in.read(4096) # Read in 4KB chunks
while chunk:
f_out.write(chunk)
chunk = f_in.read(4096)
Corey Schafer's video covers this in the section from 20:30 to 24:00 if you are interested in a practical demonstration of copying an image file.
Conclusion
You have now learned the two fundamental rules for professional-grade text file handling in Python. Internalizing these practices will prevent a significant class of common bugs related to resource leaks and data corruption.
Key Takeaways:
- Always use
with open(...): It guarantees that files are closed automatically and safely, making your code robust against errors. It is the only idiomatic way to manage file resources. - Always specify
encoding="utf-8"for text files: This ensures your code is portable and correctly handles all text data, preventingUnicodeErrorexceptions and silent data corruption. - Iterate for efficiency: For reading text files, the
for line in f:loop is the most memory-efficient and Pythonic approach. - Choose the right mode: Use
'r'for reading,'w'for overwriting, and'a'for appending. Add'b'for binary files and omit the encoding.
In the next lesson, we'll build on this knowledge. Now that you can reliably read the contents of a text file, we will explore how to parse structured text data using the csv and json modules from Python's standard library.