← Schedule

HW 3: Strings, lists & file I/O

CMSC 141: Introduction to Python

Due Sunday, July 12, 11:59pm

A string is text, but it is also a sequence you can loop over one character at a time. This homework works through both views: counting and transforming characters with loops, and using string methods that do common jobs for you. The theme is passwords and simple ciphers, which are really just strings being inspected and rearranged. Toward the end you pull text in from a file on disk, which is the same kind of work with the words coming from somewhere other than a string you typed. The final group reads a file into a list of lines and works on that list, which is how most real programs get their data.

Run git pull, then cd hw3. All of your work goes in hw3.py.

What's in the hw3 folder

Once you pull, the hw3 folder has a handful of files:

Goals

What this homework builds

This homework builds S9 (strings) and gives you more practice with S4 (loops), S8 (lists), and S11 (files), which you learned this past week.

Looping over characters

A for loop over a string hands you one character at a time:

for ch in "cat":
    print(ch)     # prints c, then a, then t

Exercise 1: count_vowels

Write count_vowels(word) that returns how many vowels are in word. Loop over the characters and count the ones that appear in "aeiou". You can test membership with if ch in "aeiou":.

>>> count_vowels("banana")
3

Exercise 2: count_number_of_each

Write count_number_of_each(password) that returns a list of three counts, in this order: the number of letters, the number of digits, and the number of everything else. Use .isalpha() to test for a letter and .isdigit() to test for a digit; anything that is neither is a special character.

>>> count_number_of_each("ab3!c")
[3, 1, 1]

Return the three counts in a list, like [letters, digits, specials].

Building a new string

You can grow a string the same way you grew a list in Homework 2, by starting empty and adding on:

result = ""
for ch in "cat":
    result = ch + result    # put each new character in front
# result is "tac"

Exercise 3: reverse_loop

Write reverse_loop(word) that returns word spelled backwards, using a loop. (In Homework 6 you will write this again with recursion. It is worth remembering how the loop version feels so you can compare.)

>>> reverse_loop("recurse")
'esrucer'

Characters as numbers

Every character has a number behind it. ord("a") is 97, ord("b") is 98, and so on through ord("z") at 122. The chr() function goes the other way: chr(97) is "a". Because the letters are numbered in order, you can do arithmetic on them, which is how a shift cipher works.

Exercise 4: caesar_shift

Write caesar_shift(text, n) that shifts every lowercase letter forward by n positions in the alphabet and leaves all other characters unchanged. Shifting must wrap around, so shifting "z" forward by 1 gives "a".

The move for one lowercase character is: subtract ord("a") to get its position 0 through 25, add n, take % 26 to wrap, then add ord("a") back and convert with chr(). Characters that are not lowercase letters (spaces, punctuation) pass through untouched.

>>> caesar_shift("abc xyz!", 3)
'def abc!'
>>> caesar_shift("hello", 0)
'hello'

Exercise 5: is_strong_password

Write is_strong_password(pw) that returns True if pw is at least 8 characters long and contains at least one digit and at least one letter, and False otherwise. Loop through the characters once, noticing whether you have seen a digit and whether you have seen a letter, then combine those facts with the length check.

>>> is_strong_password("abc123de")
True
>>> is_strong_password("short1")
False
>>> is_strong_password("alllettersxyz")
False

Reading from a file

So far the text you have worked on has been a string sitting right there in your code. Real text usually lives in a file. To read one, you open it and loop over its lines:

with open("poem.txt") as f:
    for line in f:
        print(line)

The with open(...) as f: line opens the file and gives it the name f, and Python closes it for you when the block ends. Looping over f hands you one line at a time as a string, including its trailing newline. Once you have a line, it is just a string, so everything you already know about strings still applies. In particular, line.split() breaks a line into a list of its whitespace-separated words.

Exercise 6: count_words

Write count_words(filename) that opens the file with that name and returns the total number of words in it. Treat a word as anything separated by whitespace, which is exactly what line.split() gives you for a single line. Loop over the lines, and add the number of words on each line to a running total.

The provided poem.txt has four lines and 27 words, so:

>>> count_words("poem.txt")
27

From a file to a list

