Lab 02 - Functions, While loops, and File I/O

Due by 11:59pm on January 16, 2025

In this lab you’ll continue to practice your skills with fundamental Python concepts. Specifically, you’ll be looking at file I/O, functions, while loops, and lists.

As always, you should read the entire assignment before starting on any part of it.

A note on testing

Throughout the lab questions you’ll be prompted to run the tests on your code. This is a habit you should get into on all of your assignments and we’ll prompt you to do it in the early ones. If you are testing as you go (as you should be), there will be many errors reported for the parts of the assignments that you haven’t implemented yet. We sometimes provide commands to just test a portion of your code reducing the errors but not always. This is okay, you should just be looking for errors on the parts you have done and eliminate them before moving on.

Starter Files

Once you’ve read through the entire assignment, come back here and download lab02.zip. Inside the archive, you will find starter files for the questions in this lab.

Topics

You can skip over this Topics section if you and everyone in your group feels confident in your understanding as this is just a review of concepts from lecture, but if you run in to questions later on, this should be the first place you look for help.

If not everyone is confident in their understanding of the topics, you should discuss with one another to help them understand. Teaching others is one of the best way to cement your own understanding. Take advantage of the opportunity when it arises.

While Loops

You can review the syntax of while loops in Section 1.5.5 of Composing Programs. It will also be reviewed down below.

A while loop requires an expression that evaluates to either a truthy or falsey value. As long as that expression evaluates to truthy, the code in the while loop’s body will continue to execute. Because of this, you must be careful not to put a condition that will never become falsey or else the while loop will run forever! Here’s the syntax:

while <conditional expression>:
    <suite of statements>

Here’s an example of a while loop that will iterate (execute the loop’s body) 5 times using a variable called i.

i = 0
while i < 5:
    print(i)
    i += 1 # equivalent to `i = i + 1`

Lists

A list is a data structure that can store multiple elements. Each element can be any type, even a list itself. We write a list as a comma-separated list of expressions in square brackets:

>>> list_of_ints = [1, 2, 3, 4]
>>> list_of_bools = [True, True, False, False]
>>> nested_lists = [1, [2, 3], [4, [5]]]

Each element in the list has an index, with the index of the first element starting at 0. We say that lists are therefore “zero-indexed.”

With list indexing, we can specify the index of the element we want to retrieve. A negative index represents starting from the end of the list, where the negative index -i is equivalent to the positive index len(lst)-i.

>>> lst = [6, 5, 4, 3, 2, 1, 0]
>>> lst[0]
6
>>> lst[3]
3
>>> lst[-1] # Same as lst[6]
0
>>> lst[-2]
1

To check if an element is within a list, use Python’s in keyword. For example,

>>> numbers = [2, 3, 4, 5]
>>> 1 in numbers
False
>>> n = 2
>>> n in numbers
True

To find the number of elements in a list, Python comes with a prebuilt function len(<list>).

>>> numbers = [0, 1, 2, 3, 4]
>>> len(numbers)
5

With the len() function, we can iterate through each element of a list using an index.

>>> numbers = [10, 9, 8, 7, 6]
>>> index = 0
>>> while  index < len(numbers):
...     print(numbers[index])
...     index += 1
...
10
9
8
7
6

When accessing an element through a list using an index, we are given a reference to the element and are allowed to manipulate it within the list.

>>> numbers = [10, 9, 8]
>>> numbers[0] = 5
>>> numbers
[5, 9, 8]

Additional List Methods

Some of these may come in very handy when coding, but they may not be necessarily required in this class.

Adding to a List

  • .append(<elem>) - Adds an element to the back of a list. Returns None.
>>> nums = [0, 1, 2]
>>> nums.append(3)
>>> nums
[0, 1, 2, 3]
  • .extend(<list>) - Extends a list given another list. Returns None.
>>> nums = [0, 1, 2]
>>> new_nums = [3, 4, 5]
>>> nums.extend(new_nums)
>>> nums
[0, 1, 2, 3, 4, 5]
  • .insert(<index>, <elem>) - Inserts an element at a given index and returns None. If there is already an element at the index, it will be pushed back to make space for the new element.
>>> forbidden_toppings = ["pineapple", "tomato"]
>>> forbidden_toppings.insert(1, "mushroom")
>>> forbidden_toppings
["pineapple", "mushroom", "tomato"]

