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.
self._songs
, which means that Album
objects never have their own _songs
attribute. Consequently, any time we access the value of self._songs
, we're accessing the same class attribute, no matter what Album
it is.self._songs.append(song)
in the add_song
method, we were appending an element to the existing list. Since that list was accidentally being shared amongst all Album
objects, adding a song to one album instead added that song to all albums.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:
should_raise
function does not need to rely on any of the details of how context management has been implemented, other than the effect we want: a MissingExpectedError
exception being raised or not, depending on whether an exception of the expected type was raised while in the context.should_raise
function can expect a MissingExpectedError
exception to be raised, so there might be some benefit in being able to catch it and know what it is. (One reason is so that we could write automated tests of our should_raise
function.) 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.