 Hi, everyone. I hope I'm audible to everyone. My name is Sreeya Bansal and welcome to my talk today, where we talk about handling errors the graceful way in Python. Before starting, I think you already got introduced to me a little bit, but I'll just go over it again. My name is Sreeya Bansal and I'm currently working as a Software Engineer at Microsoft. I think this is my third PyCon I'm speaking at, virtually only this time, though I couldn't attend. So let's get right into it. So what we are going to talk about today is the very important part that every software developer here would agree that errors are the bane of a programmer's existence. Things rarely go planned in the world of software engineering. You write an awesome piece of code. You're ready to execute it. You build a very powerful machine learning model and then poof. Python throws an unexpected error, ending your hope of quick code execution. So I think it would be wise to conclude that errors are unavoidable while you are writing code. And in a way, it would be also wise to say that dealing with bugs and errors, that's what actually builds your confidence and in the long run, it also teaches you some valuable lessons along the way. Hence, this tells us the answer to our very first slide, which is why do we do error handling, right? Why is the very concept of error handling required? Now, we'll go a little bit deeper into it, right? And we'll talk a little bit in detail about what exactly error handling is, what are errors, right? How do we deal with errors, right? And what are the different situations we should keep in mind. So we know that the Python program basically terminates as soon as it encounters an unhandled error, right? And these errors can broadly be classified into two classes. First, we think of that as syntax errors. And second, we think of it as logical errors, right? Or we call it as exceptions, which is what the talk is about today. When we talk about syntax errors, these are errors which are caused by, you know, when you're properly not using the syntax, wrong way of declaring the errors, right? You don't keep in mind certain syntax, right? Or basically you're not understanding the syntax, what Python wants you to do. That's what we consider as syntactical errors, right? But then we also have something which are known as logical errors. So what happens is many times a program can result into an error after it is, you know, when it is running, right? And even though it does not have any syntax error, such types of errors, we think of it as an exception, right? They are called as runtime errors. And Python itself, Python library itself has defined a lot of built-in exceptions, right? For example, we have assertion error, we have floating point error, overflow error, name error, and the list is very long. And whenever these exception occurs, the Python interpreter basically stops the current process and passes it basically to the calling process, right? And it keeps on passing it to the calling process. The error keeps on percolating to the calling process until and unless the error is handled. And if you don't really handle that error, right? The program will simply crash. Let's take an example. Let's say you have written a program where a function A calls function B, function B calls function C. So if an error occurs in function C, right, and it is not handled in function C, it passes on to function B. And then if it's not even handled in function B, it passes on back to function A. So the error is going back to the functions which are calling those, right? And if you don't handle that error at all, a error message is simply displayed, right? And your program comes to an unexpected halt. So that is why Python, just like any other language, provides us with the provision to handle exceptions as you might have seen in other languages as well, right? And in other languages, you might know it as try and catch block, whereas in Python, I really like this actually. We call it as try and accept block, right? So try block basically allows you to write the code that is going to have error, right? If there is certain set of code that you think that might come across some errors, right? That piece of code, which is vulnerable to error, the better word to say is, is basically goes into the try block and the code, right, which, so for example, if any exception occurs inside the try block, right? The accept block is basically triggered in which you can handle the exception, right? Because as now because of this, because you are into the accept block, the code will not halt, right? It will not go into the halt state, right? But the flow of the accept block will basically take care everything that you want to take care of. So here is a small syntax representation for that. The critical operation, right? Which, like I said, is prone to error and which can raise an exception, right? It goes into the try block and then the code that handles those errors, that handles those exceptions goes into the accept block, so we thus choose what operations to perform once we have actually caught the exception, right? So there's like a block of code, the responsibility for which is just to take care of the exceptions which come in a vulnerable piece of code, right? So trying an accept, remember, goes hand in hand, okay? That is the syntax to write this, to write and to use both of them together. If you're just simply writing try block or if you're just simply using an accept block, you can face errors by choosing that kind of implementation. So let's take a very simple example. I think the best way we can go ahead is to just take examples and understand. So here, there's an example where we have a function where we are dividing two numbers, okay? So Python interpreter will basically raise an exception, right? When we basically try to divide a number with zero, that's basic mathematics. So when it does, we can take some custom action right on it and we can do something in case we encounter an error and we can handle that in the accept clause, okay? Also understand that whenever a Python interpreter basically raises an exception, it is in the form of objects, okay? So whenever you have an exception, the exception always comes in as an object. Here we have taken that object as E, okay? And it is in the form of object that basically stores the information about the exception, right? What kind of an exception it is, right? And what kind of message? Like earlier we talked about, we have so many exceptions, right? We have an overflow exception, name error exception, assertion exception. So similar type of exceptions will occurs when you will try to divide a number with zero, okay? And also with every exception, right? Every exception type. So like I said, there are a lot of exceptions and all of those exceptions basically in Python, they inherit from a base class which is exception class, right? So whatever exceptions you have, right? They all are inherited from the base exception class. So now in this example, right? When we were dividing two numbers, right? Python interpreter will basically raise like an exception over here whenever you will try to divide the, when you will try to divide 10 by zero. So the previous example that we just saw, right? It did not mention any specific exception, right? It was just a basic base exception class as you can see over here, right? Here we did not have any specific exception that we're trying to catch, right? And that is not a very good programming practice, right? As it will catch all the exceptions, right? There are a lot of exceptions and no matter what exceptions comes in your code it will just simply catch all of them, right? And it will handle every case in the same way. So no matter what kind of exception occurs it's gonna show you the same message for all of them, right? Again, that's not a very good exception practice. For every specific type of exception you should have a specific way of handling all of them, right? And with that the next thing that comes is how do you catch specific type of exceptions? So here the syntax for that is actually very simple. A tricloss can have any number of accept clauses to handle different types of exceptions. However, only one kind of, one of them will be executed, right? In case any exception occurs. So although you can associate multiple accept blocks with a single tri-block not all of them are going to be executed with the tri-block, okay? And only one of them will get actually executed. So what we do is we actually create a tuple of value. So you can, I would say catch a single exception also. For example, accept value error. You can basically expect just that kind of exception also or you can have like multiple exceptions clubbed together, right? And you can handle them together in a particular accept block as well. So in the first example you see accept value error where we are just taking care of one exception but in the next one we're taking care of two exceptions and in the last one, any error apart from the above three errors are basically taken care of in the last accept block. So this is a good practice, right? To basically catch specific type of exceptions and to handle them and to segregate those error scenarios separately, okay? And yes, you can have as many accept blocks as you can, right? You can club a lot of them together also and you can basically have one level of segregation for all exception blocks as well. So now again, let's take a look at the piece of code over here. So in this code what you will see is again we have the divide function, right? And in this code what you will see is that the exception that is being raised, it totally depends on the input that the user is going to give, okay? So if the user basically gives a value B in the input so any value of A is acceptable, right? But in the input B, if you give a value zero, right? The Python interpreter will basically raise a zero division error. So it's not even going to read the rest of the code which is the print statement, an array declaration and then a print statement again. It's not gonna read all of them, it's just simply going to go to the zero division error and your code will simply terminate at that point, right? But what if you give valid values of A and B and then, right? You come to the declaration of the array A and then you do print A of four. So in that case, you're gonna go in the different error which is the index error over here, right? Because you're trying to access element of the array, right? Which is not really valid over here, right? So an index error will be raised by the Python interpreter, right? So here every except block that you have has been defined for both the exceptions as one of them receives the exceptions of type index error and the other receives the exception of the type zero error. And the moment you come across an error, that's where you're actually going to halt the code and go into the except block. So you're not gonna go and execute the rest of the lines. Now that was about how you can use and try block, catch, except block, I'm sorry. It's still in that try, catch and except, but yeah. You can have multiple except blocks. But what if you also want to consider the scenario where you use yourself wants to raise some errors, right? So even though exceptions in Python are automatically raised in runtime, right? When some kind of an error occurs, there are ways to have custom and predefined exceptions also that can be thrown manually as well for raising it for specific conditions, right? On a scenario where you use the raise keyword. The idea being that you do not really want to wait for the interpreter to throw you an error, right? You want to make sure that your code is so strictly and well-defined that whenever an error comes up, right? You have written those certain keywords and assertions yourself where you know where you want to throw those errors. So this is kind of an optional case. You can optionally pass values also to those exceptions, right? To clarify what kind of, and why that kind of exception is being raised. The syntax for that is very simple. You have a try block again. The critical code goes into that and on specific condition, right? Or otherwise as well, if just want to test out the code, you can do raise and then after that, you can just write some error over there, right? And you can just mention a message also as in why you are raising that kind of a error, right? And in the accept log, you are going to raise that particular error that it says some error over here. And then you can just do some sort of, you know, printing statement on that or maybe how do you want to handle that piece of code? So here, if you will see again, we have a syntactical code over here. You have a try block which has a variable A which has the values one, two and three, right? And now you're calling a function is string empty. So in this piece of code, right? The variable A can hold whatever value that is assigned to it. Here, we have assigned it a number, right? And we are passing it to a custom method, which is string empty that checks whether the variable past has a string in it or is it empty or not, okay? So it could be either a string or it could be a empty string as well. But what we did was we orchestrated the code to be written in such a way that we're actually throwing a type error because we are actually assigning a number to A variable, right? So in this method, we are checking if the variable is a string or not and it holds a value or not. So in this case, it is supposed to be a string but it is assigned as a number and that is why we are raising a specific exception type error or a value error in this case, right? So what will happen is whenever you'll try to execute this piece of code, right? You're gonna come across the type error exception, right? Where in the type of A in the first if condition, the type of A is not equal to string and that is why you are raising a type error where you are saying that A has to be a string, okay? So like we talked about this in that, you do raise, you do type error, you mentioned the error that you really want to raise and after that you have the part that you are mentioning a message also because you just don't want to throw the errors, right? That's not what the objective is. You want to throw out the errors but the client should also know the relevant reason why that error is being thrown. So that's why although sending a message is completely optional but it's definitely a good practice to do that because when you're on the other end, when you're on the receiving end of errors, it can become difficult on the debugging end to understand why that error has been thrown. So as a general practice, I always recommend to throw a relevant and a readable message to the client, right? Not giving any details which are internal to the server but also making sure that the user understand all those errors and they can do relevant changes which are required so that it's easier for the client to take relevant actions on top of those errors. So that's what the overall idea is. Next, not just try and accept but Python as a language gives you a lot more functionalities on top of that as well which can actually help you to make your code more robust and you can handle multiple scenarios that come along with it. For example, in some situations, you might want to run a block of code, right? If the code inside the try ran without any errors and for those cases, you can use the else keyword also along with the try keyword, okay? Again, let's take an example. So again, the try block will have your particular keys of code, right? On some specific condition on otherwise, let's say you want to throw some error and you have send an optional message as well. Now in the accept case, you're just handling the error. So remember that the accept clause or the accept block of code only execute when there's an error. If there is no error, the accept block will never ever execute, right? It only gets executed if we have an error, if we get an error in the try block, right? And we are handling the exception in the object that we have as E and we're taking relevant actions. Apart from that, in the else block, that else block only gets executed if no exception has been raised, okay? So only when the code was exception-free, like I like to say it, then only you have your else block executed. So let's see, here we have again a piece of code in which you have a try block, you're entering a number taken by the user, and then you are asserting, right? Whether the, I would say the remainder that you got on division of that number with two is zero or not. Basically, you're trying to see whether the number is even or not. So what will happen is, if you will pass an odd number in this code, right? You will go into the accept block, okay? And you will simply get the message, not an even number. That will happen when you're passing an odd number. But if you will pass an even number, the accept block is not going to be executed because the number was indeed even, and you will move to the else block where you will see that the reciprocal of that number will be computed. So if you enter the number of four, you'll get the remainder zero, and you'll simply from the try block, you'll go to the else block. And when you will do one by four, which is for the original number, you'll get 0.25. And then you'll just print that output, right? So if there's a try block, you'll execute. If there's an exception, you'll go to the accept clause. And if there is no exception from the try block, you'll go to the else block, right? However, in this code, if you will pass zero, right? Now there's a scenario, let's say you are passing zero. So when you do zero mod two equal to zero, that's actually a zero division error, right? So the code, right, will go into the accept clause, right? And here, even in the accept clause, what will happen over here is, you will actually have a trace back over here, right? Trace back basically helps you to understand and trace back on what levels your code basically, you know, tried to give you an error and how do you trace back that error, right? So what will happen is, if you basically pass zero over here, you get a zero division error, right? As the code inside else is not handled by the preceding accept. So sometimes what happens is a code may be prone to error, right? And you're in this case, when there was a zero division error, you did not handle that in accept and you did not handle that in else as well. So in that case, you'll have a proper error where no error handling has been done. So now in another part that you have along with it is finally as well. So the try statement is Python can also have a finally clause. Now this clause is executed no matter what and is generally used to, you know, release external resources. Let's take a real example. For example, you're connected to a remote data center through your network or working with a file or like, you know, a GUI graphical user interface. In all of these circumstances, you must clean up the resources before the program comes to a halt, right? Irrespective of the fact whether your code executed successfully or not, you have to make sure that just because you came across an error, you're not still holding onto the resources. Now these actions closing a file, GUI or disconnecting from the network are performed in the finally clause to guarantee the execution of the code. Now, here is an example for that. You have, let's say a function where you're reading a file's content, right? And in the try block, you're opening the file, right? You're getting access to a JSON and you're reading that JSON, right? But in some case, let's say while opening it, you come across an error due to some server issues, right? Now what will happen is, although you are giving a relevant message to the user that you were not able to read the specified file and you are handling the exception or that error well, you also need to make sure that even after the exception has been raised, the finally block is being executed, right? Which is closing to the access to that file. Now remember, people do get confused between the finally block and the else block over here. The else block will not execute if the accept block is executing. But here in this case, the finally block will get executed no matter I would say whether you did face an exception or whether you did not face an exception. So that's very crucial when you're holding onto resources in your code and you might want to let go of them, making sure that you're not holding to the resources irrelevant of your use case. And the last one that's very important and that's very important to conclude as well is where you see all of them together finally. That is where you have try, accept, else, and finally. So in this code, you have a try block. You have an array and you're trying to access anything at the index one. If you have an accept block, right? You're creating an array over here in the three elements and that is the maximum index that it goes up is to do. But when you try to access the second index, right? Now it will not raise an exception and control will finally go to another block, right? And then to the finally block, right? But here we need to observe that finally block has been triggered even though the exception was not raised. So if you do temp of one over here, you will execute the else block and then you will execute the finally block. But if you do raise an exception, you will execute the accept block and the finally block. So finally block irrespective of whether you face error or not and accept and I would say else block does not go in hand in hand, right? So either you execute the accept clause or you execute the else clause, right? So these all basically in a nutshell form a very important part of how do you really want to error I would say handle the scenarios, right? Using try and accept is just the way you get started into this journey, right? But what if you wanna go one level deeper into it, right? Another scenario that you might consider is that what happens if the errors are basically raised in accept or finally block, right? What if you've written some piece of code, right? And the error actually comes into the finally block or the accept block. Let's say you're doing something to handle the error in the accept block and the error happens in the accept block itself, right? So if an error occurs while you're actually handling something in the accept block itself, the finally block is still there to save you, right? So you do an error in try block the accept block is there for you. But what if you do error in the accept block itself then you also have finally block to basically take care for you, right? Now over here, once an exception or an error is raised in the accept block, the finally block is triggered, right? But the program still goes into a halt state, post to that and the flow will still get broken. Finally block will help you to raise relevant, I would say or help you to relevantly show the error to the user, but the program will still go into a faulting state. But if an error occurs in the finally block, right? What will happen over here is during handling, they're also in the finally block you can raise an exception, right? And it will still show you the finally error. So the finally block won't be completed beyond this point, right? And the exception is still thrown, right? So that's what the overall idea is. I hope that that was useful for all of you to consider different scenarios. Again, the idea being is that you start small but as soon as you realize that your complexity of the code is increasing, right? When you're dealing with objects on a very frequent basis, right? You're dealing with a scenario where you're coming across errors very frequently. You're coming across a situation where there's a situation, you know, you're trying to place an order, you're building an e-commerce system, you're trying to check out the items in the cart, right? And there's some error but you still want to show relevant messages to the users so that they can basically get their order ID, right? And whether it's taking time to process that order, customers do have relevant order IDs or some values or booking reference numbers available during their checkout scenarios, right? They do have that order ID using which they can raise relevant query, right? Even if your booking was successful or failed, right? Or partially failed or some error happened but the checkout experience is smooth so that the people do have a relevant order ID using which they can generate relevant errors. That happens a lot when you're booking and making reservations online, right? You do make a reservation but it's taking some time to the partner to confirm your order and to confirm that actually you've received the relevant booking as well. So at that time, you do still get a reference number, an order ID that is created irrespective of your booking that happened successfully or unsuccessfully. So in those situations, it makes a lot of sense to have an Excel block or finally block so that it's comfortable for the users as well. The clients as well. The server, the maintainers and the software engineers as well as well as the partners who are dealing with it. And that concludes my talk for today as well. I hope you did find it useful and you would be able to incorporate it all as well in the future engineering practice that you do take care of. Thank you so much, everyone. Thank you very much, Ria Banzal for this talk and with this, we will close the session and let's give her thanks again for this great talk and see you during the next conference. Bye, everyone. Thank you so much.