How to properly handle bugs in Python: from try-except-finally to catch all exceptions

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Exception Handling in Python: A Comprehensive Guide

Errors are an inevitable part of software development. Even the most meticulously written code can encounter unforeseen circumstances: a missing file, division by zero, network connection issues, and so on. To prevent a program from crashing in such situations, Python provides a powerful exception handling mechanism based on the try-except-finally construct.

How Does the try-except-finally Construct Work in Python?

The try-except-finally construct allows you to catch exceptions that occur during program execution and ensure a proper response to them.

General syntax:

try:
    # Code that may raise an exception
except SomeException:
    # Exception handling code
finally:
    # Code that will execute in any case

Explanation of the blocks:

  • try: This block contains the main code that may potentially raise an exception.
  • except: This is where you specify what to do if an exception occurs in the try block. You can specify the specific type of exception (SomeException) that you want to catch.
  • finally: The finally block always executes, regardless of whether an exception was raised in the try block. It is commonly used for releasing resources, such as closing files, network connections, or database connections.

Example of using try-except-finally:

try:
    number = int(input("Enter an integer: "))
    result = 10 / number
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")
except ValueError:
    print("Error: Enter a valid integer.")
finally:
    print("Program execution completed.")

How this code works:

  1. The program prompts the user to enter an integer.
  2. If the user enters 0, a ZeroDivisionError exception is raised, and the corresponding except block is executed.
  3. If the user enters a non-integer, a ValueError exception is raised, and the corresponding except block is executed.
  4. The finally block executes in any case, printing a message indicating that the program has finished.

Why is the finally Block Needed?

The finally block ensures that important code is executed regardless of whether an exception occurred. Here are some common use cases:

  • Closing files:
file = open("data.txt", "r")
try:
    content = file.read()
    # Processing the file content
finally:
    file.close()  # Guaranteed file closure
  • Releasing resources: Releasing network connections, database connections, and other resources that must be closed or released after use.
  • Completing logging or cleaning up temporary data: The finally block can be used to complete writing to a log file or to delete temporary files created during program execution.

Handling Multiple Exceptions

Multiple different types of exceptions can occur in a single try block. Python offers several ways to handle this situation:

Using Multiple except Blocks

try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(result)
except ZeroDivisionError:
    print("Error: Division by zero!")
except ValueError:
    print("Error: You did not enter a number!")

Each exception is handled separately, which provides more precise control over the program's behavior.

Handling Multiple Exceptions in One Block

If the same handling is required for multiple exceptions, they can be combined into one except block:

try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(result)
except (ZeroDivisionError, ValueError):
    print("Error: Invalid input or division by zero.")

This is convenient if there is no need for different handling code for different types of errors.

Using a General Exception Handler

You can use the base class Exception to catch any exceptions. However, caution should be exercised, as this can hide important errors that should not be ignored:

try:
    # Code that may raise an exception
    risky_operation()
except Exception as e:
    print(f"An error occurred: {e}")

Getting Information About an Exception

The keyword as is used to obtain detailed information about an exception:

try:
    1 / 0
except ZeroDivisionError as e:
    print(f"Error details: {e}")

Output:

Error details: division by zero

Here, e is an exception object containing information about the error.

Recommendations for Exception Handling

  • Catch only expected exceptions: Do not catch all possible exceptions unnecessarily.
  • Avoid general except Exception: Use it only in extreme cases.
  • Use finally to release resources: Always close files, network connections, and other resources in the finally block.
  • Do not leave empty except blocks: Be sure to log errors or inform the user.

Correct:

except Exception as e:
    print(f"An error occurred: {e}")

Incorrect:

except Exception:
    pass  # The error will be ignored

What Happens If an Exception Is Not Handled?

If an exception is not caught by any except block, the program terminates with an error and prints a traceback to the console, which contains a detailed description of the error and the line of code where it occurred.

Advanced Error Handling Techniques in Python

In addition to basic exception handling methods, there are more complex situations in Python that require special attention. Let's look at two common problems: using mutable objects as default arguments and catching all exceptions while preserving error information.

The Problem of Mutable Objects as Default Arguments

In Python, default argument values are calculated only once - when the function is defined. If a mutable object (for example, a list, dictionary, or set) is used as a default argument, this can lead to unexpected behavior.

Example:

def append_to_list(value, lst=[]):
    lst.append(value)
    return lst

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2] – not [2] as might be expected!

Explanation:

The first time append_to_list is called, a list lst is created, which is stored in memory and used for subsequent calls to the function unless a new argument value is passed. As a result, the elements accumulate in the same list.

Solution: Using None as the Default Value

To avoid this problem, use None as the default argument value and create a new object inside the function if the argument is not passed:

def append_to_list(value, lst=None):
    if lst is None:
        lst = []
    lst.append(value)
    return lst

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [2] – as expected!

Mutable and Immutable Objects in Python

Mutable objects:

  • Lists (list)
  • Dictionaries (dict)
  • Sets (set)
  • User-defined objects (if their state can change)

Immutable objects:

  • Numbers (int, float)
  • Strings (str)
  • Tuples (tuple)
  • Boolean values (True, False)

Using mutable objects as default arguments is a common mistake that can lead to elusive bugs in large projects.

How to Correctly Catch All Exceptions Without Hiding Errors?

Sometimes it is necessary to catch all possible exceptions in a program in order to, for example, correctly complete the work or write error information to a log. However, completely suppressing errors without processing them is a bad practice, as it can hide important problems in the code.

Correct Approach to Catching All Exceptions

  • Use the base class Exception: Catch all standard exceptions that inherit from Exception.
  • Log or output error information:
try:
    # Code that may raise an exception
    result = 10 / 0
except Exception as e:
    print(f"An error occurred: {e}")

Why Can't I Use except: Without Specifying an Exception Type?

try:
    risky_operation()
except:
    pass  # BAD! Errors are hidden, the program will continue silently

This approach completely suppresses all exceptions, including system exceptions (such as KeyboardInterrupt and SystemExit), which makes debugging much more difficult.

Correct Way to Handle All Exceptions While Preserving Information

  • Catch only descendants of Exception:
  • Re-throw the exception: So as not to lose error information.
  • Use the logging module: To record errors instead of simply outputting them to the console.
import logging
logging.basicConfig(level=logging.ERROR)

try:
    perform_task()
except Exception as e:
    logging.error("An error occurred", exc_info=True)
    raise  # Re-throw the exception

The try-except-else-finally construct can also be useful:

try:
    print("Trying to execute code...")
    result = 100 / 2
except Exception as e:
    print(f"Error: {e}")
else:
    print("Code executed without errors.")
finally:
    print("Program termination.")

Exceptions That Should Not Be Intercepted Without Extreme Need

  • KeyboardInterrupt: The user pressed Ctrl+C, the program should complete correctly.
  • SystemExit: Calling the sys.exit() function.
  • MemoryError: Lack of memory.
  • GeneratorExit: Closing the generator.

It is better not to globally intercept these exceptions so that the program can complete correctly in critical situations.

Conclusions and Recommendations

  • Do not use mutable objects as default arguments. Use None and create an object inside the function if necessary.
  • Catch only those exceptions that you can handle.
  • Always inform the user or log errors.
  • Use the logging module to track problems in large projects.

Competent error handling increases the reliability and stability of your programs.

News