ICS 33 Fall 2025
Exercise Set 1 Solutions


Problem 1

One possible solution using only techniques we've learned so far follows.


def only_truthy(**kwargs):
    result = {}

    for name, value in kwargs.items():
        if value:
            result[f'_{name}'] = value

    return result

The way we'd approach this problem will evolve as we learn more Python as this quarter unfolds.


Problem 2

The mistake made in the original implementation boiled down to a common misunderstanding of the meaning of class attributes. When an object is missing an attribute that is defined in the object's class, the rules of attribute lookup in Python will ensure that the class attribute is found, even in the absence of the object attribute. That this is the behavior leads to the misguided use of a class attribute as a sort of default attribute for objects — "Here's what the value should be if I haven't set one in the object yet." And that's essentially what the design in this problem was attempting to do: defaulting the list of songs in each album to be empty automatically.

The problem with this technique is that it doesn't mesh well with other aspects of how Python works. In particular, the problem we have here is a combination of two things working poorly alongside each other.

Our best bet for fixing this problem is to simply assign self._songs to be an empty list in Album's __init__ method, so that every Album object has its own separate list of songs stored in an object attribute. At that point, there's little reason for the class attribute; it would best be removed.


Problem 3

A useful technique for testing these functions is to use contextlib.redirect_stdout to temporarily redirect the standard output elsewhere, just long enough to run one test, then see what the redirected output looked like. By doing that with a context manager, we're sure that we're only having an impact on our one test, rather than a global impact that might cause other aspects of a large program to behave differently than we expect.

One example of such a test might look like this.


class TestPrinting(unittest.TestCase):
    def test_printing_one_reversed_element_prints_only_the_one_element(self):
        with contextlib.redirect_stdout(io.StringIO()) as output:
            print_reversed_list(['Boo!'])

        self.assertEqual('!ooB\n', output.getvalue())

The rest of a complete solution could be written as "more of the same" — perhaps using this same class for testing both functions — though you might also consider techniques to avoid duplication of test code, such as a helper method that does the redirection, calls the function, splits the output into lines, and returns a list of them, so that each individual test case would become shorter and simpler. (One of the recurring themes we'll see in this course is the simplifying of formulaic code, with many techniques offered throughout the course for doing so. Those techniques simplify testing, just as they simplify writing the programs we're testing.)


Problem 4

Here's how we could solve this problem, using only the techniques taught so far. The choices we have when designing solutions to many problems will increase as we learn more techniques; all things in time.


def should_raise(expected_exc_type):
    return _ShouldRaiseContext(expected_exc_type)


class MissingExpectedError(Exception):
    pass


class _ShouldRaiseContext:
    def __init__(self, expected_exc_type):
        self._expected_exc_type = expected_exc_type


    def __enter__(self):
        return self


    def __exit__(self, exc_type, exc_value, exc_traceback):
        if exc_type is self._expected_exc_type:
            return True
        else:
            # Later this quarter, we'll learn how to outfit our exceptions
            # with custom error messages.
            raise MissingExpectedError

It's worth noting that _ShouldRaiseContext is defined here as a protected attribute of the module, but that MissingExpectedError is not. There's a method to that particular madness:

So, ultimately, should_raise and MissingExpectedError are the "public surface area" of our module, while _ShouldRaiseContext is an underlying implementation detail. We should endeavor to keep public parts of our modules separate from protected ones, as you've likely done in previous coursework, though we'll have to refine our sensibilities about where the line is drawn as we continue to learn new Python techniques.