Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
142: Raising Exceptions Deliberately
Up until now, we've mostly dealt with exceptions that Python throws at us because we messed something up—like trying to divide by zero or accessing a list index that doesn't exist. But as you start building larger systems, you'll realize that you actually want to trigger errors on purpose. You want to stop the program dead in its tracks the moment a business rule is violated, rather than letting it continue with bad data and causing a catastrophic failure later on.
Taking Control of the Crash
Let's imagine we're building a simple module for a banking app. The core logic is straightforward: you have a balance, and you subtract a withdrawal amount. If I just write a basic subtraction function, Python is happy to let the balance go negative. In the real world, that's a problem.
def withdraw_funds(balance, amount):
return balance - amount
# This works, but it's "wrong" for our business logic
print(withdraw_funds(100, 150)) # Result: -50
I don't want the function to return -50; I want it to scream that this operation is illegal. This is where the raise keyword comes in. It allows us to manually trigger an exception.
Enforcing a Simple Balance Rule
I'll add a check to see if the withdrawal amount exceeds the balance. If it does, I'll raise an exception. This effectively tells Python, "Stop everything right here; something has gone wrong."
def withdraw_funds(balance, amount):
if amount > balance:
raise Exception("Insufficient funds for this withdrawal!")
return balance - amount
# Now this will crash the program with our specific message
withdraw_funds(100, 150)
The Danger of Being Too Generic
Now, here is where I've tripped up in the past. When I'm prototyping quickly, I often just use raise Exception("...") because it's fast. But I'll tell you right now: don't do this in production code. I once spent three hours debugging a production crash because I had used a generic Exception, and my try/except block was catching everything—including system interrupts and syntax errors—making it impossible to find the actual bug.
If you raise a generic Exception, you're basically throwing a "something went wrong" grenade into your code. You want to be specific so that the code calling your function knows exactly what happened and how to handle it.
Using Specific Exception Types
Instead of the generic Exception class, we should use a more appropriate built-in type, like ValueError, or better yet, create our own custom exception. Creating a custom exception is just creating a new class that inherits from Exception. It sounds fancy, but it's actually just two lines of code.
class InsufficientFundsError(Exception):
"""Raised when a withdrawal amount exceeds the current balance."""
pass
def withdraw_funds(balance, amount):
if amount > balance:
# We raise our specific custom error now
raise InsufficientFundsError(f"Attempted to withdraw ${amount} but only ${balance} available")
return balance - amount
try:
withdraw_funds(100, 150)
except InsufficientFundsError as e:
print(f"Transaction Failed: {e}")
By doing this, we've created a clear contract. Anyone using my withdraw_funds function knows exactly what error to look for. We aren't just crashing the program; we're communicating a specific failure state in a way that the rest of the application can gracefully handle.
📋 Practical Task
Build a Password Strength Validator
You are tasked with creating a password validation system. Write a script that does the following:
- Define a custom exception class called
PasswordTooWeakError. - Create a function
validate_password(password)that checks the length of the password. - If the password is shorter than 8 characters, the function should raise the
PasswordTooWeakErrorwith a message explaining that the password must be at least 8 characters long. - If the password is 8 characters or longer, it should return
True. - Wrap the function call in a
try/exceptblock to catch your custom exception and print a user-friendly error message.
There are no comments for now.