1

I get what it does but I don't understand why we do it? Why are we trying to except something?

For example:

    for i, value in enumerate(arr):
        try:
            result_dict[value] += 1
        except KeyError:
            result_dict[value] = 1

Why do I have to do except KeyError? I don't know why a KeyError is thrown. Why can't I just do

result_dict[value] += 1

in the first place?

What goes inside the except block for any try/catch? I get the error has to be thrown but what condition has to do inside the except block?

Sorry if my question is too dumb. I'm a beginner.

Maan Patel
  • 33
  • 1
  • 4

4 Answers4

2

Here's why. You cant add to something that is None. For example say var1 = None. I couldn't do something like var = var + 1. That would throw an error. In the example you shared the code will check if you can add 1 to value's key. If you cannot do this you assign a value to the key.

for i, value in enumerate(arr):
        try:
            #Try and add 1 to a dict key's value. 
            result_dict[value] += 1
        except KeyError:
            #Say we get an error we need to assign the dict key's value to 1.
            result_dict[value] = 1
Buddy Bob
  • 5,829
  • 1
  • 13
  • 44
2

A KeyError will crash/terminate your program if result_dict[value] doesn't exist. By using an except block, you tell the program what to do in the event of a KeyError (here, assign result_dict[value] = 1 if it doesn't exist), and then the program continues. Basically, you're avoiding a crash.

Let's say your value is "banana". If you don't have a result_dict["banana"], you can't add 1 to nothing, i.e. None += 1.

By using except KeyError, you intercept the error before it stops your program, and now you have set a key-value pair for your result_dict, rather than terminating the program.

will-hedges
  • 1,254
  • 1
  • 9
  • 18
2

TLDR

try/except blocks makes sure that your program continues running rather than ending abruptly. By including a except KeyError, we are specifically making sure that the program does not end abruptly if a KeyError occurs. result_dict[value] += 1 will throw an error if value is not a key in the dictionary, because it tries to do access a key that does not exist. The += makes the code run similarly to:

result_dict[value] = result_dict[value] + 1

and since value is not in result_dict, it is similar to saying

result_dict[value] = None + 1

which is bad.

The Non-TLDR version

When an error occurs in python, the program usually terminates abruptly and exits. It does not run any code that occurs below the part where the exception occurs. For example, take the following code. It takes 2 numbers from the user a and b, and it will output a/b

a = int(input("Enter a value for a: "))
b = int(input("Enter a value for b: "))

print("a/b is:", a/b)

If the user gives a valid input (say a=4, b=2) the program proceeds smoothly. However if the user were to give, say a = "c", then the following happens

Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'c'

And the program ends abruptly. It is perfectly valid to have the program end like that, but rarely do you want the program to end abruptly. Take the case where the user has 1000 inputs, and the last one messes up. The user will then have to restart the program and re-input all 1000 inputs again, because the program ended abruptly.

So now we introduce a try/except block. We know that converting a non-numeric character into an integer will throw a ValueError as seen in the error, so we will handle them accordingly

while True:
    try:
        a = int(input("Enter a value for a: "))
        break
    except:
        print("An error has occurred. Please input a again")

while True:
    try:
        b = int(input("Enter a value for b: "))
        break
    except:
        print("An error has occurred. Please input b again")

print("a/b is:", a/b)

So now, we take an input from the user, try to convert it into an integer and put that value into a. If it succeeds, it will proceed smoothly to the next line (break) and exit the loop. If it fails, then an exception will occur, and it will enter the except block, where it will print a helpful error message, and run the loop again (until the user enters a valid input). Now the program doesn't just terminate abruptly when it fails. It gives appropriate feedback to the user, and lets the user retry the input again.

That is a general exception block, where you just catch any exception.

But let's now say that there are multiple possible errors that could occur. Take the following code:

a = input()
b = input()

print(int(a)/int(b))
print("I am done")

Now a few errors can occur. One of them is the ValueError stated above, where the input given cannot be converted into an integer. The other error, is a ZeroDivisionError where b=0. Python doesn't like dividing by zero, hence it will throw an exception and terminate the program immediately.

So now, you want to print a special message for each type of program. How you do that is by catching specific exceptions

a = input("Enter a value for a: ")
b = input("Enter a value for b: ")

try:
    print(int(a)/int(b))
except ValueError:
    print("We cannot convert a non-numeric character to an integer")
except ZeroDivisionError:
    print("We cannot divide by 0")
except:
    print("Some unexpected error has occurred")

print("I am done")

If python was unable to convert the input into an integer, it will enter the ValueError block of the code, and print "We cannot convert a non-numeric character to an integer"

If python tried to divide by 0, then it will enter the ZeroDivisionError block and print "We cannot divide by 0"

If any other type of error occurred, it will end up in the final except block and print "Some unexpected error has occurred"

And after the exception is handled, it prints "I am done"

Note that if the final except block was not there to catch any other exceptions, then the program will not terminate nicely because the exception was unhandled, and it will not print the final statement.

Now, how does one realise what possible error can occur? That is up to practice, and once you get familiar with the errors, you can handle them accordingly. Or, purposefully make your program throw exceptions, and read the stack trace for what kind of exceptions occur. And then handle them accordingly. You don't have to handle each specific error differently, you could handle all of them in the same way.

You can read more here: Python's documentation for errors and exceptions

Amp
  • 158
  • 1
  • 2
  • 7
1

Let's start with an example

We have tenants in an apartment building who can reserve parking spots. Some tenants have no car and won't have a parking spot, but only tenants can reserve a spot. We track parking spots in a dictionary:

parking_spots = { "alice": 1, "bob": 2, "chris": 0 }

Chris doesn't have spots because he walks to work.

What should happen if Eve tries to reserve a spot?

parking_spots["eve"]

That code asks "how many spots has eve reserved?" However, another question this answers is whether Eve is a tenant of the building at all. Python represents this by having parking_spots["eve"] throw a KeyError which is not like any other value. If python didn't do this and returned 0 by default, then parking_spots["eve"] and parking_spots["chris"] would look the same.

But wait, I know that this particular dictionary isn't being used that way

Cool, that's pretty common. It's so common in fact that there are multiple ways of doing this.

Instead of

result_dict = {}
for i, value in enumerate(arr):
    try:
        result_dict[value] += 1
    except KeyError:
        result_dict[value] = 1

You can use

result_dict = {}
result_dict.setdefault(0)

result_dict += 1

Or you can use defaultdict.

from collections import defaultdict

result_dict = defaultdict(int)
result_dict += 1

Some CS Theory

There are two theoretical concepts that we could talk about here:

  • Exceptions as a control flow mechanism
  • The api of a dictionary

Exceptions for Control Flow

try/catch is style of control flow. It is simply a nicer way to think of code for certain cases. It is almost never required as long as you can use if/else, but is many times the best way to think about code.

Depending on who you talk to, exception handling can be a somewhat controversial feature: C++ - Arguments for Exceptions over Return Codes

In my opinion, it's rare that exceptions are truly the right answer if you design a system from scratch. Generally, I prefer using an Option (or more generally sum types).

However, in many cases, they are the best answer given you starting constraints.

Dictionary API

Python throwing an exception when the key isn't present is a design choice and not the only way the API could be written. Instead, it could simply return None. Many languages follow the latter approach as they consider it more safe - especially when you work in a modern typed language like Rust, Haskell, Elm, etc.

Further Reading

I would also encourage you to read Amp's answer as it covers some other details on the particulars of exception handling that may be instructive.

Mezuzza
  • 419
  • 4
  • 14