Removing from a List

  • .pop(<index>) - Removes the element at the provided index. If an index is not provided, the index will default to the last index in the list (len(<list>) - 1) and remove the last element. .pop() returns the removed element.
>>> nums = [0, 1, 2, 3]
>>> three = nums.pop()
>>> nums
[0, 1, 2]
>>> three
3
>>> nums.pop(1)
1
>>> nums
[0, 2]
  • .remove(<elem>) - Removes the first occurrence of the element provided. Returns None.
>>> food = ['potato', 'tomato', 'tomato', 'pineapple']
>>> food.remove('tomato')
>>> food
['potato', 'tomato', 'pineapple']

It is important to notice that none of these methods return a modified list but instead modify the list provided directly.

You can also refer to w3schools for more detailed information and more methods like sort()

Functions

If we want to execute a series of statements over and over, we can abstract them away into a function to avoid repeating code. In fact, it is a good programming practice that whenever you find yourself writing the same code a second time, you should move that code to a function and call the function where you would have written the code.

For example, let’s say we want to know the results of multiplying the numbers 1-3 by 3 and then adding 2 to it. Here’s one way to do it:

>>> 1 * 3 + 2
5
>>> 2 * 3 + 2
8
>>> 3 * 3 + 2
11

If we wanted to do this with a larger set of numbers, that’d be a lot of repeated code! Let’s write a function to capture this operation given any input number.

def foo(x):
    return x * 3 + 2

This function, called foo, takes in a single argument and will return the result of multiplying that argument by 3 and adding 2.

Now we can call this function whenever we want this operation to be done:

>>> foo(1)
5
>>> foo(2)
8
>>> foo(1000)
3002

Applying a function to some arguments is done with a call expression.

Call Expressions

A call expression applies a function, which may or may not accept arguments. The call expression evaluates to the function’s return value.

The syntax of a function call:

  add   (    2   ,    3   )
   |         |        |
operator  operand  operand

Every call expression requires a set of parentheses delimiting its comma-separated operands.

To evaluate a function call:

  1. Evaluate the operator, and then the operands (from left to right).
  2. Apply the operator to the operands (the values of the operands).

If an operand is a nested call expression, then these two steps are applied to that inner operand first in order to evaluate the outer operand.

return and print

Most functions that you define will contain a return statement. The return statement will give the result of some computation back to the caller of the function and exit the function. For example, the function square below takes in a number x and returns its square.

def square(x):
    """
    >>> square(4)
    16
    """
    return x * x

When Python executes a return statement, the function terminates immediately. If Python reaches the end of the function body without executing a return statement, it will automatically return None.

In contrast, the print function is used to display values in the terminal. This can lead to some confusion between print and return because calling a function in the Python interpreter will print out the function’s return value.

However, unlike a return statement, when Python evaluates a print expression, the function does not terminate immediately.

def what_prints():
    print('Hello World!')
    return 'Exiting this function.'
    print('CS111 is awesome!')

>>> what_prints()
Hello World!
'Exiting this function.'
Note

Notice that print will display text without the quotes, but return will preserve the quotes when the return value is displayed by the interpreter.

File I/O

Whenever we perform an operation with a program, unless we save the output of that operation to a file, the information is lost when we close the program. In this class, we will primarily focus on writing to and reading from text files.

Python already has a built-in way to create, write to, and read files.

To work with a file, we need to open it by using the open function and provide as arguments the filename and the mode we want to use. There are several different modes for opening a file such as write mode, read mode, or append mode.

Opening and Closing a File

There are three steps when working with files:

  1. Open the file in a mode.
  2. Do stuff with the file.
  3. Close the file.

To open and close a file, you could use this syntax:

file = open(<file_name>, <mode>)

# code that does stuff with the file

file.close()
Warning

Please don’t do it that way in your code unless you need to. It’s very common to forget to close a file, the problems it causes are not obvious, and it will break the autograder. Instead, make the computer do that work for you.

Do this instead:

with open(<file_name>, <mode>) as file:

    # code that does stuff with the file

# the file is closed out here

All the code that is indented after the with open, will have access to the file for reading or writing. Once Python is finished executing the indented code suite, then the file automatically closes.

