More Exceptions

Instead of handling all errors, we can just handle the TypeError we saw above:

def add(self, item):
  try:
    self.tree.add(item)
  except TypeError:
    pass

This way, we’re only ignoring ~TypeError~s.

We’re still just failing silently if we get multiple types of values, though. We really want to raise an exception–it should just be a more informative exception than the one we see here! We can do something like this:

def add(self, item):
  try:
    self.tree.add(item)
  except TypeError:
    raise TypeError("Values in a TreeSet must all have the same type")

Now our caller will still get an error message, but it will be much more helpful!

Testing exceptions

If your code raises exceptions (either via your own raise statements or, for instance, because it might divide by zero), you should test those error cases. (In my experience, error-handling code is frequently riddled with bugs–it doesn’t run in normal operations, so bugs don’t get noticed and fixed! Always test your error-handling code.) pytest lets you do this:

from tree_set import *
import pytest

def test_add():
    s = TreeSet()
    s.add(1)
    with pytest.raises(TypeError, match="same type"):
        s.add("1")

Exceptions and the “call stack”

Let’s say we have a few functions defined:

def divide(x, y):
  return x / y

def average(lst):
  return divide(sum(lst), len(lst))

def variance(lst):
  try:
    avg = average(lst)
    avg_2 = average([x * x for x in lst])
    return abs(avg * avg - avg_2)
  except ZeroDivisionError:
    return 0

What happens when we call variance([])? See the lecture capture for details.

Polymorphism example: Python’s special methods

With Python’s built-in sets, lists, and hashtables, we can check if an element is in the collection with in:

l = [1, 2, 3]
2 in l # => True
4 in l # => False

With our TreeSet, though, we have to use the contains method. This doesn’t seem fair!

Luckily, Python’s in is actually implemented by calling a special method on the container object. We can add an implementation like this:

# on the TreeSet class
def __contains__(self, item):
  return self.contains(item)

Now we can do this:

s = TreeSet()
s.add(1)
s.add(2)
1 in s # => True
3 in s # => False

Python uses this kind of special method for a lot of things. Even things like addition using the `+` operator are actually implemented using these special methods. This is a classic example of polymorphism. Python doesn’t need to know anything about how a TreeSet is implemented: it just has to call its `_contains__` method.