In Exercise 6 you looped over a file's lines and threw each one away after using it. Often you want to keep them all so you can work on them afterward. The move is to read the lines into a list, using the same accumulator pattern you used for lists in Homework 2: start with an empty list and append once per line.

There is one wrinkle. Each line you read still has a newline character ("\n") stuck on the end, and sometimes trailing spaces. Left alone, "hello\n" is not equal to "hello", which will trip up later comparisons. The string method .strip() removes whitespace, including the newline, from both ends of a string, so "hello\n".strip() is "hello". Strip each line as you store it.

For this group you are given passwords.txt, one password per line:

sunshine
p@ssw0rd
abc123de
hello
correcthorse
Tr0ub4dor
qwerty
letmein99
x7
DragonFly2024

Exercise 7: read_passwords

Write read_passwords(filename) that opens the file and returns a list of its lines, with the trailing newline stripped off each one. Start with an empty list, loop over the file, and append line.strip() each time. The other exercises in this group build on this one, so get it working first.

>>> read_passwords("passwords.txt")
['sunshine', 'p@ssw0rd', 'abc123de', 'hello', 'correcthorse', 'Tr0ub4dor', 'qwerty', 'letmein99', 'x7', 'DragonFly2024']

Exercise 8: strong_passwords

Write strong_passwords(filename) that returns a list of only the strong passwords in the file. Call read_passwords to get the list, then build a new list: loop over the passwords, and append each one for which is_strong_password (from Exercise 5) returns True. Reuse that function rather than rewriting the check.

>>> strong_passwords("passwords.txt")
['p@ssw0rd', 'abc123de', 'Tr0ub4dor', 'letmein99', 'DragonFly2024']

Exercise 9: longest_password

Write longest_password(filename) that returns the longest password in the file. Do not use Python's built-in max(). Use the same idea as largest in Homework 2: assume the first password is the longest, then loop through and replace your guess whenever you find one with a greater len(). (If two are tied for longest, returning the first one you meet is fine.)

>>> longest_password("passwords.txt")
'DragonFly2024'

Exercise 10: last_n

A slice copies a run of items out of a list. Writing items[a:b] gives you the items from position a up to but not including b, and a negative number counts from the end, so items[-3:] is the last three items (leaving off the second number means "to the end of the list"). Write last_n(filename, n) that returns a list of the last n passwords in the file, using a slice. Read the passwords into a list, then slice it.

>>> last_n("passwords.txt", 3)
['letmein99', 'x7', 'DragonFly2024']

Requirements summary

In hw3.py, all ten functions return the correct values: count_vowels, count_number_of_each, reverse_loop, caesar_shift (with wraparound, leaving non-letters alone), is_strong_password, count_words (reading from the file), read_passwords, strong_passwords, longest_password (without max()), and last_n.

Testing your work

Gradescope grades your code when you submit, but it will not walk you through every case it runs. So instead of leaving you to guess, the same checks ship with the assignment, in a file called test_hw3.py. It is a local autograder: you run it on your own machine, as many times as you like, before you ever submit. You do not edit it; you run it against your code and read what it tells you. From inside the hw3 directory, run all the tests with:

uv run pytest

That finds test_hw3.py on its own and runs every case. Each case shows up as a pass or a fail, and a fail prints the value your function returned next to the value the test expected, which is usually enough to point you at the bug.

Run it from inside hw3, not the folder above it. The file exercises open poem.txt and passwords.txt by name, so they only find those files when that is your current directory.

You do not have to write all ten functions before testing. To focus on a single function while you build, add the flags -xvk followed by its name. The -k keeps only the cases whose name contains what you type, -x stops at the first failing case so you can fix one thing at a time, and -v lists each case as it runs:

uv run pytest -xvk read_passwords

Since the file exercises build on read_passwords, get that one passing first, then move down the list. When everything passes locally, you are ready to submit.

Questionnaire

Fill out QUESTIONS-03.txt and submit it with your code. You won't be able to get an S on the homework without it.

Submission

Submit hw3.py on Gradescope under "Homework 3." Pushing your GitHub repo with a finished QUESTIONS-03.txt is sufficient to submit the questionnaire.