DEV Community

Fredrik Sjöstrand
Fredrik Sjöstrand

Posted on • Updated on • Originally published at fronkan.hashnode.dev

Don't Try This at Work: Exception Driven Fizz Buzz

So, I was catching up on some of the talks from PyCon 2020. First, I watched Steven Lott - Type Hints: Putting more Buzz in your Fizz and then Elizaveta Shashkova - The Hidden Power of the Python Runtime. I think both talks were really interesting, however, together they inspired me to write this abomination of Fizz buzz implementation. If you haven't heard about Fizz buzz, see this Wikipedia page.

Background

So from Elizavetas talk, I learned a bit about how Python stack frames are easily accessible at runtime. Most importantly for this post, when you capture an exception using the except-keyword you can get the current stack frame from the traceback in the exception.

try:
    raise ValueError()
except ValueError as e:
    tb = e.__traceback__
    current_stack_frame = tb.tb_frame
Enter fullscreen mode Exit fullscreen mode

After experimenting a bit I also found that you can use tb.tb_next, to get the next traceback object in the chain. So, if you iterate until there is no tb_next then you have reached the bottom of the trace. This is where the error was raised.

Implementation

Now let's get to the actual implementation. First, we will define three functions, fizz, buzz and fizzbuzz:

def fizzbuzz(val):
    if val % 3 == 0 and val % 5 == 0: 
        raise ValueError()
    fizz(val)

def fizz(val):
    if val % 3 == 0: 
        raise ValueError()
    buzz(val)

def buzz(val):
    if val % 5 == 0: 
        raise ValueError()
Enter fullscreen mode Exit fullscreen mode

fizzbuzz is the only function we will call from another part of the code. Therefore, we can think of that as our entry point to this call stack. In fizzbuzz we do our first check, to see if the value is both divisible by 5 and 3 in which case we should print fizzbuzz. We will get to how we print the values in just a little bit. However, if the value didn't fulfill the condition we call fizz. Here again, the fizz-condition is checked, and if the condition fails it calls buzz which tests the buzz-condition.

Now let's see how we write this to the console:

def exception_driven_fizzbuzz(val):
    try:
        fizzbuzz(val)
        print(val)
    except ValueError as e:
        tb = e.__traceback__
        while tb.tb_next:
            tb = tb.tb_next
        print(tb.tb_frame.f_code.co_name)
Enter fullscreen mode Exit fullscreen mode

Given some value, we call fizzbuzz. This triggers the call stack we just looked at. The idea is that the function which raises the exception should have its name written to the console. This is what we do in the except-block of this function. If none of the functions in the call stack raised an error the print-function after the fizzbuzz call will run, writing just the number to the console. As no error then has been raised the except-block isn't evaluated. However, if one the functions do raise an error we will iterate until we reach the last traceback. This traceback corresponds to the function which raised the error. Therefore, we extract the stack frame from this traceback and access it's code-object (f_code) which contains a name, f_name. f_name is the name of the function or module of the code object. In this case this will be either: fizz; buzz or fizzbuzz, depending on which function raised the exception.

Let's take an example to see how this is evaluated. We call the function with val=3. This doesn't trigger an error in fizzbuzz, but it does trigger one in fizz. The program goes into the except-block, without evaluating the print in the try-block. The first value of tb is the traceback in this function exception_driven_fizzbuzz. We take one lap in the loop, tb now correspond to fizzbuzz. Then we take a final lap in the loop and tb is now fizz. From tb we then extract the name fizz and write it to the console. If, we do the same for val=2, this will trigger no error in our call stack. Therefore, just the try-block will be executed, but this time it reaches the print-function.

Now we just need to slap a loop around this and our fizzbuzz is done:

for val in range(1, 16):
    exception_driven_fizzbuzz(val)
Enter fullscreen mode Exit fullscreen mode

output:

1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
Enter fullscreen mode Exit fullscreen mode

Words of warning

I think this is a fun way to challenge yourself and learn new things about the language. However, I wouldn't do this on my next job interview if I were you.

Top comments (0)