How to fix a runtime error in Python

Struggling with Python runtime errors? This guide shows you how to fix them, with debugging tips, common errors, and real-world examples.

How to fix a runtime error in Python
Published on: 
Wed
Mar 25, 2026
Updated on: 
Thu
Mar 26, 2026
The Replit Team

Runtime errors in Python occur while a program executes, often causing it to crash unexpectedly. Unlike syntax errors, these issues appear only when the code runs, which makes them tricky.

In this article, we'll provide key techniques and debugging advice to resolve these errors. You'll find practical tips and real-world examples to help you write more robust Python code.

Basic error handling with try-except blocks

def divide_numbers(a, b):
   try:
       result = a / b
       return result
   except ZeroDivisionError:
       return "Error: Division by zero"

print(divide_numbers(10, 2))
print(divide_numbers(10, 0))--OUTPUT--5.0
Error: Division by zero

The divide_numbers function uses a try-except block to anticipate and manage a potential ZeroDivisionError. The operation a / b is placed within the try block because it's the part of the code that could fail. If b is zero, Python would normally halt and raise an error.

Instead of crashing, the except ZeroDivisionError: block catches this specific error. It then executes its own code, returning a user-friendly error message. This approach allows the program to continue running even when it encounters a predictable issue, making your code more robust and preventing unexpected crashes.

Understanding common runtime errors

Building on the try-except block, you can write even more resilient code by learning to interpret error messages and handle a wider range of exceptions.

Reading and interpreting error messages

try:
   print(undefined_variable)
except NameError as e:
   print(f"Error type: {type(e).__name__}")
   print(f"Error message: {str(e)}")
   print(f"Error occurred at line: {e.__traceback__.tb_lineno}")--OUTPUT--Error type: NameError
Error message: name 'undefined_variable' is not defined
Error occurred at line: 2

Python's error messages are packed with useful details, and the code demonstrates how to programmatically access this information. By using except NameError as e, you assign the exception object to the variable e, which you can then inspect.

  • type(e).__name__ reveals the error's class name, like NameError.
  • str(e) returns the specific error message, such as "name 'undefined_variable' is not defined".
  • e.__traceback__.tb_lineno pinpoints the exact line number where the issue occurred.

This technique allows you to build custom logging or user-friendly error feedback directly into your application.

Using assert statements to catch logic errors

def calculate_average(numbers):
   assert len(numbers) > 0, "Cannot calculate average of empty list"
   return sum(numbers) / len(numbers)

print(calculate_average([1, 2, 3, 4, 5]))
try:
   print(calculate_average([]))
except AssertionError as e:
   print(e)--OUTPUT--3.0
Cannot calculate average of empty list

The assert statement in calculate_average acts as an internal check, verifying a condition is true before the program continues. If the condition is false, it immediately stops and raises an AssertionError.

  • The statement assert len(numbers) > 0 ensures the function doesn't try to divide by zero when calculating an average.
  • If you pass an empty list, the assertion fails and provides the message "Cannot calculate average of empty list."

This makes assert a powerful tool for catching logic bugs during development, rather than handling expected runtime errors.

Handling multiple exception types

def process_data(data):
   try:
       return int(data[0]) / len(data)
   except ZeroDivisionError:
       return "Error: Empty data"
   except IndexError:
       return "Error: Data is empty list"
   except ValueError:
       return "Error: First element is not a number"

print(process_data(["10", 2, 3]))
print(process_data([]))
print(process_data(["abc", 2]))--OUTPUT--3.3333333333333335
Error: Data is empty list
Error: First element is not a number

The process_data function demonstrates how a single try block can handle several distinct errors. You can chain multiple except statements, and Python will execute the code for the first block that matches the specific error encountered.

  • An IndexError is caught if the code tries to access data[0] on an empty list.
  • A ValueError is raised if int() fails because the first element isn't a number.

This approach lets you create tailored, user-friendly messages for different failure scenarios instead of using one generic response.

Advanced error handling techniques

Moving beyond basic try-except blocks, you can write even more resilient code by proactively managing resources and creating more structured error-reporting systems.

Using context managers with with statements

def read_file_safely(filename):
   try:
       with open(filename, 'r') as file:
           return file.read()
   except FileNotFoundError:
       return f"Error: File {filename} not found"
   except PermissionError:
       return f"Error: No permission to read {filename}"

print(read_file_safely("existing_file.txt" if False else "nonexistent_file.txt"))--OUTPUT--Error: File nonexistent_file.txt not found

The read_file_safely function introduces the with statement, which acts as a context manager to simplify resource handling. When you use with open(...), Python guarantees the file is closed automatically after the code inside the block finishes. This cleanup happens even if an error is raised, preventing resource leaks.

  • You don't need to manually call a close method; the with statement handles it for you.
  • The try-except block wraps this logic to gracefully manage specific issues like a FileNotFoundError or PermissionError, making your code both safe and clean.

Creating custom exception classes

class ValueTooLargeError(Exception):
   """Raised when the input value is too large"""
   pass

