Lecture 8: Strings and File I/O¶
Welcome! Today we'll dive deeper into two important topics: strings (text data) and file I/O (input/output - reading from and writing to files). By the end of this lecture, you will be able to:
- manipulate strings using various methods
- understand string indexing and slicing
- format strings for output
- read data from files
- write data to files
- navigate file systems with the
osmodule
These skills will help you understand the code used in HW2 knitout_helpers.py and will come in handy for future assignments too.
Strings, not just in print()¶
We've been using strings since the beginning of the course:
print("Hello, Knitter!")
Hello, Knitter!
and also f-strings for formatting output:
count = 5
price = 12.5
print(f"Item {count:3d}: ${price:.2f}") # format specifier
Item 5: $12.50
But there's a lot more to them! Strings in Python are actually sequences, just like lists. This means they have indices, you can iterate over them, and they have a length.
Think about a knitting pattern written out as text: it's a sequence of characters that conveys meaning. We can manipulate this text programmatically, and we'll learn how to do it today.
String Indexing and Slicing¶
Strings support the same kind of indexing as lists with the [] operator:
class_number = "COMP116"
print(f"First character: {class_number[0]}") # C
First character: C
print(f"Last character: {class_number[-1]}") # 6
Last character: 6
print(f"Fourth character: {class_number[3]}") # P
Fourth character: P
And you can also use len to check the number of characters in a string:
print(f"Length: {len(class_number)}") # 7
Length: 7
Slicing lets you extract portions of a string using the syntax string[start:stop:step]:
start: index to begin (inclusive)stop: index to end (exclusive)step: increment (optional, default is 1)
which is exactly how list slicing works.
class_number[0:4] # "COMP" (indices 0, 1, 2, 3)
# Omitting the start works too because start defaults to 0
# class_number[:4]
'COMP'
class_number[4:] # "116" (from index 4 to end)
'116'
class_number[::2] # "CM16" (every other character)
'CM16'
class_number[::-1] # "611PMOC" (reversed!)
'611PMOC'
Squence Operations¶
Because it's a sequence, you can similarly check membership with in and not in:
print('116' in class_number) # True
print('112' not in class_number) # True
True True
And we've seen this before, sequences allow integer multiplication (repeating the same copy of string multiple times) and addition (string concatenation):
class_number * 3
'COMP116COMP116COMP116'
class_number + " Spring 2026"
'COMP116 Spring 2026'
Immutability of Strings¶
Unlike lists, strings are immutable! You cannot change individual characters. If you want to do that, you must create a new string instead.
yarn = "wool"
# This will cause an error:
# yarn[0] = "W" # TypeError!
# Instead, create a new string:
new_yarn = "W" + yarn[1:]
yarn, new_yarn # "wool", "Wool"
('wool', 'Wool')
String Methods¶
Python provides many useful methods for working with strings. Methods are functions that belong to a specific type of object. You call them using dot notation: string.method_name()
Case Conversion Methods¶
Sometimes it's convenient to convert all letters in a string to be all lowercase or uppercase (e.g., when searching for a substring you don't have to think about its case).
pattern = " Knit 10, Purl 10 "
pattern.upper() # " KNIT 10, PURL 10 "
' KNIT 10, PURL 10 '
pattern.lower() # " knit 10, purl 10 "
' knit 10, purl 10 '
Whitespace Removal Methods¶
It's also nice to be able to get rid of additional whitespaces that might interfere with some text processing code.
pattern.strip() # "Knit 10, Purl 10" (removes leading/trailing whitespace)
'Knit 10, Purl 10'
pattern.lstrip() # "Knit 10, Purl 10 " (removes leading whitespace only)
'Knit 10, Purl 10 '
pattern.rstrip() # " Knit 10, Purl 10" (removes trailing whitespace only)
' Knit 10, Purl 10'
Searching and Replacing Methods¶
find can find where the substring is in the parent string.
instruction = "Knit 20 stitches"
instruction.find("20") # 5 (index where "20" starts)
5
instruction.find("30") # -1 (not found)
-1
You can also give an optional argument start to specify starting from which index to find the substring:
instruction.find("20", 6) # -1 (not found because after index 6 there is no substring 20 anymore)
-1
Another method index() performs the exact same computation but instead of returning -1 if not found, it raises a ValueError.
instruction.index("30") # This errors out!
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[24], line 1 ----> 1 instruction.index("30") # This errors out! ValueError: substring not found
By default, these methods search from left to right; to search from right to left, use the rfind() or rindex().
We can also match starting substrings or ending substrings with startswith() or endswith():
class_number.startswith("COMP")
True
pattern_filename = "pattern.txt"
pattern_filename.endswith(".txt")
True
Conveniently you can replace a substring with another substring with replace. This also creates a new string instead of modifying the original string because strings are immutable.
instruction.replace("20", "30") # "Knit 30 stitches"
'Knit 30 stitches'
You can also count the number of occurrences of a substring.
instruction.count("t") # 3 (number of "t" substring occurrences)
3
Splitting and Joining Strings¶
Two very important string methods are split() and join(). These are essential for parsing pattern files!
split() converts a string into a list:
pattern = "k p k k p p k p"
stitches = pattern.split() # Splits on whitespace by default
stitches
['k', 'p', 'k', 'k', 'p', 'p', 'k', 'p']
split by default splits on whitespace, but you can specify a different separator:
colors = "red,blue,green,yellow"
color_list = colors.split(",")
color_list
['red', 'blue', 'green', 'yellow']
join() converts a list into a string:
stitches = ['k', 'p', 'k', 'k', 'p']
pattern = " ".join(stitches) # Join with spaces
pattern
'k p k k p'
And you can join with a different separator:
colors = ['red', 'blue', 'green']
color_string = ", ".join(colors)
color_string
'red, blue, green'
Checking String Properties¶
There are also a suite of methods for checking what is in a string. Recall that float and int can take a string argument and output the corresponding number if the string is actually a number. To check that, we can use:
"116".isdigit()
True
But floating point numbers do not satisfy because of the dot:
"2.17".isdigit()
False
Poll Time: How would you check if a string is a floating point number?¶
Select from the most suitable option. Can you come with other alternatives?
File I/O¶
Something related to strings is files. Files contain strings (could also be binary but binary arrays are also sequences), and we can read these strings from a file to process them, or to write some strings we created programmatically to a file so that we save it and can view it later. Python makes this easy with built-in file operations.
The os Module for Navigating the File System¶
Before we read and write files, let's learn about navigating the file system. If you still recall, in the first lecture, we learned a bit about how to navigate your folders in a command-line window like Terminal or Powershell. You can review the command line content through this Ed post. The os module provides similar functions for working with files and directories.
import os
# Get current working directory, equivalent to pwd
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")
Current directory: /Users/Bluefish_/Desktop/27_SP26/COMP116
# List files in current directory, equivalent to ls
files = os.listdir('.')
print(f"Files in current directory: {files[:5]}") # Show first 5
Files in current directory: ['textbook', 'assignments', '.DS_Store', 'machine_training', 'admin']
We can create full file path using os.path.join():
pattern_path = os.path.join(current_dir, 'pattern.txt')
print(f"Pattern path: {pattern_path}")
Pattern path: /Users/Bluefish_/Desktop/27_SP26/COMP116/pattern.txt
This is nice because it adapts to each operating system's file naming style. But does this file exist?
# Check if a file exists
print(f"Does 'pattern.txt' exist?")
os.path.exists(pattern_path)
Does 'pattern.txt' exist?
False
Writing to Files¶
Let's start by creating and writing to files. We use the open() function with mode 'w' for writing.
with open('my_pattern.txt', 'w') as file:
file.write("My First Knitting Pattern\n")
file.write("Row 1: Knit all stitches\n")
file.write("Row 2: Purl all stitches\n")
print("File created successfully!")
print(f"Find file at {os.path.join(current_dir, 'my_pattern.txt')}")
File created successfully! Find file at /Users/Bluefish_/Desktop/27_SP26/COMP116/my_pattern.txt
By default, this creates the file under the current_dir that you can find out with os.getcwd().
You don't technically need to use the with, and you can do something like file = open("file.txt", "w"), but this requires you to manually close the file stream so that no data is leaked. Using with automatically closes the file when done. This is the more idiomatic to work with files in Python.
Here, the write method takes in a string and writes that string. Note how we added the \n newline character at the end of the strings. Compared to print that by default adds this newline character to the string being printed to your screen, write does not do that automatically so we need to add the appropriate whitespace ourselves.
Question: is file in the local scope of this with statement?
You can also write multiple lines with writelines:
# Writing multiple lines at once
instructions = [
"Cast on 20 stitches\n",
"Row 1: Knit\n",
"Row 2: Purl\n",
"Repeat rows 1-2\n"
]
with open('stockinette.txt', 'w') as file:
file.writelines(instructions)
print("Stockinette pattern saved!")
Stockinette pattern saved!
Alternatively, you can use the join method we saw earlier, which is what the knitout_helpers.py does:
instructions_lines = [
"Cast on 20 stitches",
"Row 1: Knit",
"Row 2: Purl",
"Repeat rows 1-2"
]
instructions = '\n'.join(instructions_lines)
with open('stockinette.txt', 'w') as file:
file.writelines(instructions)
print("Stockinette pattern saved!")
Stockinette pattern saved!
And we are using the mode 'w' for writing into the file. The available file modes are:
'w'- write (creates new file or overwrites existing)'a'- append (adds to end of existing file)'r'- read (default, you don't need to include it inopen())
Reading from Files¶
Now let's read the files we just created. We can read the entire file as one string with read():
with open('my_pattern.txt', 'r') as file:
content = file.read()
print(content)
My First Knitting Pattern Row 1: Knit all stitches Row 2: Purl all stitches
But if we try to read the pattern.txt that was never created, it will cause a FileNotFoundError. So remember to use os.path.exists to check before reading a file!
with open('pattern.txt', 'r') as file:
content = file.read()
print(content)
--------------------------------------------------------------------------- FileNotFoundError Traceback (most recent call last) Cell In[68], line 1 ----> 1 with open('pattern.txt', 'r') as file: 2 content = file.read() 3 print(content) File ~/miniforge3/envs/comp116/lib/python3.14/site-packages/IPython/core/interactiveshell.py:344, in _modified_open(file, *args, **kwargs) 337 if file in {0, 1, 2}: 338 raise ValueError( 339 f"IPython won't let you open fd={file} by default " 340 "as it is likely to crash IPython. If you know what you are doing, " 341 "you can use builtins' open." 342 ) --> 344 return io_open(file, *args, **kwargs) FileNotFoundError: [Errno 2] No such file or directory: 'pattern.txt'
We can also use readlines() that returns a list of lines in this file:
with open('my_pattern.txt') as file: # 'r' can be omitted because it's default
lines = file.readlines()
print(f"Number of lines: {len(lines)}")
print(f"First line: {lines[0]}")
Number of lines: 3 First line: My First Knitting Pattern
Another way to read line by line is to use the for ... in ... syntax:
# Read file line by line (memory efficient for large files)
with open('my_pattern.txt', 'r') as file:
for line in file:
print(f"Line: {line.strip()}") # recall that strip() removes whitespace (the newline)
Line: My First Knitting Pattern Line: Row 1: Knit all stitches Line: Row 2: Purl all stitches
Practical Example: Reading a Knitting Pattern¶
Let's create a file with the Washboard 3 pattern from stitch-maps and then process it.

# First, create the pattern file
washboard_pattern = """Washboard 3 Pattern
Cast on: multiple of 6 stitches
Rows 1 and 3 (RS): *K1, p1, repeat from *.
Rows 2 and 4: Purl.
Rows 5 and 7: *K1, p4, k1, repeat from *.
Rows 6 and 8: Knit.
Repeat rows 1-8.
"""
washboard_filepath = os.path.join(current_dir, 'washboard3.txt')
with open(washboard_filepath, 'w') as file:
file.write(washboard_pattern)
print(f"Washboard 3 pattern saved to {washboard_filepath}!")
Washboard 3 pattern saved to /Users/Bluefish_/Desktop/27_SP26/COMP116/washboard3.txt!
# Now read and process it
with open(washboard_filepath, 'r') as file:
lines = file.readlines()
print("Pattern file contents:")
for i, line in enumerate(lines, 1):
print(f"{i}: {line.strip()}")
Pattern file contents: 1: Washboard 3 Pattern 2: Cast on: multiple of 6 stitches 3: 4: Rows 1 and 3 (RS): *K1, p1, repeat from *. 5: Rows 2 and 4: Purl. 6: Rows 5 and 7: *K1, p4, k1, repeat from *. 7: Rows 6 and 8: Knit. 8: 9: Repeat rows 1-8.
Practice Time¶
Let's try to write some file processing code using the string methods we learned earlier. They don't have to be a function.
- Extract the rows that start with
"Row"and only print those rows.
# Extract just the instruction rows
# TODO: your code here
- Count how many times
"Knit"(case insensitive) appears in the file.
# Count how many times "Knit" appears in the pattern
# Hint: process line by line and use the count() method and sum the counts up
# TODO: your code here
- Extract the parts that are surrounded by the
*(inclusive). So your code is expected to output
*K1, p1, repeat from *
*K1, p4, k1, repeat from *
# Extract the pattern repeats
# Hint: process line by line and use the string search methods and slicing methods
# TODO: your code here
Summary¶
Today we learned about:
Strings:
- Strings are sequences (can be indexed and sliced)
- Strings are immutable (cannot be changed in place)
- String methods:
.upper(),.lower(),.strip(),.split(),.join(),.find(),.replace(), etc. - String formatting with f-strings
File I/O:
- Using the
osmodule to navigate file systems (getcwd(),listdir(),path.exists(),path.join()) - Writing to files with
open(filename, 'w')and.write() - Reading from files with
open(filename, 'r')and.read(),.readlines(), or line-by-line iteration - Using
withstatements for automatic file closing
These will help you understand the starter code for HW2 and also become useful for future assignments and possibly your project!