Errors and exceptions are two closely related but distinct concepts in Python. An error is any problem that prevents code from running correctly — from syntax mistakes (caught before execution) to runtime issues (during execution). Exceptions are a specific type of runtime error: special objects Python raises when an abnormal condition occurs, such as dividing by zero, accessing a missing key, or file not found. In 2026, understanding and handling exceptions is essential for writing robust, production-ready code that fails gracefully instead of crashing.
Here’s a complete, practical introduction to errors and exceptions: how they work, the most common types, how to catch and handle them, and modern best practices that make your programs reliable and debuggable.
Errors come in two main flavors: syntax errors (caught before code runs) and runtime exceptions (raised during execution). Syntax errors stop parsing — you fix them before the program starts.
# Syntax error — caught before execution
def bad_function(:
print("This won't run") # SyntaxError: '(' was never closed
Exceptions happen at runtime — Python raises them when something unexpected occurs. The most common ones include TypeError (wrong type), ValueError (bad value), ZeroDivisionError, IndexError, KeyError, FileNotFoundError, and AttributeError.
# Runtime exceptions — raised during execution
print(10 / 0) # ZeroDivisionError: division by zero
print("hello"[10]) # IndexError: string index out of range
print({"a": 1}["b"]) # KeyError: 'b'
print(int("abc")) # ValueError: invalid literal for int()
print(5 + "hello") # TypeError: unsupported operand type(s)
open("missing.txt") # FileNotFoundError: [Errno 2] No such file
The core mechanism for handling exceptions is the try/except/else/finally block. Put risky code in try, catch specific exceptions in except, run success-only code in else, and always-run cleanup in finally.
try:
result = 100 / int(input("Enter a number: ")) # Risky: user input + division
except ValueError:
print("Please enter a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
else:
print(f"Result: {result}")
finally:
print("Cleanup complete — thanks for trying!")
Real-world pattern: file operations — always use try + finally or better, with context manager for automatic cleanup.
try:
with open("data.txt", "r") as f:
content = f.read()
print(content[:50])
except FileNotFoundError:
print("File not found — please check the path")
except PermissionError:
print("Permission denied — check file access")
except Exception as e:
print(f"Unexpected error: {type(e).__name__} - {e}")
Best practices make exception handling professional and maintainable. Catch specific exceptions — never bare except: — it hides bugs and makes debugging impossible. Use as e to access exception details — log or display str(e) for users. Prefer else for success code — keeps logic clean and avoids indenting too much. Always use finally for cleanup — files, connections, locks. Raise meaningful exceptions — use ValueError for bad values, TypeError for wrong types, custom classes for domain errors. Log exceptions — use logging.exception() in production — includes traceback. Avoid swallowing errors — re-raise with raise or raise ... from e when wrapping. Modern tip: use structural pattern matching (Python 3.10+) in except for advanced handling, and contextlib.suppress to silently ignore specific errors when appropriate. In production, never let unhandled exceptions reach the top level — wrap main code in try/except and log the crash.
Errors and exceptions are inevitable — but crashes are optional. In 2026, use try/except/else/finally, catch specifically, log properly, and raise meaningfully. Master exception handling, and your code becomes robust, debuggable, and production-ready — turning potential failures into controlled, informative responses.
Next time something can go wrong — wrap it in try, catch what you expect, clean up in finally, and log the rest. That’s not just error handling — it’s professional Python.