Python Forum

Full Version: Type conversion and exception handling (PyBite #110)
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
PLEASE NOTE: This is a school assignment so dear fellow Pythonistas and forum members: Please provide as many tips and hints as possible without providing a solution until I am able to come up with a complete solution on my own.

Here is my assignment:

Quote:https://codechalleng.es/bites/110/

In this Bite you complete the divide_numbers function that takes a numerator and a denominator (the number above and below the line respectively when doing a division).

First you try to convert them to ints, if that raises a ValueError you will re-raise it (using raise).

To keep things simple we can expect this function to be called with int/float/str types only (read the tests why ...)

Getting passed that exception (no early bail out, we're still in business) you try to divide numerator by denominator returning its result.

If denominator is 0 though, Python throws another exception. Figure out which one that is and catch it. In that case return 0.

My teacher has provided some further instructions:

Quote:For this exercise you can assume numerator and denominator are of type int/str/float. Try to convert numerator and denominator to int types, if that raises a ValueError, reraise it. Following, do the division and return the result. However if denominator is 0 catch the corresponding exception Python throws (cannot divide by 0), and return 0

Here is the unit test provided:

import pytest

from division3 import divide_numbers


@pytest.mark.parametrize("numerator, denominator, expected", [
   (1, 2, 0.5),
   (8, 2, 4),
   # strings that look like ints are converted (casted) fine
   ('3', '2', 1.5),
   # floats work too but when casted to int they are rounded down!
   (8.2, 2, 4),
   (1, 2.9, 0.5),
])
def test_divide_numbers_good_inputs(numerator, denominator, expected):
   assert divide_numbers(numerator, denominator) == expected


@pytest.mark.parametrize("numerator, denominator", [
   # ignoring dict/set/list to keep it simple, those would actually
   # throw a TypeError when passed into int()
   (2, 's'),
   ('s', 2),
   ('v', 'w'),
])
def test_divide_numbers_raises_value_error(numerator, denominator):
   with pytest.raises(ValueError):
       divide_numbers(numerator, denominator)


def test_divide_by_zero_does_not_raise_zero_division_exception():
   assert divide_numbers(10, 0) == 0
Passing the first assertion test is very easy. Just by adding the line return int(numerator) / int(denominator) to the divide_numbers() function converts the string numbers into integers.
So this is the basic function that I am starting with:

def divide_numbers(numerator, denominator):
    return int(numerator) / int(denominator)
That passes the first unit test function with flying colours. Easy.

What I am struggling with is the following two assertion checks, specifically handling exceptions. For example when ('s', 2) is passed into the function, since the first variable is a string and can't be converted to an int, I need to handle it by invoking an exception. I’ve made a few different attempts placing exceptions in different places and in different order. This is what I think is my strongest attempt so far:

def divide_numbers(numerator, denominator):
   if denominator == 0:
       raise ZeroDivisionError('cannot divide by 0')
   elif numerator or denominator == str:
       raise ValueError('cannot be string')
   else:
       return int(numerator) / int(denominator)
The first obvious problem I can already identify is at lines 4 and 5 because when ('3', '2', 1.5) are passed into the function, ‘3’ and ‘2’ are exited before reaching lines 6 and 7 and so are not converted into a string. To fix this issue I know I need to add further control with more conditionals but I can’t for the life of me find the right arrangement. That’s just one problem. The other problem is at lines 2 and 3. I am not raising the ZeroDivisionError properly. When I try change the == equality operator at line 2 to the = assignment operator, I get a syntax error and none of the assertions even process, so the unit test fails completely.

The first doc I found on Google related to zero division in Python was from a chapter titled Python “Programming / Exceptions” under the sub-section titled: “Catching exceptions”. It’s slightly outdated. Even though it’s written for Python 2, I found it to be easy to follow. To adapt the guide for Python 3, I referred to the official Python doc on porting Python 2 code to Python 3, specifically the section on “Capturing the Currently Raised Exception”. I also used Sebastina Raschka’s “The key differences between Python 2.7.x and Python 3.x with examples” on “Handling exceptions”. I also found a detailed guide on ZeroDivisionErrors titled “Python Exception Handling – ZeroDivisionError”. However it was verbose and hard to follow. I found Dan Bader + Said van de Klundert's guide on Python Exceptions to be enormously helpful as well.

What might you people recommend I explore next? Again, I am looking for tips and hints. Please do not complete the assignment for me.
I think you dislike letting your program generate exceptions. The "Pythonic" way to write code is assume everything will work and catch any exceptions that may occur. Testing for every possible error before doing anything results in lengthy code that is hard to read, brittle, and error prone. If you need any proof, just look at what you have written. You input test is completely wrong. If you used exceptions your input test would be guaranteed correct because there would not be an input test.

This assignment should force you to change your ways. The assignment explicitly states that you use exceptions and what you should do when exceptions occur. You should convert the inputs to integers and catch the exception that may occur. You should do the division and catch the exception that may occur. Nowhere in your code should you be testing anything. The only testing should be the unit test.
Thank you @deanhystad for your feedback and your patience. Although I didn't quite understand what you mean.

I re-read the course assignment more carefully. Based on each sentence outlined by the assignment instructions found in my quote block in my original post, in the end (after a few progressive iterations) I arrived at this working pseudo code:

 def divide_numbers(numer_arg1, denom_arg2):
   try to:
       convert numer_arg1 to integer
       convert denom_arg2 to integer
   in the event of the above casting failure:
       if due to string contents:
           raise ValueError
   try to:
       calculate division operation from function args
   if the above attempt fails with a ZeroDivisionError:
       return 0
   return result
Using that pseudo code I came up with this script:

 def divide_numbers(numerator, denominator):
   try:
       int_numer = int(numerator)
       int_denom = int(denominator)
   except:
       if numerator or denominator == str:
           raise ValueError('Cannot be string.')
   try:
       result = int(numerator)/int(denominator)
   except ZeroDivisionError:
       if denominator == 0:
           return 0
   return result
Here is the output of the unit test:

Quote:python -m pytest test_division.py
test session starts
platform linux -- Python 3.8.3, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/<user>/dev/projects/python/2018-and-2020/bitesofpy/<path>
collected 9 items
test_division.py [100%]
9 passed in 0.01s

Eureka! I completed my assignment!

Since I didn’t really understand your reply, @deanhystad, is the script that passed the test the right way or wrong way? Is there a way of catching the exceptions that is more Pythonic than I did? Did I test it right? How might you improve my script?
Close but your program contains multiple logic and programming errors.

As I said, you should not be doing any testing, including testing if the numerator or denominator are strings or if the denominator is zero. Python will do that testing for you and raise an exception.

Why are you testing if the numerator or denominator are strings. '1' is a string, but int('1') is valid Python. What if I mistakenly input [1] as the numerator? I know it isn't one of the test cases, but that just means the unit test is bad. Does your exception handler catch that error? Does your exception reporting make any sense for that case? What kind of exceptions can be raised by int()? If you use int() you should know.

Some comments about the program
def divide_numbers(numerator, denominator):
  try:
      int_numer = int(numerator)  # Why not numerator = int(numerator)?
      int_denom = int(denominator)
  except:  # Too generic.  What type of exception?
      if numerator or denominator == str:  # Does not do what you think and is not needed
          raise ValueError('Cannot be string.')
  try:
      result = int(numerator)/int(denominator)  # Why not using int_numer and int_denom?
  except ZeroDivisionError:
      if denominator == 0:  # Why this test?
          return 0
  return result
Try to be consistent. This code is inconsistent in how it returns the result:
  try:
      result = int(numerator)/int(denominator)  # Either use return here
  except ZeroDivisionError:
      return 0  # or use result here.  Don't mix and match 
  return result
And this is a bad practice:
    except:
        raise ValueError('Cannot be string.')
This will raise an exception, but all I get for an error is 'Cannot be a string'. What cannot be a string? Where did this error happen? If you are going to catch and then re-raise an exception, do it like this:
    except ValueError as value_err:
        # Do error processing here
        print('Numerator and denominator must be numbers')
        raise value_err  # This will provide a complete error message and traceback
When you are ready I can show you a short and concise version of this function if you like. And then others can critique my code (fun, fun).
Based on your feedback, I have cleaned up my script a little. Here are the changes I’ve made:
  1. Replaced int_numer = int(numerator) with numerator = int(numerator) (and did the same with the denominator)
  2. Specified the ValueError exception at line 5 so that it is less generic
  3. Removed the conditional which was supposed to check if the numerator or denominator is a string
  4. Removed the conditional check when denominator == 0
  5. Returned the quotient within the second try block rather than outside.

With all of the above changes, here is what my script looks like now:

def divide_numbers(numerator, denominator):
    try:
        numerator = int(numerator)
        denominator = int(denominator)
    except ValueError as value_err:
        print("Numerator and denominator must be numbers. Can't use letter characters")
        raise value_err 
    try:
        return int(numerator)/int(denominator)
    except ZeroDivisionError:
        return 0
Is this an improvement, @deanhystad?

The instructor provided this as the solution:

def divide_numbers(numerator, denominator):
    try:
        return int(numerator)/int(denominator)
    except ValueError:
        raise
    except ZeroDivisionError:
        return 0
This is quite elegant and concise compared to my script which had a lot of unnecessary conditionals.

@deanhystad, you explained: "you should not be doing any testing, including testing if the numerator or denominator are strings or if the denominator is zero."

I'm curious: How would you rewrite the script without using the try/except keywords (without doing any testing, as you say)?

Thanks again, deanhystad, for your guidance and sharing best practices
Your original function with try..except
def divide_numbers(numerator, denominator):
    try:
        return int(numerator) / int(denominator)
    except ZeroDivisionError:
        return 0
Without try...except you have to check the denominator.
def divide_numbers(numerator, denominator):
    if denominator == 0:
        return 0
    else:
        return int(numerator) / int(denominator)
and if you want to have a compact form:

def divide_numbers(numerator, denominator):
    return 0 if denominator == 0 else int(numerator) / int(denominator)
PS: If you don't catch a ValueError, you don't need to re-raise it.