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.
Once you pull, the hw3 folder has a handful of files:
hw3.py is where you write your answers. It has ten blocks,
one per exercise, each starting with def. Run it directly with
uv run python hw3.py to try your functions out.test_hw3.py is the autograder. You run it against your code
but never edit it. More on this in "Testing your work" below.poem.txt and passwords.txt are the text files
the file exercises read from.QUESTIONS-03.txt is a short reflection you fill in and
submit alongside your code.pyproject.toml and README.md come with the
folder too. The first configures uv, our Python package
manager; the second lists what each file is. You don't edit either one..isalpha(), .isdigit(),
and .isupper().ord() and
chr().This homework builds S9 (strings) and gives you more practice with S4 (loops), S8 (lists), and S11 (files), which you learned this past week.
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
count_vowelsWrite 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
count_number_of_eachWrite 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].
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"
reverse_loopWrite 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'
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.
caesar_shiftWrite 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'
is_strong_passwordWrite 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
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.
count_wordsWrite 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
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
read_passwordsWrite 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']
strong_passwordsWrite 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']
longest_passwordWrite 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'
last_nA 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']
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.
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.
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.
Submit hw3.py on Gradescope under "Homework 3." Pushing your
GitHub repo with a finished QUESTIONS-03.txt is sufficient to
submit the questionnaire.