file_name and mode have to be strings. Mode specifically has to be either

  • "w" - write mode
  • "r" - read mode
  • "a" - append mode
  • or other modes supported by python
Note

You only really need to use the first method (using open() and close()) if you have a bunch of files open simultaneously that you are reading and writing to at the same time. You could still use the with open() syntax to do that, but you have to nest all the with statements and if you have a lot of files, that’s a lot of indentation.

Important

If you don’t use the with open() method, make sure to close your file. Not doing so will waste space and break the autograder.

Reading From a File

If you do not provide a mode when opening a file, python will automatically open it in read mode.

There are multiple ways to read from a file. The following are a few methods to do so:

  • .read() - returns a string containing all the contents of the file including newlines \n and tabs \t.
  • .readline() - returns a string with the contents of the first line including newlines \n and tabs \t. Once it returns the contents of the first line, another call to readline() will return the contents of the second line, and so on and so forth until the final line.
  • .readlines() - returns a list of strings where each string is a line. Newlines and tabs are explicitly shown in each string.

Remember, since these are methods, these follow the format of <file>.read(). The differences between methods and functions will become more apparent in the Classes lectures later in the semester.

Writing to a File

If the file does not exist and you are opening the file in write mode, then python will automatically create the file. If the file already has content, opening it in write mode will erase everything and override it with what you choose to write to it.

There are two ways to write to a file in python:

  1. .write(<line_content>) - writes the exact contents given in a string.
  2. .writelines(<list_of_line_content) - writes each string in the list into the file.

.write() may not work as you might think. If we were to use the following code:

with open("cs_is_cool.txt", 'w') as file:
    file.write("Hello World")
    file.write(":D")

Right now, our cs_is_cool.txt looks like this:

Hello World:D

By default, the write() method does not put a new line at the end of the string; we have to do that ourselves by putting "\n" (backslash-n) at the end of the string.

file.write("Hello World\n")
file.write(":D")

Now, with the addition of the "\n", our cs_is_cool.txt looks like this:

Hello World
:D

Additionally, we can use the writelines() method which takes in a list of strings and writes each of them into the file:

lines = ["Hello World\n", ":D"]
file.writelines(lines)

Required Questions

Q1: WWPD: Lists

What would Python display for each of these? (If you are completely stumped on what the code is doing, type the code in to the interpreter and see what happens and if you were correct. If not, spend some time figuring out what was wrong.)

>>> a = [1, 5, 4, [2, 3], 3]
>>> print(a[0], a[-1])
______
>>> len(a)
______
>>> 2 in a
______
>>> 2 in a[3]
______
>>> a[3][0]
______
>>> a[3][0] += 1
>>> a
______
>>> a[3][2]
______

Discuss the following with your group:

  • Did the output of any of these statements surprise you or gave results you didn’t expect? Which ones?
  • Do you understand why the output you saw occurred? If you don’t completely understand, ask someone in your group to explain it. Try explaining one of the results you do understand to someone else.

Q2: WWPD: Control

Use the following “What Would Python Display?” questions to test your knowledge on while statements:

Hint

Make sure your while loop conditions eventually evaluate to a false value, or they’ll never stop! Typing [Ctrl]+[C] will stop infinite loops in the interpreter.

>>> n = 3
>>> while n >= 0:
...     n -= 1
...     print(n)
______
______
______
______
>>> positive = 13
>>> while positive:
...    print(f"Is {positive} positive?")
...    positive -= 3
______

Discuss the following with your group:

  • In the first code snippet, what was the last value printed? Do you understand why? Walk through the code flow with someone explaining what is happening
  • In the second, why didn’t the loop terminate on its own? How would you modify the code to make it stop when the positive variable becomes negative?

Q3: Even Weighted

Implement the function even_weighted that takes a list s and returns a new list that keeps only the even-indexed elements of s and multiplies those elements by their index in the original list.

def even_weighted(s):
    """
    >>> x = [1, 2, 3, 4, 5, 6]
    >>> even_weighted(x)
    [0, 6, 20]
    """
    "*** YOUR CODE HERE ***"
Hint

Recall that a even number is a number that is evenly divisible by 2 and therefore has 0 remainder. We can use the modulo operator % to find the remainder and ensure it is 0: number % 2 == 0.

