ICS H32 Fall 2025
Exercise Set 1 Solutions


What's here?

These are solutions to a set of exercises, along with a fair amount of additional background explanation. Note that I've gone into more detail with those explanations than we would have wanted or expected students to do, but I'm using these as an opportunity to solidify your understanding; don't take this as an indication that you should be writing your answers in as much detail as I am here.


Problem 1

The specific problem that arises when running this script is the following error message:


    Traceback (most recent call last):
      File "D:\Examples\H32\problem1.py", line 17, in <module>
        run()
      File "D:\Exercises\H32\problem1.py", line 12, in run
        name = read_name()
    NameError: name 'read_name' is not defined

As is so often the case, though, the symptom and the cause aren't quite the same; the problem here isn't that the read_name function doesn't exist at all, because it appears later in the script. Debugging a problem like this requires understanding the full scope of the problem. So, what's gone wrong here?

The script begins with a definition of the run function, followed by a call to that function. That's not an absolute deal-breaker in a Python script, but where things break down here is that run calls other functions that have yet to be defined. The key is that Python executes the code in a script in the order it appears, so if a call to run appears before the creation of a function that's called by run, we'll end up with exactly this problem. This turns out to be an easy trap to fall into, which leads us to a simple and general technique for avoiding it: If you write a Python script that takes action (e.g., calling functions, as opposed to just defining them), make sure those actions appear at the bottom of the script, unless they have a really good reason to appear somewhere else. That way, no matter what part of the script they end up using, it will have been defined already, and future modifications to the script won't lead to perplexing issues like this one cropping up later; as long as we follow the "one simple rule," we won't ever have a problem like this.

The simplest fix to the script is to move the call to run to the bottom of it. That change alone is enough to get the script working as specified.


Problem 2

There are multiple reasonable ways to solve this problem, but they boil down to finding a solution to the two different parts of the problem:

  1. Extracting the characters from the input string that need to be printed, while ignoring the rest.
  2. Printing the characters, surrounded by the ^ symbols, with a newline on the end.

While one could split that into two separate functions, I'd be inclined not to, since the extraction of the characters to print can be written succintly using slice notation, and since we can iterate over a string and obtain each of its characters in sequence, leading to this short and straightforward solution.


def partial_print(s: str) -> None:
    for c in s[::2]:
        print(f'^{c}^', end = '')

    print()

Given a string s, s[::2] would mean "Starting from the beginning, ending at the end, and taking every second character," which is exactly the sequence of characters we want. Iterating over them would give us each of those characters — well, technically, one-character strings containing those characters — and we could get them printed one at a time.

