Here are our solutions for the day 24 exercises in the 30 Days of Python series. Make sure you try the exercises yourself before checking out the solutions!

### 1) Ask the user for an integer between 1 and 10 (inclusive). If the number they give is outside of the specified range, raise a `ValueError` and inform them that their choice was invalid.

First things first, we need to actually get some user input and convert it to an integer. We haven't been told to handle any exceptions that arise at this step, but you can if you want to.

``````number = int(input("Please enter a number between 1 and 10: "))
``````

In order to check the number is within a specified range, we could use a condition like this:

``````number = int(input("Please enter a number between 1 and 10: "))

if number > 10 or number < 1:
pass
``````

Alternatively, we can just define a range and see if the number is in that range:

``````number = int(input("Please enter a number between 1 and 10: "))

if number not in range(1, 11):
pass
``````

In either case, we want to raise a `ValueError` inside the body of the conditional statement, like so:

``````number = int(input("Please enter a number between 1 and 10: "))

if number not in range(1, 11):
raise ValueError(f"The number must be between 1 and 10. You entered {number}.")
``````

### 2) Below you'll find a `divide` function. Write exception handling so that we catch `ZeroDivisionError` exceptions, `TypeError` exceptions, and other kinds of `ArithmeticError`.

The function we've been given looks like this:

``````divide(a, b)
print(a / b)
``````

First, we need to handle any `ZeroDivisionError` exceptions, which we can do like so:

``````divide(a, b)
try:
print(a / b)
except ZeroDivisionError:
print("Cannot divide by zero")
``````

Next we need to catch more general `TypeError` exceptions, which we can do by adding another `except` clause.

``````divide(a, b)
try:
print(a / b)
except ZeroDivisionError:
print("Cannot divide by zero")
except TypeError:
print("Both values must be numbers. Cannot divide {a} and {b}")
``````

Finally we need to catch general `ArithmeticError` exceptions.

The other of this final `except` clause is important. Make sure you don't place it before the `ZeroDivisionError` branch, because all instances of `ZeroDivisionError` are instances of `ArithmeticError`. Remember that Python is going to check the `except` clauses one at a time in order, and it will use whichever matching branch it finds first.

``````divide(a, b)
try:
print(a / b)
except ZeroDivisionError:
print("Cannot divide by zero")
except TypeError:
print("Both values must be numbers. Cannot divide {a} and {b}")
except ArithmeticError:
print("Could not complete the division. The numbers were likely too large")
``````

### 3) Below you'll find an `itemgetter` function that takes in a collection, and either a key or index. Catch any instances of `KeyError` or `IndexError`, and write the exception to a file called `log.txt`, along with the arguments that caused this issue. Once you have written to the log file, reraise the original exception.

``````def itemgetter(collection, identifier):
return collection[identifier]
``````

First things first, I'm going to write another function called `log_exception` which is going to take care of writing to the file. This is going to make our exception handling a little neater, because we won't have context managers embedded in `except` clauses, etc.

``````def log_exception(exception, fn, **kwargs):
values = ", ".join(f"{key}={value!r}" for key, value in kwargs.items())

with open("log.txt", "a") as log_file:
log_file.write(f"Exception: {exception}, Function: {fn}, Values: {values}\n")
``````

Everything here we've seen before, but let's quickly go over it.

Our function has three arguments: `exception`, `fn`, and `**kwargs`. `exception` is going to take in an exception object, which we're going to grab by naming the specific exception we catch. `fn` is going to be a string representing the function name, so that we can determine where the exception happened. `**kwargs` is going to collect any arguments that were passed into that function, and the parameters the values where assigned to.

On the first line of the function body we go through the values in `**kwargs` and we produce a string representing the keys and values as keyword arguments. For the `value` we have this special `!r` which is a way of ensuring we get a representation that matches the way a value appears in code, rather than how it appears to the users if we print them. This means we can distinguish between `4` and `"4"` in our logs.

Once we have the values, we join them together with commas, and then we write a line to our log file in append mode (`"a"`).

Now let's get to the actual exception handling. In this case we want to do the same thing for both exceptions, so we have some options. We can either catch the broader `LookupError`, or catch both `IndexError` and `KeyError`. In this case, I'm going to go for the latter.

``````def log_exception(exception, fn, **kwargs):
values = ", ".join(f"{key}={value!r}" for key, value in kwargs.items())

with open("log.txt", "a") as log_file:
log_file.write(f"Exception: {exception}, Function: {fn}, Values: {values}\n")

def itemgetter(collection, identifier):
try:
return collection[identifier]
except (IndexError, KeyError) as ex:
log_exception(ex, "itemgetter", collection=collection, identifier=identifier)
``````

Now we just need to reraise the exception using the `raise` keyword.

``````def log_exception(exception, fn, **kwargs):
values = ", ".join(f"{key}={value!r}" for key, value in kwargs.items())

with open("log.txt", "a") as log_file:
log_file.write(f"Exception: {exception}, Function: {fn}, Values: {values}\n")

def itemgetter(collection, identifier):
try:
return collection[identifier]
except (IndexError, KeyError) as ex:
log_exception(ex, "itemgetter", collection=collection, identifier=identifier)
raise
``````

One thing we can do to make this a little better is also handling `TypeError` exceptions, since this can happen when we pass in a non-integer as an index for a sequence.

``````def log_exception(exception, fn, **kwargs):
values = ", ".join(f"{key}={value!r}" for key, value in kwargs.items())

with open("log.txt", "a") as log_file:
log_file.write(f"Exception: {exception}, Function: {fn}, Values: {values}\n")

def itemgetter(collection, identifier):
try:
return collection[identifier]
except (IndexError, KeyError, TypeError) as ex:
log_exception(ex, "itemgetter", collection=collection, identifier=identifier)
raise
``````

### Note

We don't actually do logging like I've shown here in this exercise, because we don't have to. Python has a `logging` module which does a lot of the work for us, and produces more informative logs.

You can find information and getting started tutorials on logging here.