Over the weekend, I had a TIL moment with Python exceptions. I’m writing a demo app that is a random number generator and I have this function:
def random_number_generator(limits: RandomNumberLimits):
if limits.duplicates:
return random.choices(
range(limits.min, limits.max + 1), k=limits.count)
return random.sample(
range(limits.min, limits.max + 1), k=limits.count)
The RandomNumberLimits dataclass has four fields:
@dataclass
class RandomNumberLimits:
min: int = 0
max: int = 100
count: int = 1
duplicates: bool = False
The random.choices function from the Python Standard Library will select k random items from the population (the range of numbers) with replacement, meaning it will allow duplications. The random.sample function does the same except the items selected must be unique so duplicated are prevented.
Everything works the way it should, as long as the minimum value in the limits is less than the maximum value. If it is not, the random.sample function will raise a ValueError with a message informing us that the size of the sample (k) cannot be negative or greater than the size of the population. The reason for this error message is that a range where the minimum is greater than the maximum is empty and thus has size of zero. You can’t take any items from an empty range as there is nothing to take so the error message is correct. However, the cause of the error message is the minimum value being greater than the maximum value. Therefore, we need to except the exception and provide an appropriate message.
try:
random.sample(range(limits.min, limits.max), k=limits.count)
except ValueError:
if limits.min > limits.max:
print(f"The minimum value ({limits.min}) must be less than the maxmimum value ({limits.max})")
else:
print(f"The sample size ({limits.count}) must be non-negative.")
So if you try to run the random_number_generator function, and the minimum is greater than the maximum, you’ll see the first error message … if the duplicates field is False. If the duplicates field is True, you’ll see a raised IndexError has not been handled. So the two functions raise different exceptions for the same conditions and neither error message is helpful. But the same exception handling code is relevant for both functions. So this implementation is valid:
try:
if limits.duplicates:
random.choices(range(limits.min, limits.max), k=limits.count)
else:
random.sample(range(limits.min, limits.max), k=limits.count)
except ValueError:
if limits.min > limits.max:
print(f"The minimum value ({limits.min}) must be less than the maxmimum value ({limits.max})")
else:
print(f"The sample size ({limits.count}) must be non-negative.")
except IndexError:
if limits.min > limits.max:
print(f"The minimum value ({limits.min}) must be less than the maxmimum value ({limits.max})")
else:
print(f"The sample size ({limits.count}) must be non-negative.")
This is a lot of code to repeat making this implementation sopping wet it terms of DRY or “don’t repeat yourself”. It would be nice if we could use the same except block for both exception types. if we put them in a tuple it will work!
try:
if limits.duplicates:
random.choices(range(limits.min, limits.max), k=limits.count)
else:
random.sample(range(limits.min, limits.max), k=limits.count)
except (ValueError, IndexError):
if limits.min > limits.max:
print(f"The minimum value ({limits.min}) must be less than the maxmimum value ({limits.max})")
else:
print(f"The sample size ({limits.count}) must be non-negative.")