Demonstrating handling Python errors with reproducible scripts

This article demonstrates how to enhance Python error handling using the traceback module while leveraging uv's support for PEP 723 to create reproducible scripts. It provides practical examples of how to move beyond basic ValueError implementations to include stack traces in error messages, making debugging and error handling more informative in production environments.



This post combines three concepts: uv’s PEP 723 support, the traceback module, and Python error handling. The approach comes from a learning technique: combining three new concepts in one example maximizes practice time.

PEP 723 lets developers specify Python version requirements and dependencies directly inside scripts. In business environments, scripts serving critical purposes often break due to version changes or missing dependencies. PEP 723 solves this with “inline script metadata”, ensuring scripts remain reproducible and reliable. Here’s what this looks like:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "pandas==2.2.3",
# ]
# ///

uv is a high-performance Python package manager and drop-in pip replacement that natively supports PEP 723. This makes dependency management simple: install uv, then run your script. uv handles version management and dependencies automatically.

# install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# run your script
uv run your_fun_new_script.py

Simple error messages work for basic scripts. Production systems, however, need full stack traces for effective debugging. This section covers practical approaches to capturing and displaying stack traces alongside error information.

Here’s an example of a function that returns an error if b is zero.

def divide_numbers(a, b):
    if b == 0:
        raise ValueError("The divisor 'b' cannot be zero.")
    return a / b

The output reads:

The divisor 'b' cannot be zero.

The next example captures the stack trace in the error message:

import traceback

def divide_numbers_stacktrace(a, b):
    def nested_division():
        if b == 0:
            stack_trace = ''.join(traceback.format_stack())
            raise ValueError(f"The divisor 'b' cannot be zero.\nStack trace:\n{stack_trace}")
        return a / b
    return nested_division()

The output shows the full stack trace:

Error: The divisor 'b' cannot be zero.
Stack trace:
  File "/root/snippets/scripts/error_handling.py", line 38, in <module>
    simple_example()
  File "/root/snippets/scripts/error_handling.py", line 31, in simple_example
    result = divide_numbers_stacktrace(10, 0)
  File "/root/snippets/scripts/error_handling.py", line 21, in divide_numbers_stacktrace
    return nested_division()
  File "/root/snippets/scripts/error_handling.py", line 18, in nested_division
    stack_trace = ''.join(traceback.format_stack())

Another approach uses traceback.print_exc() to print the full stack trace to standard output:

try:
    result = divide_numbers(10, 0)
except ValueError as e:
    # Print full exception traceback
    print("Detailed error information:")
    print(e)
    print("\nFull traceback:")
    traceback.print_exc(file=sys.stdout)

The complete script is available as a GitHub gist.