ICS H32 Fall 2025
Exercise Set 3 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

While the usefulness of namedtuples and dictionaries intersect somewhat, they each excel at slightly different things.

  1. There are (at least) a few advantages that namedtuples provide that dictionaries don't.
    • Namedtuples are not all the same type, even though they all have similar characteristics. If you ask the type of a namedtuple, you'll get a specific type, such as Person or Point, rather than a non-specific type, such as namedtuple, which means it's possible at run time (and in your mind while you write a program) to distinguish between different kinds of namedtuples.
    • Different types of namedtuples know what the names of their necessary fields are, which means we can't create them without specifying values for all of those fields, and that we can't specify values for fields that aren't part of the type. In other words, a lot of what namedtuples provide are restrictions rather than flexibility; while restrictions can get in our way when we apply them to circumstances where we need flexibility, when we know exactly what values are supposed to be included in, say, a Point, it's worth allowing our program to fail fast when we try to create a Point that has those values missing or has values that shouldn't be there.
  2. There are (at least) a few advantages that dictionaries provide that namedtuples don't.
    • The most straightforward way to obtain the value one of a namedtuple's fields is to put the field's name into the program's code (e.g., by saying something like p.x, where the field's name is x). This means that we need to know those names at the time we're designing our program, whereas we can decide at run time what a dictionary's keys should be.
    • The keys in a dictionary are not required to be strings; other immutable types, such as int, can be used instead.
    • Dictionaries are mutable, which is to say that we can add keys and values to them during the course of their lives. While that might be a problem if we had a point in three-dimensional space, it would be a necessity if we were maintaining a collection of every student enrolled in a course, keyed by their student ID, since students can enroll and drop while the course is in progress.

Perhaps this is a good way to sum up this issue: While you could use a dictionary for anything for which a namedtuple could be used, you're better off using the right tool for the right job. Many times, the right tool for a job is the one that not only does all that you need, but also does only what you need.


Problem 2

The testing requirement imposes what may have seemed like a burden, but is actually a long-term benefit; you had to write this function in a way that it was testable. Unfortunately, there's not a straightforward way to use assert statements to test a function that prints to the Python shell, so it became necessary to divide this problem into its component parts. Paradoxically, the more carefully you did that job, the easier it was to test those parts. It may have seemed uncomfortable to be thinking about your solution in that way, if you're not accustomed to doing it, but the ability to write many small functions instead of fewer larger ones is the key that unlocks your ability to write substantially larger programs than the ones we've been assigning.

There are probably a lot of good ways to break this up, but this is the most carefully divided version I came up with. If you're wondering "How did you end up here?", the answer is "incrementally." I started solving the problem, calling functions that didn't exist yet when I recognized a new and separate problem to solve, then started working on that new and separate problem, continuing until I had no problems left to solve.

There are a couple of things I should point out about this solution.


Problem 3

As with my solution to Problem 2, the way I approached this problem was incremental, gradually adjusting what functions I had written until I had virtually everything in a form where it could be tested. The way I started my solution is not what it looked like when I finished, and that's normal; the sooner you let go of the idea that you'll always know exactly what to write and always make precisely the right design decisions every time, the sooner you'll be able to write larger programs without feeling overwhelmed. If we're careful about how we design it, software is quite malleable; where it begins to harden is where we haven't tamed its complexity.

There are a couple of things I should point out about this solution.


Problem 4

  1. Servers — or, more specifically, the owners of servers — generally have a lot more to lose than clients do. When a connection is first established, it's not possible for either program to know the identity of the other. But if a client obtains access to information that it's not authorized to access, that's a problem for the (owner of the) server, but a boon for the (person running the) client. That a server expects a client to start by identifying what it wants (e.g., what protocol we'll be using) — as well as perhaps other things, such as something that demonstrates their right to access to the information they'll be seeking — is a common-sense way to make it more difficult for a client to obtain information it shouldn't. (Think about it this way: When your phone rings and you think the call might be a spammer, but you pick up the phone anyway, do you say anything substantive, or do you wait for the caller to identify itself? I usually do the latter, because I have more to lose than the caller does.)
  2. Servers sometimes support multiple protocols, or, at the very least, multiple versions of the same protocol. But servers (and clients) are programs running isolated from one another, which means anything each side knows about the other needs to be sent as part of the protocol. Given that, even though a server may want to allow different clients to speak to it differently, it can't know what the client is able to do, so it's best to let the client begin that negotiation by saying "I'd like to use version 3 of the XYZ protocol," at which point the server can determine whether it'll be able to do that.

Problem 5

The _read_line, _expect_line, and _write_line functions are all tools that are written specifically to make it easier to write the polling module itself. In other words, they aren't the tools that the module is meant to provide to code in other modules; they're tools that make it easier to build those bigger tools.

In particular, the entire goal of the polling module is to hide the details of the implementation of the Polling protocol. While the public functions mirror the interactions that are part of the protocol, none of the details of how those interactions work, behind the scenes, are noticeable to code in modules outside of polling.py. All the rest of the program needs to know are things like "Given a host and a port, we can connect and get back an object representing that connection, which, if we pass it to other public functions, will let us ask the server questions" and "If we ask the server for the questions, we'll get back a list of them," without needing to rely on any of the details of how any of that stuff works. If I completely redesigned the details of the Polling protocol (i.e., what gets sent back and forth between the client and server), but maintained the same basic interactions (i.e., there's still a way to ask the server the same questions we could before), then the functions in polling.py would need to be rewritten, but none of the rest of the program would need to be touched.

That's why _read_line, _expect_line, and _write_line are clearly on the "protected" side of the line, ultimately; they're functions that implement the details of sending and receiving lines of text, which are a behind-the-scenes detail of how the protocol works, which is exactly what the polling module is intended to hide from the rest of the program.

(If the word "hide" seems strange there, you'll likely see the phrase "information hiding" again in your travels in computing, as it's one of the central tenets of software engineering. In the context of a large and complex program, the idea that parts of the program hide details from other parts is a good thing; a hidden detail is one that no other part of the program can depend on, which means that it can be changed without affecting any other part of the program.)