Note that the end = '' parameter is necessary in the call to print within the loop, so we'll continue printing characters on the same line until after we've printed all of the characters. The last call to print, after the loop, is to print the final newline character. (We didn't have to specify a newline explicitly, since print automatically prints one, unless we say otherwise.)

Another option would be to handle the indexing oneself, which leads to a similar solution, albeit with some fiddly details left for us (checking the length of the string, indexing into the string to obtain its characters) to be implemented carefully.


def partial_print(s: str) -> None:
    for i in range(0, len(s), 2):
        print(f'^{s[i]}^', end = '')

    print()

Without using slicing or ranges, things become a little more complicated and are probably best written using a while loop instead. A for loop in Python is a great way to iterate over all of the elements of a collection of things, but if we're doing anything other than that, a different kind of loop will be the right choice.


def partial_print(s: str) -> None:
    i = 0

    while i < len(s):
        print(f'^{s[i]}^', end = '')
        i += 2

    print()

Note, too, that the formatted string literals aren't a necessity, either, but it would be necessary to use string concatenation when combining the ^ characters with each character to be printed.


def partial_print(s: str) -> None:
    for c in s[::2]:
        print('^' + c + '^', end = '')

    print()

Or, alternatively, the characters could be printed one at a time with end = '' appearing every time.


def partial_print(s: str) -> None:
    for c in s[::2]:
        print('^', end = '')
        print(c, end = '')
        print('^', end = '')

    print()

Or, as another alternative, the sep parameter could be used to eliminate the spaces that would otherwise appear between the ^ characters and each character to be printed, instead of using string concatenation, in a single call to print.


def partial_print(s: str) -> None:
    for c in s[::2]:
        print('^', c, '^', sep = '', end = '')

    print()

Finally, one could separately build the string to be printed, then print it. While this particular problem didn't call for it, the upside is that we've separated the decision about what the output format is (i.e., putting the ^ characters in the right place) from what we wanted to do with it (i.e., printing it), in case we ever wanted to do one without the other. As the programs we build become more complex, we'll want to be thinking in these terms — "keeping separate things separate," as I call it — more carefully, but it's hardly vital in this situation.


def make_partial_string(s: str) -> None:
    result = ''

    for c in s[::2]:
        result += '^' + c + '^'

    return result


def partial_print(s: str) -> None:
    print(make_partial_string(s))

The shortest solution I could think of — one way to solve the problem in a single line — involved the use of a technique that we've not yet seen called a generator comprehension, along with slice notation, a formatted string literal, and the str type's join method.


def partial_print(s: str) -> None:
    print(''.join(f'^{c}^' for c in s[::2]))

Whether it's better to write a solution like this last one, rather than one of the more straightforward loop-based solutions, is a matter of taste — sometimes, in our zeal to make something shorter, we raise the bar on how difficult it is to read and understand later, when we've forgotten the details that led us to our clever solution in the first place.

Overall, I've shown you all of these to point out that even a problem as simple as this one can be approached in a number of different ways, given the set of tools we have available to us. One of the things you're developing when you learn a new programming language is your sense of "taste," which is certainly at least partly driven by opinion (individually, within the organizations you work, and as a broader community). When I sat down to write a solution, the first one I wrote was the first one you see listed here, but that's not to say all of the others are "wrong" relative to that one; it's the first thing that occurred to me while in the headspace of writing solutions for students at this current point in their instruction.


Problem 3

  1. List. That we're uncertain about how many students' names we'll be returning makes a list a better fit. When we're best off using tuples is when we know at program design time what their structure will be (i.e., how many elements they'll contain and what the elements at each index will mean).
  2. List. We don't know at design time how many there will be, so that eliminates the possibility of using a tuple. The Fibonacci sequence is a sequence of integers, but the difference between each adjacent pair of integers in the sequence changes as we proceed through the sequence.
    • That solution answers the question from the perspective of "Where would you put the Fibonacci sequence itself?" There's an alternative way of looking at this problem, which is "What could you use while generating it?" You could certainly use a range to count the integers from 1 to n, though the spirit of the question was intended to be the former of these: What could you use to communicate that whole sequence?
  3. Tuple. Even though some points are two-dimensional and others are three-dimensional, we know that there are exactly two possibilities, and we know what each element means in each of those cases — x- and y-coordinates, if there are two of them; x-, y-, and z-coordinates, if there are three of them — so the fact that they are otherwise immutable is a benefit to us. We could check the tuple's length to determine which situation we have, in practice. (A function that expects either a two- or three-element tuple is similar to one that accepts either an int or a float; as long as we know what possibilities we'd like to support, we can always check at run time which one we have in any case.)

Problem 4

A downside of specifying types in Python is that we need a language for describing them, yet that language needs to be intricate enough to describe the full richness of what Python has to offer. In a lot of simple situations, type annotations are an unvarnished win. If we know a variable is always intended to store a string, writing : str is hardly a burden, and reading that later is purely helpful. If we know a function never returns a value, writing -> None is similarly straightforward.

So, what's not to like? Things aren't always that simple. Consider this function, written without type annotations:


def odd_indexes(x):
    return x[1::2]

What is an appropriate type annotation for this function's parameter? What is an appropriate type annotation for its return value? Let's consider the constraints on our answers to those two questions:

If you learned Java previously, it's possible that you learned about something called a generic method, which is how we tackle a problem like that in Java. A rough equivalent to our odd_indexes function might have a signature like this, when written in Java:


public <T extends Iterable<?>> T getOddIndexes(T collection)

What we're estabilshing in that signature is that the type of the return value will be the same as the type of the parameter, and that whatever that type is, there are constraints on what it might be. (Java doesn't have an analogue to Python's slicing, so I opted for Iterable<?> instead, which basically means "A sequence that can be iterated and we don't care what kind of objects are in that sequence.")

C++, meanwhile, has wrestled with this problem for decades, and its design committee fairly recently rolled out a new feature (in 2020) called concepts, intended to describe sets of types that all share some specific set of characteristics, so someone could write a function like our odd_indexes function above, allow it to work on the broadest possible variety of types, but have a clear and understandable error message when someone uses a type that isn't compatible. This turned out to be a thorny problem, particularly when trying to add it to a language that already had a decades-long history and already permitted a healthy level of flexibility, while lacking the ability to describe it formally.

Even if you've never learned anything about Java or C++ before — few of you will have learned both, and plenty of you will have learned neither — what I'm mainly trying to impress upon you is that there's complexity here. The point is that if we want type information to be specified (and especially if we want it to be enforced automatically, as it is in Java or C++), there's a tension between that desire and the desire to be able to write functions that are maximally flexible in terms of what they can safely handle; the more flexible our functions can be, the richer the notation we'll need to describe that flexibility accurately. Python allows us to write some very flexible functions indeed, but its notation (i.e., what we can write in a type annotation and what it's expected to mean) is still evolving in every new release as the community gains experience with it and finds places where it needs additional expressivity. (The open question is whether it will evolve into something too complex to be readily understandable; time will tell.)


Problem 5

There's enough complexity that this problem would probably best be divided into pieces, because we have two subproblems that we need to handle.

The second of these is an independent problem; if all we want to know is whether a line is valid, it doesn't matter where it came from. So, we should handle that in a separate function that's focused only on that narrow problem. Among other benefits, this allows us to test that function separately. The way we can detect this easily is to use the strip method on our line of text to throw away spaces, tabs, or newlines that appear on either end of the line; at that point, if there are any characters left, they're relevant, unless the first character is a # character, in which case the line entirely consists of a comment.


def is_line_of_code(line: str) -> bool:
    line = line.strip()

    return len(line) > 0 and not line.startswith('#')

At that point, we're left with a problem very similar to the "count lines in file" problem we solved in lecture and the line_count.py example from the Exceptions notes, with the main difference being that we need not to count lines that aren't considered lines of code.


def lines_of_code(script_path: pathlib.Path) -> int:
    script_file = None

    try:
        script_file = script_path.open('r')

        code_lines = 0

        for line in script_file:
            if is_line_of_code(line):
                code_lines += 1

        return code_lines
    finally:
        if script_file != None:
            script_file.close()

You may notice that we've got a try statement, but that we haven't actually caught any exceptions. This is in line with the problem's requirements, though, because what we want is two things:

  1. A failure to read the file to completion leads to the failure of the function. We don't care what kind of failure that is. For that reason, any exception can (and should) be allowed to propagate to the caller of this function.
  2. The file must be closed, even if reading the file fails at some point along the way. That's what the finally clause on the try statement ensures; it runs whether we've succeeded or failed.