def process_value(value):
   if value > 100:
       raise ValueTooLargeError(f"Value {value} exceeds maximum limit of 100")
   return value * 2

try:
   result = process_value(150)
except ValueTooLargeError as e:
   print(e)--OUTPUT--Value 150 exceeds maximum limit of 100

You can create custom exception classes to make your error handling more descriptive. By defining a new class like ValueTooLargeError that inherits from the base Exception class, you can represent application-specific issues instead of relying on generic errors.

  • The process_value function uses the raise keyword to trigger this custom error when a specific condition is met.
  • This allows you to catch the specific ValueTooLargeError in a try-except block, making your error-handling logic clearer and more organized.

Using the logging module for error tracking

import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

def divide(a, b):
   logging.debug(f"Dividing {a} by {b}")
   if b == 0:
       logging.error("Division by zero attempted")
       return None
   return a / b

print(divide(10, 2))
print(divide(10, 0))--OUTPUT--2023-08-05 12:34:56,789 - DEBUG - Dividing 10 by 2
5.0
2023-08-05 12:34:56,790 - DEBUG - Dividing 10 by 0
2023-08-05 12:34:56,790 - ERROR - Division by zero attempted
None

The logging module offers a robust alternative to print() for tracking your program's behavior. You can set it up with logging.basicConfig() to define what level of messages to show and how they're formatted. This creates a structured, timestamped record of events that’s invaluable for debugging.

  • The divide function uses logging.debug() to note routine operations, which helps you trace the program's flow.
  • When a potential crash is detected, logging.error() records the specific problem without stopping execution, making it easier to diagnose issues later.

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. You can describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.

The error-handling techniques we've covered are essential for building robust software. Replit Agent can take these concepts and turn them into production-ready tools.

  • Build a safe calculator that gracefully handles invalid inputs, like non-numeric characters or division by zero, using custom error messages instead of crashing.
  • Create a data import utility that safely reads files, logs any FileNotFoundError or PermissionError issues, and continues processing other valid data sources.
  • Deploy an API that validates user data against specific business rules, raising custom exceptions like ValueTooLargeError when inputs are out of bounds.

Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically. Start building your next project by trying Replit Agent.

Common errors and challenges

Even with the right tools, you can run into subtle pitfalls like broad exceptions, misused finally blocks, and tricky recursive function errors.

Avoiding overly broad exception handling with except

It’s tempting to use a bare except: block to catch any and all errors, but this is a risky habit. A broad exception handler can hide bugs by catching unexpected errors, making your code much harder to debug. It can also trap system-level exceptions you don't want to interfere with, like a KeyboardInterrupt when a user tries to stop the program.

Instead, you should always specify the particular exceptions you anticipate, such as except ValueError:. This practice keeps your error handling precise and ensures you’re only managing the issues you’ve planned for, letting legitimate bugs surface so they can be fixed.

Using finally blocks correctly

The finally block is a powerful feature that guarantees a piece of code will run, regardless of whether an exception was raised or not. Its main purpose is for cleanup actions that must always happen, like closing a file or releasing a network connection. This ensures you don’t leave resources locked up, even if your program hits an error.

Unlike an except block, which only executes when an error occurs, the finally block runs every time. Placing cleanup code here makes your program more robust and prevents resource leaks that can cause problems later on.

Debugging recursive function errors

Recursive functions—functions that call themselves—can be an elegant way to solve complex problems. However, they come with a common pitfall: the RecursionError. This error happens when the function calls itself too many times without hitting a "base case" to stop it, leading to what is essentially an infinite loop that exhausts system memory.

When debugging recursion, your first step should be to verify that your base case is correctly defined and is always reachable. If the logic seems right, use print() statements or a debugger to trace the function’s inputs and return values with each call. This helps you visualize the call stack and pinpoint where the logic goes astray.

Avoiding overly broad exception handling with except

It’s tempting to use a bare except: block to catch all errors, but this approach often does more harm than good. It can mask underlying bugs and system issues by silencing them with a single, generic message, making debugging a nightmare.

The parse_data function below illustrates this pitfall. Observe how it handles both a ZeroDivisionError and a ValueError with the same vague response, hiding the root cause of the problem.

def parse_data(data):
   try:
       value = int(data)
       result = 100 / value
       return result
   except:  # This catches ALL exceptions - too broad
       return "Error occurred"

print(parse_data("10"))
print(parse_data("0"))
print(parse_data("text"))

The parse_data function returns the same "Error occurred" message for different problems, like division by zero or invalid text. This ambiguity makes debugging difficult. The following example shows a more specific way to handle these errors.

def parse_data(data):
   try:
       value = int(data)
       result = 100 / value
       return result
   except ValueError:
       return "Error: Invalid number format"
   except ZeroDivisionError:
       return "Error: Division by zero"

print(parse_data("10"))
print(parse_data("0"))
print(parse_data("text"))

The improved parse_data function demonstrates the correct approach. By catching specific exceptions like ValueError and ZeroDivisionError, you can provide tailored error messages instead of a vague one.

  • This tells you precisely whether the issue was an invalid number format or division by zero.

