Python Tutorials

Custom Exceptions In Python : Validating User Input

Imagine you are building the software for an ATM. A user attempts to withdraw -$500 from their account. From a strict programming perspective, Python sees absolutely nothing wrong with this. It is perfectly legal to perform math with negative numbers. However, from a business perspective, allowing a user to withdraw negative money is a catastrophic flaw.

How do we stop Python from running perfectly valid code when the actual data violates our real-world rules? We must purposefully force the program to crash (or trigger an error) using the raise keyword.

By defining and raising custom exceptions, you can enforce strict input validation, create meaningful error messages, and build highly secure Python applications that perfectly reflect your unique business logic.

What is Custom Exceptions?

In Python, an Exception is a signal that an error has occurred. While Python automatically raises built-in exceptions (like ZeroDivisionError when dividing by zero), developers can manually trigger errors using the raise keyword.

A Custom Exception is a user-defined error class that inherits from Python’s base Exception class. Instead of relying on generic built-in errors, developers create specialized, highly descriptive exceptions (e.g., InsufficientFundsError, InvalidEmailError) to make their codebase intuitive and deeply integrated with their business rules.

Syntax & Basic Usage

The simplest way to validate user input is to use the raise keyword paired with a built-in exception class (like ValueError), passing a custom error message as an argument.

def process_user_age(age_input):
    # Validating the input against our business rules
    if age_input < 0:
        # We manually trigger an error to stop execution instantly
        raise ValueError("User age cannot be negative.")
        
    print(f"Age {age_input} validated and saved to database.")

# Testing the validation
try:
    process_user_age(-5)
except ValueError as error_message:
    print(f"Validation Failed: {error_message}")

# Expected Output:
# Validation Failed: User age cannot be negative.
Code language: PHP (php)

Python Custom Exception Methods and Function Arguments

While reusing built-in exceptions like ValueError is a good start, building large applications requires creating your own distinct, trackable error types. Let’s explore the complete lifecycle of creating and raising custom exceptions.

1. Creating a Basic Custom Exception Class

To create your own exception, you simply define an empty class that inherits from Python’s master Exception class. You can leave the body completely blank using the pass keyword.

# 1. Defining the custom exception (Notice it inherits from 'Exception')
class InvalidUsernameError(Exception):
    pass

def register_username(username):
    # 2. Input validation
    if len(username) < 5:
        # 3. Raising our brand new, highly specific error
        raise InvalidUsernameError(f"The username '{username}' is too short.")
        
    print(f"User '{username}' registered successfully!")

# 4. Catching the custom exception
try:
    register_username("Bob")
except InvalidUsernameError as e:
    print(f"Registration Error: {e}")

# Expected Output:
# Registration Error: The username 'Bob' is too short.

2. Advanced Exceptions with Custom Initialization (__init__)

An empty custom exception is great, but what if you want the error object itself to securely store the invalid data so your logging system can analyze it later? We can override the __init__ method inside our custom exception to accept specific arguments.

class TemperatureTooHighError(Exception):
    # We require the current temp and the max allowed temp to be passed in
    def __init__(self, current_temp, max_temp):
        self.current_temp = current_temp
        self.max_temp = max_temp
        
        # We dynamically construct the error message
        self.message = f"DANGER: Core is at {current_temp}°C. Maximum safe limit is {max_temp}°C!"
        
        # We must initialize the base Exception class with our new message
        super().__init__(self.message)

def monitor_reactor_core(temperature):
    maximum_safe_limit = 1000
    if temperature > maximum_safe_limit:
        # Raising the error and passing in the required data
        raise TemperatureTooHighError(temperature, maximum_safe_limit)
        
    print("Core temperature is stable.")

try:
    monitor_reactor_core(1250)
except TemperatureTooHighError as e:
    print(e.message)
    # We can also access the raw data attached to the error object!
    print(f"--> Exceeded by: {e.current_temp - e.max_temp}°C")

# Expected Output:
# DANGER: Core is at 1250°C. Maximum safe limit is 1000°C!
# --> Exceeded by: 250°C

3. Re-Raising Exceptions

Sometimes you want to catch an error, log it to your server’s security file, and then immediately push that exact same error back out for another part of the program to handle. You do this by typing raise entirely by itself inside an except block.

