Python Error Handling & Debugging
Errors are a normal part of programming. This guide covers the whole cycle in four parts: reading errors, handling them so your program does not crash, raising your own when something is wrong, and debugging code to find the cause.
At a Glance
Section titled “At a Glance”| Task | Tool | Example |
|---|---|---|
| Understand an error | Read the traceback from the bottom up | ValueError: invalid literal for int() |
| Catch an error | try / except | except ValueError: |
| Run only on success | else | else: print('OK') |
| Always clean up | finally, with | with open(path) as f: |
| Signal an error | raise | raise ValueError('Age cannot be negative') |
| Re-raise | bare raise | except ValueError: raise |
| Link errors | raise ... from | raise AppError('...') from e |
| Check an assumption | assert | assert x >= 0, 'x must be positive' |
| Inspect values quickly | print() with f-strings | print(f'{x = }') |
| Record what happened | logging | logging.exception('Failed') |
| Pause and explore | breakpoint() (pdb) | (Pdb) p x |
| Debug visually | IDE debugger | Breakpoints and stepping |
Part 1: Reading Errors
Section titled “Part 1: Reading Errors”Anatomy of a Traceback
Section titled “Anatomy of a Traceback”When an error is not handled, Python stops and prints a traceback. The error type and message are on the last line, and the lines above show the chain of calls that led to it.
def divide(a, b): return a / b
print(divide(10, 0))Traceback (most recent call last): File "traceback_example.py", line 4, in <module> print(divide(10, 0)) ^^^^^^^^^^^^^ File "traceback_example.py", line 2, in divide return a / b ~~^~~ZeroDivisionError: division by zero-
Read the last line first.
ZeroDivisionError: division by zerotells you the type of error and what went wrong. -
Find where it happened. The frame closest to the bottom (
line 2, in divide) is where the error was raised. -
Follow the calls upward. Each frame above shows who called whom, ending with the top-level call (
<module>). The carets (^^^) point at the exact expression involved.
Common Error Messages
Section titled “Common Error Messages”Python’s most common errors occur when Python does not understand your code (syntax) or when your logic does not match the data you are working with (everything else).
SyntaxError
Section titled “SyntaxError”The code does not follow Python’s grammar. Python reports it before running anything.
print("Hello, world!"# SyntaxError: '(' was never closedThe line is missing a closing parenthesis. In Python 3.9 and earlier the message read SyntaxError: unexpected EOF while parsing. The cause is the same.
Related: an IndentationError (a subclass of SyntaxError) means a block is not indented correctly.
if True:print('Hello')# IndentationError: expected an indented block after 'if' statement on line 1NameError
Section titled “NameError”You used a name Python cannot find. Typical causes are a typo, using a variable before defining it, or a missing import.
print(name)# NameError: name 'name' is not definedTypeError
Section titled “TypeError”An operation was applied to a value of the wrong type.
5 + "5"# TypeError: unsupported operand type(s) for +: 'int' and 'str'You cannot add an integer and a string. More TypeError variants you will meet:
len(5) # TypeError: object of type 'int' has no len()'abc'() # TypeError: 'str' object is not callableNone[0] # TypeError: 'NoneType' object is not subscriptable
def greet(name): return name
greet() # TypeError: greet() missing 1 required positional argument: 'name'greet('a', 'b') # TypeError: greet() takes 1 positional argument but 2 were givenIndexError
Section titled “IndexError”You asked for a position that does not exist in a sequence.
my_list = [1, 2, 3]print(my_list[5])# IndexError: list index out of rangeAttributeError
Section titled “AttributeError”You used a method or property that does not exist on that type of object.
num = 42num.append(5)# AttributeError: 'int' object has no attribute 'append'A very common variant involves None. Methods like list.sort() return None, so chaining onto the result fails:
numbers = [3, 1, 2].sort()numbers.append(4)# AttributeError: 'NoneType' object has no attribute 'append'More Errors You Will Meet
Section titled “More Errors You Will Meet”| Error | Typical cause | Example |
|---|---|---|
ValueError | Right type, unacceptable value | int('abc') gives invalid literal for int() with base 10: 'abc' |
KeyError | Missing dictionary key | {'a': 1}['b'] gives KeyError: 'b' |
ZeroDivisionError | Dividing or taking modulo by zero | 10 / 0 gives division by zero |
UnboundLocalError | Assigning to a name inside a function before it has a value | counter += 1 with no global or local value |
ModuleNotFoundError | Module is not installed or is misspelled | import numpyy gives No module named 'numpyy' |
ImportError | Name does not exist in the module | from math import foo gives cannot import name 'foo' from 'math' |
FileNotFoundError | The path does not exist | open('missing.txt') gives [Errno 2] No such file or directory: 'missing.txt' |
PermissionError | Not allowed to access the file | Opening a protected file |
RecursionError | A function calls itself without stopping | maximum recursion depth exceeded |
StopIteration | next() on an exhausted iterator | next(iter([])) |
AssertionError | An assert condition was false | assert 1 == 2 |
KeyboardInterrupt | The user pressed Ctrl+C | Stopping a running program |
Part 2: Handling Exceptions
Section titled “Part 2: Handling Exceptions”An exception is an error that occurs while the program runs. If nothing handles it, it travels up the call stack and ends the program with a traceback. Exception handling lets you anticipate, catch, and respond to errors in a structured way so your program does not crash unexpectedly.
try and except
Section titled “try and except”try: x = 10 / 0except ZeroDivisionError: print("You can't divide by zero!")tryholds the code where you anticipate an error.exceptruns if an error of the specified type is raised inside thetryblock.
Here the division raises a ZeroDivisionError, which is caught and handled, and the program continues.
The Four Blocks
Section titled “The Four Blocks”Python gives you four blocks: try, except, else, and finally.
| Block | Runs when… |
|---|---|
try | Always. It is where the risky code lives |
except | An exception of the matching type was raised in try |
else | No exception was raised in try |
finally | Always, whether or not an exception occurred |
try: x = 10 / 2except ZeroDivisionError: print("You can't divide by zero!")else: print('Division successful:', x) # Runs: no exceptionfinally: print('This block always runs.') # Always runsfinally is useful for clean-up tasks such as closing files or releasing resources.
Why Use else?
Section titled “Why Use else?”Code in else only runs on success, and errors raised inside it are not caught by the preceding except clauses. This keeps the try block small and focused on the one operation that can fail.
text = '42'
try: number = int(text) # Only the risky line lives in tryexcept ValueError: print('Not a valid number')else: print(number * 2) # 84, runs only if the conversion workedCatching Multiple Exceptions
Section titled “Catching Multiple Exceptions”Separate except blocks
Section titled “Separate except blocks”Separate clauses let you respond differently to each error.
try: number = int('abc') result = 10 / numberexcept ValueError: print('That was not a valid number.') # Runsexcept ZeroDivisionError: print("Can't divide by zero.")One except for several types
Section titled “One except for several types”Group exception types in a tuple to handle them the same way.
try: number = int(input('Enter a number: ')) result = 10 / numberexcept (ValueError, ZeroDivisionError) as e: print(f'Error occurred: {e}')The Exception Object (as e)
Section titled “The Exception Object (as e)”Use as to give the caught exception a name, typically e. It lets you access the actual error message for logging or debugging.
try: x = 1 / 0except ZeroDivisionError as e: print(f'Error occurred: {e}') # Error occurred: division by zerotry: int('abc')except ValueError as e: print(e) # invalid literal for int() with base 10: 'abc' print(type(e).__name__) # ValueError print(repr(e)) # ValueError("invalid literal for int() with base 10: 'abc'") print(e.args) # ("invalid literal for int() with base 10: 'abc'",)The Exception Hierarchy
Section titled “The Exception Hierarchy”Exceptions are classes organized in a hierarchy. Catching a parent class also catches all of its children.
BaseException ├── SystemExit ├── KeyboardInterrupt └── Exception ├── ArithmeticError │ ├── ZeroDivisionError │ └── OverflowError ├── AssertionError ├── AttributeError ├── ImportError │ └── ModuleNotFoundError ├── LookupError │ ├── IndexError │ └── KeyError ├── NameError │ └── UnboundLocalError ├── OSError │ ├── FileNotFoundError │ └── PermissionError ├── RuntimeError │ └── RecursionError ├── StopIteration ├── SyntaxError │ └── IndentationError ├── TypeError └── ValueErrordata = {'a': 1}items = [1, 2, 3]
for container, key in [(data, 'b'), (items, 10)]: try: container[key] except LookupError as e: # Catches both KeyError and IndexError print(f'{type(e).__name__}: {e}')# KeyError: 'b'# IndexError: list index out of rangeOrder Matters
Section titled “Order Matters”Python checks except clauses top to bottom and uses the first match. Put specific exceptions before general ones, or the specific clause will never run.
try: int('abc')except Exception: # Too broad, listed first print('General handler') # Runsexcept ValueError: print('Specific handler') # Never reachedCatch Specific Exceptions
Section titled “Catch Specific Exceptions”try: value = int(text)except ValueError: value = 0Only the failure you expect is handled. Real bugs still surface.
try: value = int(text)except Exception as e: logging.exception('Unexpected failure')Acceptable at a program’s outer edge (for example, to log a crash), never to silence errors.
try: value = int(text)except: passCatches everything, including KeyboardInterrupt and SystemExit, and hides bugs. Never do this.
finally in Detail
Section titled “finally in Detail”finally runs even if the try block hits a return, break, or continue, or if an exception is not handled.
def get_value(): try: return 'from try' finally: print('cleanup runs before the function returns')
print(get_value())# cleanup runs before the function returns# from tryAutomatic Clean-up with with
Section titled “Automatic Clean-up with with”The with statement guarantees clean-up even if an error occurs, so you rarely need to write finally.
This is only used for the: objects that support the Context Manager protocol (objects that have enter and exit methods, like files, network connections, or database sessions).
with open('data.txt') as file: content = file.read()# The file is closed automatically, even if read() failsfile = open('data.txt')try: content = file.read()finally: file.close()Handling Patterns
Section titled “Handling Patterns”Safe Conversion Helper
Section titled “Safe Conversion Helper”def to_int(text, default=None): try: return int(text) except (ValueError, TypeError): return default
print(to_int('42')) # 42print(to_int('abc')) # Noneprint(to_int(None, 0)) # 0, int(None) raises TypeErrorRe-Ask Until the Input Is Valid
Section titled “Re-Ask Until the Input Is Valid”while True: text = input('Enter a whole number: ') try: number = int(text) except ValueError: print('Not a valid number, try again.') else: breakprint(f'You entered {number}')Skip Bad Records and Keep Going
Section titled “Skip Bad Records and Keep Going”records = ['10', 'x', '30', '']numbers, errors = [], []
for record in records: try: numbers.append(int(record)) except ValueError: errors.append(record)
print(numbers) # [10, 30]print(errors) # ['x', '']Handling a Missing File
Section titled “Handling a Missing File”try: with open('config.txt') as f: data = f.read()except FileNotFoundError: print('config.txt not found, using defaults') data = ''except PermissionError: print('You do not have permission to read config.txt') data = ''Dictionary Lookups
Section titled “Dictionary Lookups”user = {'name': 'Ann'}
try: email = user['email']except KeyError: email = 'unknown'
email = user.get('email', 'unknown') # Often simpler when a default is enoughRetrying a Failing Operation
Section titled “Retrying a Failing Operation”import time
def fetch_with_retry(action, attempts=3): for attempt in range(1, attempts + 1): try: return action() except ConnectionError as e: print(f'Attempt {attempt} failed: {e}') if attempt == attempts: raise # Out of retries, let the error propagate time.sleep(1)Ignoring an Error on Purpose
Section titled “Ignoring an Error on Purpose”contextlib.suppress states clearly that an error is expected and harmless.
import osfrom contextlib import suppress
with suppress(FileNotFoundError): os.remove('temp.txt') # No error if the file is already goneEasier to Ask Forgiveness than Permission
Section titled “Easier to Ask Forgiveness than Permission”Python programmers often attempt an operation and handle the failure, rather than checking first.
# Look before you leapif 'email' in user: email = user['email']
# Ask forgiveness (EAFP)try: email = user['email']except KeyError: email = 'unknown'Part 3: Raising Exceptions
Section titled “Part 3: Raising Exceptions”The raise statement lets you trigger an exception manually. It gives you control over when and how errors are generated, so you can create custom error conditions, signal that an error has occurred, and enforce requirements that were not met.
Basic raise
Section titled “Basic raise”def check_age(age): if age < 0: raise ValueError('Age cannot be negative') return age
try: check_age(-5)except ValueError as e: print(f'Error: {e}') # Error: Age cannot be negativeraise is the keyword that triggers the exception. Here a ValueError is raised with a custom message when an invalid age is provided.
Forms of raise
Section titled “Forms of raise”raise ValueError # A class: Python creates the instance for youraise ValueError('Bad value') # An instance with a message (the usual form)raise # Bare: re-raise the current exception (inside except only)raise AppError('msg') from original # Chain to a causeraise AppError('msg') from None # Suppress the original contextUsing a bare raise outside an except block raises RuntimeError: No active exception to reraise.
Choosing the Right Exception
Section titled “Choosing the Right Exception”| Situation | Raise |
|---|---|
| Wrong type of argument | TypeError |
| Right type, unacceptable value | ValueError |
| Missing key or index | KeyError, IndexError |
| Feature intentionally not implemented | NotImplementedError |
| A required file or resource is missing | FileNotFoundError |
| Your own domain-specific problem | A custom exception |
def set_discount(percent): if not isinstance(percent, (int, float)): raise TypeError(f'percent must be a number, got {type(percent).__name__}') if not 0 <= percent <= 100: raise ValueError(f'percent must be between 0 and 100, got {percent}') return percent
set_discount('10') # TypeError: percent must be a number, got strset_discount(150) # ValueError: percent must be between 0 and 100, got 150Re-Raising an Exception
Section titled “Re-Raising an Exception”A bare raise inside an except block re-raises the same exception. This lets you log or clean up while still propagating the error up the call stack.
def process_data(data): try: result = int(data) return result * 2 except ValueError: print('Logging: Invalid data received') raise # Re-raises the same ValueError
try: process_data('abc')except ValueError: print('Handled at higher level')Output:
Logging: Invalid data receivedHandled at higher levelCustom Exceptions
Section titled “Custom Exceptions”Define your own exception by inheriting from Exception or one of its subclasses. Custom exceptions let you attach extra data and let callers catch exactly your errors.
class InsufficientFundsError(Exception): def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f'Insufficient funds: ${balance} available, ${amount} requested')
def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(balance, amount) return balance - amount
try: new_balance = withdraw(100, 150)except InsufficientFundsError as e: print(f'Transaction failed: {e}') # Transaction failed: Insufficient funds: $100 available, $150 requested print(e.balance, e.amount) # 100 150Classes and inheritance are covered in more depth in later lessons. For now, know that this is a way to create your own exceptions with custom logic.
Build a Small Hierarchy
Section titled “Build a Small Hierarchy”A shared base class lets callers catch all of your errors at once, or just one kind.
class AppError(Exception): """Base class for every error raised by this application."""
class ValidationError(AppError): """Input failed validation."""
class StorageError(AppError): """Reading or writing data failed."""
try: raise ValidationError('Email address is missing an @')except AppError as e: # Catches ValidationError and StorageError print(f'{type(e).__name__}: {e}') # ValidationError: Email address is missing an @Chaining Exceptions with from
Section titled “Chaining Exceptions with from”Use raise ... from to show how two errors relate.
def parse_config(filename): try: with open(filename, 'r') as file: data = file.read() return int(data) except FileNotFoundError: raise ValueError('Configuration file is missing') from None except ValueError as e: raise ValueError('Invalid configuration format') from e
config = parse_config('config.txt')from None suppresses the original
Section titled “from None suppresses the original”If config.txt does not exist, only the new, clean error is shown:
Traceback (most recent call last): File "main.py", line 11, in <module> config = parse_config('config.txt') ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "main.py", line 7, in parse_config raise ValueError('Configuration file is missing') from NoneValueError: Configuration file is missingfrom e preserves the error trail
Section titled “from e preserves the error trail”If the file exists but its contents are not a number, the new exception is chained to the original:
Traceback (most recent call last): File "main.py", line 5, in parse_config return int(data) ^^^^^^^^^ValueError: invalid literal for int() with base 10: ''
The above exception was the direct cause of the following exception:
Traceback (most recent call last): File "main.py", line 11, in <module> config = parse_config('config.txt') ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "main.py", line 9, in parse_config raise ValueError('Invalid configuration format') from eValueError: Invalid configuration format| Form | What the traceback shows |
|---|---|
raise New from e | Both errors: “The above exception was the direct cause of the following exception” |
raise New (inside an except) | Both errors: “During handling of the above exception, another exception occurred” |
raise New from None | Only the new error |
The original exception stays available on the new one as __cause__ (for from e) or __context__ (implicit chaining).
assert
Section titled “assert”An assert statement raises an AssertionError when its condition is false. It is essentially shorthand for a conditional raise.
def calculate_square_root(number): assert number >= 0, 'Cannot calculate square root of negative number' return number ** 0.5
try: result = calculate_square_root(-4)except AssertionError as e: print(f'Assertion failed: {e}') # Assertion failed: Cannot calculate square root of negative numberThese two blocks are equivalent:
assert number >= 0, 'must be non-negative'
if not number >= 0: raise AssertionError('must be non-negative')Why Raise?
Section titled “Why Raise?”Strategic use of raise makes code more predictable and easier to debug. It lets you enforce business rules, validate input, and give callers clear feedback about what went wrong, instead of letting bad data travel deeper into the program and fail somewhere confusing.
Part 4: Debugging Techniques
Section titled “Part 4: Debugging Techniques”Debugging is the process of identifying and resolving errors or bugs in your code. It involves examining the code, understanding the flow, and using tools to pinpoint the source of problems.
| Technique | Best for |
|---|---|
print() and f-strings | Quick checks of values and flow |
logging | Permanent diagnostic output |
pdb / breakpoint() | Interactive exploration in the terminal |
| IDE debugger | Visual inspection with breakpoints and stepping |
print() and f-strings
Section titled “print() and f-strings”Printing values at various points in your code helps you understand the flow of your program and the state of its variables.
def add(a, b): result = a + b print(f'Adding {a} and {b} gives {result}') return result
add(2, 3) # Adding 2 and 3 gives 5By printing a, b, and result, you can verify that the function behaves as expected. A few tricks make this faster:
x = 'hello'items = [1, 2]
print(f'{x = }') # x = 'hello', prints the name AND the value (Python 3.8+)print(f'{x!r}') # 'hello', repr shows quotes and hidden charactersprint(f'{type(x)}') # <class 'str'>print(f'{len(items)=}') # len(items)=2print('DEBUG reached step 2') # Confirm that a code path actually runslogging
Section titled “logging”The logging module records messages with severity levels and can be switched on or off without editing your code.
import logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
logging.debug('Starting calculation') # DEBUG: Starting calculationlogging.info('Processing 3 records') # INFO: Processing 3 recordslogging.warning('Disk space is low') # WARNING: Disk space is low
try: 1 / 0except ZeroDivisionError: logging.exception('Calculation failed') # ERROR message plus the full tracebackTo print a traceback without stopping the program, use the traceback module:
import traceback
try: 1 / 0except ZeroDivisionError: traceback.print_exc() # Prints the traceback text = traceback.format_exc() # Or capture it as a stringInteractive Debugging with pdb
Section titled “Interactive Debugging with pdb”Python’s built-in pdb module lets you pause the program, inspect variables, and step through code line by line.
import pdb
def divide(a, b): pdb.set_trace() return a / b
print(divide(10, 2))By setting a trace with set_trace(), you can step through the code, inspect variables, and understand the program’s behavior.
What You See
Section titled “What You See”When you run the code, the output shows the location of the file you are running, the line where you called set_trace() and the code immediately after it, and an interactive (Pdb) prompt:
> /Users/fcc/Desktop/debugging.py(5)divide()-> return a / b(Pdb)The help Command
Section titled “The help Command”Entering help at the prompt lists the commands you can use:
Full output of help
(Pdb) help
Documented commands (type help <topic>):========================================EOF c d h list q rv undisplaya cl debug help ll quit s untalias clear disable ignore longlist r source untilargs commands display interact n restart step upb condition down j next return tbreak wbreak cont enable jump p retval u whatisbt continue exit l pp run unalias where
Miscellaneous help topics:==========================exec pdbType help <command> (for example help next) for details on any single command.
Inspecting Types with whatis
Section titled “Inspecting Types with whatis”To look at the type of elements at that moment, use the whatis command:
(Pdb) whatis a<class 'int'>(Pdb) whatis divideFunction divideBy the time set_trace() runs, the parameter a is an integer and divide is a function.
Continuing Execution
Section titled “Continuing Execution”Use continue, or its aliases cont and c, to resume the program:
(Pdb) continue5.0Most Useful Commands
Section titled “Most Useful Commands”| Command | Alias | Action |
|---|---|---|
next | n | Run the current line and stop at the next one in this function |
step | s | Run the current line, entering any function it calls |
continue | c, cont | Resume until the next breakpoint or the end |
list | l | Show source code around the current line |
longlist | ll | Show the whole current function |
print expr | p expr | Print the value of an expression |
pp expr | Pretty-print a value | |
whatis expr | Show the type of an expression | |
args | a | Show the arguments of the current function |
where | w, bt | Show the call stack |
up / down | u / d | Move up or down the call stack |
break N | b N | Set a breakpoint at line N |
until | unt | Continue until a later line is reached (useful to skip a loop) |
return | r | Run until the current function returns |
quit | q | Exit the debugger and stop the program |
(Pdb) p a10(Pdb) p b2(Pdb) p a / b5.0(Pdb) nDebugging a Crash After It Happens
Section titled “Debugging a Crash After It Happens”python -m pdb script.pyRunning a script with -m pdb starts the debugger immediately. If the script crashes, pdb drops you into a post-mortem session at the line that raised, so you can inspect the variables as they were.
IDE Debugging Tools
Section titled “IDE Debugging Tools”Many Integrated Development Environments (IDEs) offer advanced debugging tools, such as breakpoints, step execution, and variable inspection. They provide a visual interface to examine the state of your program, making it easier to identify and fix issues compared to using print statements alone.
Using the VS Code Debugger
Section titled “Using the VS Code Debugger”If you use VS Code, you can set breakpoints in your code and run the debugger to pause execution at those points. Here is how to debug the same divide function.
-
Set up your code. Create a file called
main.pywith the following content:def divide(a, b):result = a / breturn resultprint(divide(10, 2))print(divide(15, 3)) -
Set a breakpoint. Click in the gutter (the left margin) next to line 2 (
result = a / b). A red dot appears, indicating the breakpoint is set. -
Start debugging. Press
F5or go to Run > Start Debugging. Select “Python File” when prompted. The debugger pauses execution at your breakpoint. -
Inspect variables. Hover over variables to see their current values. Use the Variables panel on the left to see all local variables. Use the Debug Console at the bottom to evaluate expressions.
-
Step through code. Use the debug toolbar to move through the program (see the table below).
| Control | Shortcut | Action |
|---|---|---|
| Continue | F5 | Resume execution until the next breakpoint |
| Step Over | F10 | Execute the current line and move to the next |
| Step Into | F11 | Enter into function calls |
| Step Out | Shift + F11 | Exit the current function |
A Systematic Approach
Section titled “A Systematic Approach”-
Reproduce the bug. Find the smallest input that reliably triggers it.
-
Read the error. Start with the last line of the traceback, then find the line number.
-
Form a hypothesis. Decide what you think is wrong, and what value you expect at that point.
-
Inspect. Use a
print(),breakpoint(), or IDE breakpoint to compare the actual value with your expectation. Narrow the search by checking the midpoint of the suspect code, then the half that is wrong. -
Fix and verify. Change one thing at a time, rerun, and confirm the bug is gone.
-
Lock it in. Add a test (see below) so the bug cannot quietly return.
Choosing between the techniques: use print() statements for quick checks, pdb for interactive exploration, and IDE debuggers for visual inspection. Each technique has its place.
Testing That Code Raises
Section titled “Testing That Code Raises”Tests can confirm that your code raises the right exception. With pytest (a third-party library):
import pytest
def check_age(age): if age < 0: raise ValueError('Age cannot be negative') return age
def test_negative_age_raises(): with pytest.raises(ValueError, match='negative'): check_age(-5)The standard library equivalent is self.assertRaises(ValueError) inside a unittest.TestCase.
Reference
Section titled “Reference”Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Fix |
|---|---|---|
Bare except: | Catches KeyboardInterrupt and hides bugs | Catch specific exceptions |
except Exception: pass | Silently swallows errors | Handle, log, or let it propagate |
Putting many lines in try | Catches errors you did not expect | Keep try to the one risky operation, use else for the rest |
Broad except listed before a specific one | The specific handler never runs | Order from specific to general |
| Printing an error and carrying on | Loses the traceback | logging.exception(...), or re-raise |
raise e to re-raise | Adds a redundant traceback frame | Bare raise |
return inside finally | Overrides returns and swallows exceptions | Keep finally for clean-up only |
assert to validate input | Removed when running with -O | Use raise ValueError(...) |
Vague messages (raise ValueError('bad')) | Hard to diagnose | Say what was wrong and include the value |
Using e after the except block | NameError, e is deleted | Copy it to another name inside the block |
Expecting else to run after an error | else runs only when no exception occurred | Put error handling in except |
open() without with | The file may stay open after an error | with open(...) as f: |
Leaving debug print() calls in code | Noisy output in production | Remove them, or use logging |
Chaining onto list.sort() | AttributeError on None | Call it on its own line, or use sorted() |
Best Practices Checklist
Section titled “Best Practices Checklist”- Read the last line of a traceback first, then work upward.
- Catch specific exceptions, and keep
tryblocks small. - Use
elsefor code that depends on thetrysucceeding andfinally(orwith) for clean-up. - Never use a bare
except:, and never swallow errors silently. - Raise the most specific built-in exception that fits, and write messages that include the offending value.
- Use bare
raiseto re-raise, andraise ... from eto wrap one error in another. - Define a base exception for your application and derive your custom errors from it.
- Validate input with
raise, and keepassertfor internal sanity checks. - Use
print()for quick checks,loggingfor permanent diagnostics,pdborbreakpoint()for interactive exploration, and an IDE debugger for visual stepping. - After fixing a bug, add a test that reproduces it.