This targeted feedback makes debugging significantly faster and your application more user-friendly. Always aim to catch the most specific exceptions possible to avoid masking unexpected bugs.

Using finally blocks correctly

While the finally block is perfect for cleanup, its behavior can sometimes hide errors. If an exception occurs in the try block, a poorly structured finally block can cause that original error to be lost, making debugging incredibly difficult.

The code below shows how this can happen. In the process_file function, an exception in the try block gets suppressed, making it appear as if the code ran without any issues.

def process_file(filename):
   file = open(filename, 'r')
   try:
       content = file.read()
       return content
   finally:
       file.close()
       # If an exception occurs in try block, it will be lost

print(process_file("example.txt"))

In process_file, the finally block guarantees file.close() will run. The problem is that if the try block fails and file.close() also raises an error, this second error masks the original one. The following example demonstrates a safer pattern that avoids this risk.

def process_file(filename):
   file = None
   try:
       file = open(filename, 'r')
       content = file.read()
       return content
   except FileNotFoundError:
       return "Error: File not found"
   finally:
       if file:
           file.close()

print(process_file("example.txt"))

The improved process_file function demonstrates a safer pattern that prevents cleanup code from masking the original error. This approach is crucial whenever you're manually managing resources like files or network connections.

  • Initialize your resource variable, like file, to None before the try block.
  • In the finally block, use an if statement to check that the resource was actually created before trying to close it.

This ensures file.close() only runs if the file was successfully opened, avoiding a new error that would hide the real issue.

Debugging recursive function errors

A recursive function must have a clear exit condition, or base case, to prevent it from calling itself indefinitely. When this condition is missing, Python raises a RecursionError. The factorial function below illustrates what happens without a proper base case.

def factorial(n):
   # Missing base case leads to infinite recursion
   return n * factorial(n - 1)

try:
   print(factorial(5))
except RecursionError as e:
   print(f"Error: {e}")

The factorial(n) function keeps calling itself with n - 1 but never reaches a stopping point. This endless loop exceeds Python's recursion depth limit. The following example demonstrates the correct implementation with a proper base case.

def factorial(n):
   if n <= 1:  # Proper base case
       return 1
   return n * factorial(n - 1)

try:
   print(factorial(5))
except RecursionError as e:
   print(f"Error: {e}")

The corrected factorial function solves the infinite loop by introducing a base case. This condition, if n <= 1, gives the function a clear stopping point, which is essential for any recursive logic.

  • When n reaches 1, the function simply returns 1 instead of calling itself again.
  • This exit strategy is what prevents the call stack from overflowing and causing a RecursionError.

It's a simple but critical check that ensures your function has a defined end.

Real-world applications

Putting these principles into practice, you can build robust features that validate user input and automatically retry failed operations.

Validating user input with try-except

You can use a try-except block to create a robust validation system that gracefully handles incorrect data types and enforces specific business logic, like an acceptable age range.

def validate_user_age(age_str):
   try:
       age = int(age_str)
       if age < 0 or age > 120:
           raise ValueError("Age must be between 0 and 120")
       return f"Valid age: {age}"
   except ValueError as e:
       return f"Invalid age: {e}"

print(validate_user_age("25"))
print(validate_user_age("abc"))
print(validate_user_age("-5"))

The validate_user_age function shows a powerful pattern for input validation. It uses a single try-except block to gracefully handle both incorrect data types and values that fail your business logic.

  • It first tries converting the input string to an integer. If the input isn't a number, Python's built-in ValueError is caught.
  • If the conversion works, the code then checks if the age is within a valid range. If it's not, you can manually raise your own ValueError with a custom message.

This technique centralizes your error handling, allowing one except block to catch multiple kinds of validation failures and provide specific feedback.

Implementing retry logic with try-except

By placing a try-except block inside a loop, you can build robust retry logic for operations that might fail intermittently, such as network requests.

import time
import random

def fetch_data_with_retry(max_retries=3, delay=1):
   for attempt in range(1, max_retries + 1):
       try:
           if random.random() < 0.7 and attempt < max_retries:
               raise ConnectionError("Temporary network issue")
           return "Data successfully retrieved"
       except ConnectionError as e:
           print(f"Attempt {attempt} failed: {e}")
           if attempt < max_retries:
               time.sleep(delay)
   return "Failed to retrieve data after multiple attempts"

print(fetch_data_with_retry())

The fetch_data_with_retry function is a practical example of handling temporary failures. It wraps the core operation in a for loop, which gives the code a chance to automatically retry after an error.

  • A try block attempts the operation. If a ConnectionError is raised, the except block catches it and waits for a short delay.
  • This pause, handled by time.sleep(), prevents the next attempt from happening too quickly.
  • If the operation succeeds, the function returns a success message. If all retries fail, it returns a final error message instead of crashing.

Get started with Replit

Turn your knowledge into a real tool. Tell Replit Agent to "build an age validator that raises a custom error for invalid inputs" or "create a utility that retries fetching data from a flaky API."

The agent writes the code, tests for runtime errors, and deploys your application from a simple prompt. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.