def process_secure_payment(amount):
    if amount <= 0:
        raise ValueError("Payment amount must be greater than zero.")
    print("Payment processed.")

def checkout_cart():
    try:
        process_secure_payment(-10)
    except ValueError as error_data:
        print("System Log: Fraudulent payment attempt intercepted.")
        # Re-raise the exact same error to the outer application
        raise 

try:
    checkout_cart()
except ValueError as final_error:
    print(f"Customer UI Error: {final_error}")

# Expected Output:
# System Log: Fraudulent payment attempt intercepted.
# Customer UI Error: Payment amount must be greater than zero.
Code language: PHP (php)

Real-World Practical Examples

Scenario 1: Strict Password Validation Pipeline

In web development, creating strong, descriptive error classes makes writing your user interface incredibly clean. You can catch specific errors and show the user exact instructions on how to fix their password.

# 1. Define specific business-logic errors
class PasswordTooShortError(Exception):
    pass

class PasswordMissingNumberError(Exception):
    pass

# 2. Validation Function
def validate_password(password_input):
    if len(password_input) < 8:
        raise PasswordTooShortError("Password must be at least 8 characters long.")
        
    if not any(char.isdigit() for char in password_input):
        raise PasswordMissingNumberError("Password must contain at least one number.")
        
    print("Password accepted.")

# 3. Processing the user's input
user_password_attempt = "secure_pw"

try:
    validate_password(user_password_attempt)
    
except PasswordTooShortError as e:
    print(f"UI Prompt: {e}")
except PasswordMissingNumberError as e:
    print(f"UI Prompt: {e}")

# Expected Output:
# UI Prompt: Password must contain at least one number.
Code language: HTML, XML (xml)

Scenario 2: Banking Application Constraints

Here we use multiple custom exceptions to validate complex business logic for a bank account withdrawal.

class InsufficientFundsError(Exception):
    pass

class DailyLimitExceededError(Exception):
    pass

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self.daily_limit = 500
        self.withdrawn_today = 0
        
    def withdraw(self, request_amount):
        # Validation 1: Daily Limit
        if self.withdrawn_today + request_amount > self.daily_limit:
            raise DailyLimitExceededError(f"Request exceeds daily limit of ${self.daily_limit}.")
            
        # Validation 2: Available Funds
        if request_amount > self.balance:
            raise InsufficientFundsError(f"Cannot withdraw ${request_amount}. Available balance: ${self.balance}.")
            
        # If all validation passes, perform the operation
        self.balance -= request_amount
        self.withdrawn_today += request_amount
        print(f"Dispensing ${request_amount}. New balance: ${self.balance}.")

# Using the class
my_account = BankAccount(balance=300)

try:
    my_account.withdraw(400)
except InsufficientFundsError as e:
    print(f"Bank Error: {e}")
except DailyLimitExceededError as e:
    print(f"Bank Error: {e}")

# Expected Output:
# Bank Error: Cannot withdraw $400. Available balance: $300.

Best Practices & Common Pitfalls

  • Naming Conventions: Custom exception classes should always end with the word Error (e.g., InvalidDataError, not InvalidDataException or InvalidData). This immediately signals to other developers what the class does.
  • Always Inherit from Exception: Never inherit from BaseException. BaseException is the absolute root of all Python errors and includes critical system-exit events (like when a user presses Ctrl+C to kill a frozen program). If you inherit from it, a bare except block might accidentally swallow system-level commands. Always use class MyError(Exception):.
  • Don’t Over-Engineer: While custom exceptions are powerful, don’t create a new class for every minor issue. If a user inputs a string when you asked for an integer, the built-in ValueError or TypeError is already the perfect tool for the job. Only create custom classes for domain-specific business logic (e.g., SubscriptionExpiredError).

Summary

  • The raise keyword allows you to manually trigger an error and instantly halt code execution when data violates your business logic.
  • Input Validation relies heavily on raising errors to ensure bad data never reaches your database.
  • Custom Exceptions are user-defined error classes that inherit from the built-in Exception class.
  • Custom exceptions allow you to catch specific business-logic failures cleanly, separating them from generic Python syntax errors.
  • You can override the __init__ method of a custom exception to store specific data variables (like error codes or user inputs) directly inside the error object for logging and debugging.
  • Using a bare raise inside an except block allows you to intercept an error, log it, and push the exact same error back out to the application.

Leave a Comment