Test your code

Start with the doctests:

python3 -m doctest lab02.py

You should see zero errors for even_weighted. If there are errors for this function, fix them and keep testing until they are gone.

Next run the pytests:

pytest -vv test_lab02.py::test_even_weighted_1
pytest -vv test_lab02.py::test_even_weighted_2

Again, you should see zero errors for tests that include even_weighted in their name. If there are errors for this function, fix them and keep testing until they are gone.

Q4: Couple

Implement the function couple(), which takes in two lists and returns a list that contains lists with i-th elements of two sequences coupled together. You can assume the lengths of two sequences are the same. Try using a list comprehension.

def couple(s, t):
    """Return a list of two-element lists in which the i-th element is [s[i], t[i]].

    >>> a = [1, 2, 3]
    >>> b = [4, 5, 6]
    >>> couple(a, b)
    [[1, 4], [2, 5], [3, 6]]
    >>> c = ['c', 6]
    >>> d = ['s', '1']
    >>> couple(c, d)
    [['c', 's'], [6, '1']]
    """
    assert len(s) == len(t)
    "*** YOUR CODE HERE ***"

Test your code

Start with the doctests:

python3 -m doctest lab02.py

You should see zero errors for couple. If there are errors for this function, fix them and keep testing until they are gone.

Next run the pytests:

pytest -vv test_lab02.py::test_couple_1
pytest -vv test_lab02.py::test_couple_2

Again, you should see zero errors for tests that include couple in their name. If there are errors for this function, fix them and keep testing until they are gone.

Q5: WWPD: Reading from a file

Create a file called cs_is_cool.txt and type something into it that spans a few lines. Be sure to save the file. Make sure you are in the same directory as the cs_is_cool.txt file create a demo.py file. Copy the following code to that file. Before you run the code, discuss what you think will be printed out. Now run it.

# if a mode is not provided, open will automatically default to read mode
with open("cs_is_cool.txt") as input_file:

    file_lines = input_file.readlines()

print(file_lines)

Discuss with your group:

  • What shows up? Try using .readline() and .read() instead.
  • Do you understand what is happening with each function? Can you explain the results to someone else?

Q6: Copy File

Implement the copy_file function. It takes two strings, input_filename and output_filename. It opens the two files, reads the file specified by the input_filename line by line, and for each line it prints and writes to the file specified by the output_filename the line with the line number and colon prepended to it. This function does not return anything.

def copy_file(input_filename, output_filename):
    """Print each line from input with the line number and a colon prepended,
    then write that line to the output file.
    >>> copy_file('text.txt', 'output.txt')
    1: They say you should never eat dirt.
    2: It's not nearly as good as an onion.
    3: It's not as good as the CS pun on my shirt.
    """
    "*** YOUR CODE HERE ***"
Hint

When reading from a file, python will interpret the new lines as a literal "\n". As a result, when printing a line, you will have an extra empty new line because the print() function already appends a newline to the end of the content you provide. To mitigate this, you can use use print(line, end=""). By default, end="\n" and changing this removes the extra empty newline. Another solution is to use <string>.strip() on each of the lines from the file you are reading from. The .strip() method will removing any leading or trailing whitespace from a string, like newlines.

Test your code

Start with the doctests:

python3 -m doctest lab02.py

You should see zero errors for copy_file. If there are errors for this function, fix them and keep testing until they are gone.

Next run the pytests:

pytest -vv test_lab02.py::test_copy_file

Again, you should see zero errors for tests that include copy_file in their name. If there are errors for this function, fix them and keep testing until they are gone.

Submit

Once the pytests are all passing, your assignment is complete and ready to submit. Submit your lab02.py file to Gradescope to receive credit. Submissions will be in Canvas.


Extra Practice

Q7: Factors List

Write factors_list, which takes a number n and returns a list of its factors in ascending order.

def factors_list(n):
    """Return a list containing all the numbers that divide `n` evenly, except
    for the number itself. Make sure the list is in ascending order.

    >>> factors_list(6)
    [1, 2, 3]
    >>> factors_list(8)
    [1, 2, 4]
    >>> factors_list(28)
    [1, 2, 4, 7, 14]
    """
    all_factors = []
    "*** YOUR CODE HERE ***"