- A child class can have multiple parents:
class C(A, B) - Python uses the MRO (C3 linearization) to resolve conflicts
- Check the order with
ClassName.__mro__ - Useful for mixins — small classes that add specific behavior
This presentation is designed for desktop or projector displays.
← Back to curriculumPython Object Oriented Programming
Course 6
← → to navigate
Where we are in the journey
Can a Python class inherit from multiple parent classes?
A) No, Python only supports single inheritance
B) Yes, Python supports multiple inheritance
C) Only with abstract base classes
D) Only if the parents have no shared methods
Python supports multiple inheritance — a class can inherit from several parents. The MRO (Method Resolution Order) determines which parent method is called first.
Recap: Lecture 03
class C(A, B)ClassName.__mro__class Flyable: def move(self): return "Flying" class Swimmable: def move(self): return "Swimming" class Duck(Flyable, Swimmable): pass print(Duck().move()) # "Flying" (MRO)
What is polymorphism in OOP?
A) Hiding data inside a class
B) Creating multiple constructors
C) Same interface, different behavior
D) Copying objects from one class to another
Polymorphism lets different classes share the same method name but provide their own implementation — "many forms"
Recap: Lecture 04
class Cat(Animal): def speak(self): return "Meow!" class Dog(Animal): def speak(self): return "Woof!" for animal in [Cat(), Dog()]: print(animal.speak())
What happens to composed parts when the owner is destroyed?
A) They move to another owner
B) They continue to exist independently
C) They are destroyed too
D) They become global variables
In composition, parts are owned by the parent — when the owner is destroyed, so are all its composed parts
Recap: Lecture 05
A circuit breaker in an electrical system is designed to cut off the flow of electricity when there is an overload or a short circuit, preventing fires and equipment damage.
Similarly, exception handling acts as a circuit breaker for your code, stopping the execution flow in the case of an error but also preventing further damage and allowing for a safe recovery or shutdown.
Imagine exception handling as the emergency services (fire department, paramedics, police) in a city.
Just as these services respond to unexpected situations (fires, medical emergencies, crimes), exception handling in programming allows a system to respond to unexpected errors.
Without emergency services ready to respond, situations could escalate quickly, much like unhandled exceptions can cause a program to crash or behave unpredictably.
Exception handling is a programming mechanism that enables developers to manage errors and anomalies that arise during the execution of a program.
In essence, it allows a program to continue running or gracefully terminate when it encounters unexpected situations, rather than crashing abruptly.
The purpose of exception handling is not just to prevent software crashes but also to:
It is a fundamental aspect of writing robust, reliable software that can handle the unpredictable nature of real-world operation.
“
Roses are Red,
Violets are Blue,
Unexpected '{'
on line 32.
”
When writing a program, we, more often than not, will encounter errors.
Error caused by not following the proper structure (syntax) of the language is called a syntax error or parsing error.
Syntax errors are detected by the interpreter during the parsing stage, before the execution begins. Examples include missing colons, incorrect indentation, or unmatched parentheses.
Incorrect version:
if True print("This will cause a syntax error because the colon is missing.")
Error Message:
File "<stdin>", line 1 if True ^ SyntaxError: invalid syntax
Corrected version:
if True: print("This will cause a syntax error because the colon is missing.")
Incorrect version:
def my_function(): print("Hello") # This line should be indented.
Error Message:
File "<stdin>", line 2 print("Hello") ^ IndentationError: expected an indented block
Corrected version:
def my_function(): print("Hello")
These are mistakes in the program's logic that lead to incorrect behavior or results.
Logical errors do not prevent the code from running but can lead to unexpected outcomes.
# Incorrect: overwrites instead of accumulating total = 10 prices = [2, 5, 8] for price in prices: total = price print(f"Total: {total}") # 8, not 25!
Corrected version:
Use += to accumulate instead of = which overwrites.
# Corrected: accumulates properly total = 10 prices = [2, 5, 8] for price in prices: total += price print(f"Total: {total}") # 25
Definition:
Exceptions are events that occur during the execution of a program, disrupting its normal flow.
They often arise from errors in the program's logic or unexpected conditions, such as:
Nature of Exceptions:
Unlike syntax errors, which are identified before the program runs, exceptions can happen at any time during execution. They need to be explicitly handled to maintain the stability and reliability of the software.
Raised when you divide a value or variable with zero
Incorrect version:
print(1 / 0)
Error Message:
ZeroDivisionError: division by zero
Raised when a variable is not found in the local or global scope.
Incorrect version:
print(undefined_variable)
Error Message:
NameError: name 'undefined_variable' is not defined
After learning about errors and exceptions, we will learn to handle them by using try, except, else, and finally blocks.
So, what do we mean by handling them?
In normal circumstances, these errors will stop the code execution and display the error message.
To create stable systems, we need to anticipate these errors and come up with alternative solutions or warning messages.
In this example, we will try to print the undefined x variable.
try: print(x) except: print("An exception has occurred!")
try statementx is not defined, it will run the except statement and print the warningtry: result = 10 / 0 except ZeroDivisionError: print("Caught a division by zero error!")
try block attempts to divide 10 by 0, which raises a ZeroDivisionErrorexcept block catches this specific error and prints a message, preventing the program from crashingtry: x = 1 result = 10 / x except ZeroDivisionError: print("Caught a division by zero error!") except: print("Something else went wrong")
ZeroDivisionError is raised, the program will print "You cannot divide a value with zero."try: with open('data.csv') as file: read_data = file.read() except FileNotFoundError as fnf_error: print(fnf_error) print("Explanation: We cannot load the 'data.csv' file")
Output:
[Errno 2] No such file or directory: 'data.csv' Explanation: We cannot load the 'data.csv' file
We can print default error messages without breaking the execution.
try: choice = input("Enter '1' for ValueError, '2' for TypeError: ") if choice == '1': int('xyz') elif choice == '2': sum('123') except (ValueError, TypeError) as e: print(f"Caught an exception: {e}")
except block catches either a ValueError or a TypeErrortry: value = int(input("Enter a number: ")) except ValueError: print("You must enter a valid integer.") else: print(f"You entered {value}...")
try block: code that might failelse runs only if no exceptionThe finally keyword in the try-except block is always executed, irrespective of whether there is an exception or not.
It is quite useful in cleaning up resources and closing the object, especially closing the files.
try: # Code that might raise an exception except ExceptionType: # Code that runs if an exception occurs finally: # Cleanup code that runs no matter what
try: file = open("example.txt", "r") data = file.read() print(data) except FileNotFoundError: print("The file was not found.") finally: file.close() print("File closed.")
Whether or not the file is found and read, the finally block ensures that the file is attempted to be closed, avoiding a potential resource leak.
def example_function(): try: print("Inside try block") return "Return from try block" except Exception as e: print(f"Exception occurred: {e}") return "Return from except block" else: print("Inside else block") return "Return from else block" finally: print("This is the finally block.") print(example_function())
Output:
Inside try block This is the finally block. Return from try block
The finally block is executed even if the try block has a return statement.
try: print("Performing an operation that doesn't specifically need exception handling.") finally: print("This cleanup code runs no matter what.")
This will ensure that no matter what happens in the try block, the cleanup code in the finally block will always run.
except block — sometimes you only care about cleanup, not about catching the exception.def divide(a, b): try: result = a / b try: print(f"Opening non-existent file.") with open("non_existent_file.txt", "r") as file: data = file.read() except FileNotFoundError as fnf_error: print(f"File error: {fnf_error}") except ZeroDivisionError as zde_error: print(f"Math error: {zde_error}") divide(10, 0) divide(10, 2)
Outer Try-Except Block: Handles division operations. Catches ZeroDivisionError if b is zero.
Inner Try-Except Block: Inside the successful division, attempts to open a non-existent file. Catches FileNotFoundError.
Depending on the inputs, different exceptions can be caught at different levels.
As a Python developer, you have the option to throw an exception if certain conditions are met.
It allows you to interrupt the program based on your requirement.
To throw an exception, we have to use the raise keyword followed by an exception name:
raise ExceptionType("Error message")
try: if age < 0: raise ValueError("Age cannot be negative.") print(f"Age set to {age}") except ValueError as e: print(e)
This example raises a ValueError if a negative age is passed, which is then caught and handled in the try-except block.
def set_age(age): if age < 0: raise ValueError("Age cannot be negative.") print(f"Age set to {age}") try: set_age(-1) except ValueError as e: print(e)
This example raises a ValueError if a negative age is passed, which is then caught and handled in the try-except block.
✅ Validation: To enforce certain conditions or validate inputs. If a function requires a positive integer, you can raise an exception if a negative value is passed.
📣 Error Reporting: To signal errors in a way that's consistent with Python's error handling model, making your code more intuitive to other Python developers.
🔀 Control Flow: In complex functions, to signal that an error condition has occurred, leading to an early exit from a function or triggering specific error handling logic.
Use exceptions to communicate that something unexpected has happened, to enforce correct usage of functions, and to make your code more robust and maintainable.
def divide(a, b): try: result = a / b except ZeroDivisionError: print("Logging: Attempted to divide by zero.") raise # Reraising the exception return result try: divide_result = divide(1, 0) except ZeroDivisionError: print("Caught in the outer try-except block.")
Output:
Logging: Attempted to divide by zero. Caught in the outer try-except block.
The except block catches the exception, prints a logging message, and then uses raise to reraise it.
The reraised exception is propagated upwards, until it reaches the outer try-except block.
In Python, everything is an object,
including exceptions.
This is a fundamental concept in Python's object-oriented programming model.
When an error occurs, Python creates an exception object with useful attributes:
try: int("abc") except ValueError as e: print(type(e)) print(e.args) print(str(e)) print(e.__traceback__)
Output:
<class 'ValueError'> ("invalid literal for int()...",) invalid literal for int()... <traceback object at 0x...>
ValueError
.args — tuple of arguments
.__str__() — error message
.__class__ — exception type
.__traceback__ — call stack
The exception hierarchy you just saw is the same inheritance you learned in Lecture 03:
ZeroDivisionError is-a ArithmeticErrorArithmeticError is-a ExceptionArithmeticError also catches ZeroDivisionError — just like a parent reference can hold a child objectexcept SomeError, you're using inheritance to decide which errors to catch.try: 1 / 0 except ArithmeticError: print("Catches ZeroDivisionError too!") print("Because it inherits from") print("ArithmeticError")
Custom exceptions bundle error data inside the object — self.message, self.value
class ValidationError(Exception): def __init__(self, msg, val): self.message = msg self.value = val
Every custom exception calls super().__init__() — same pattern as any child class
class ValidationError(Exception): def __init__(self, msg): super().__init__(msg)
Multiple except blocks — same structure, different behavior based on exception type
except ValueError: print("Bad value") except TypeError: print("Wrong type")
Custom exceptions are defined by subclassing Python's built-in Exception class.
The simplest form:
class MyCustomError(Exception): pass
With a constructor:
class ValidationError(Exception): def __init__(self, message, value): self.message = message self.value = value super().__init__(message)
class ValidationError(Exception): def __init__(self, message, value): self.message = message self.value = value super().__init__(message)
def validate_age(age): if age < 0: raise ValidationError("Age cannot be negative", age) print(f"Valid age: {age}") try: validate_age(-1) except ValidationError as e: print(f"Error: {e.message} - Invalid Value: {e.value}")
🏭
Always inherit from Exception or its subclasses. Avoid BaseException directly.
class MyError(Exception): # good pass
🏷️
End exception names with "Error" suffix. Makes intent immediately clear.
ValidationError # good InvalidInput # unclear
📄
Add docstrings explaining when it's raised and any extra attributes.
class AgeError(Exception): """Raised for invalid age"""
The assert statement in Python is a debugging aid that tests a condition as an internal self-check in your program.
It is intended as a debugging aid, not as a mechanism for handling run-time errors.
assert condition, "Optional error message"
condition: If True, nothing happens. If False, an AssertionError is raisedAssertionError will be raised without a messagedef calculate_age(birth_year, current_year): assert current_year >= birth_year, "Current year must be greater than or equal to birth year" return current_year - birth_year age = calculate_age(1990, 2022) # This will pass age = calculate_age(2022, 1990) # This will raise an AssertionError
Output:
AssertionError: Current year must be greater than or equal to birth year
-O flag, which removes all assert statementsassert for validating data or security decisions — since it can be disabled, it could introduce vulnerabilitiesLogging exceptions is a crucial practice in software development, allowing developers to record errors that occur during the execution of a program.
This practice not only aids in debugging by providing a historical record of what went wrong and when, but it also helps in monitoring the health of applications in production.
import logging try: 1 / 0 except ZeroDivisionError: logging.exception("Exception occurred")
Output: 2023-04-04 12:30:55 - root - ERROR - Exception occurred
2023-04-01 09:15:30 - root - DEBUG - This is a debug message 2023-04-02 10:20:45 - root - INFO - This is an info message 2023-04-03 11:25:50 - root - WARNING - This is a warning message 2023-04-04 12:30:55 - root - ERROR - This is an error message 2023-04-05 13:35:00 - root - CRITICAL - This is a critical message
2023-04-06 08:00:00 - webapp - INFO - Starting the web application. 2023-04-06 08:00:05 - webapp - DEBUG - Loading configuration from 'config.yaml'. 2023-04-06 08:00:06 - webapp - INFO - Configuration loaded successfully. 2023-04-06 08:00:10 - webapp - INFO - Web server is running on http://localhost:5000 2023-04-06 08:00:10 - webapp - DEBUG - Registered routes: /home, /login, /api/users, /api/orders 2023-04-06 08:05:23 - webapp - INFO - Received GET request for /home from 192.168.1.42 2023-04-06 08:05:23 - webapp - DEBUG - Serving page from template 'home.html'. Render time: 12ms. 2023-04-06 08:10:37 - webapp - INFO - Received POST request for /login from 192.168.1.42 2023-04-06 08:10:38 - webapp - WARNING - Login attempt with unrecognized username 'admin'. 2023-04-06 08:10:39 - webapp - WARNING - 3 failed login attempts from 192.168.1.42 in the last 5 minutes. 2023-04-06 08:12:15 - webapp - INFO - Received GET request for /api/orders from 192.168.1.50 2023-04-06 08:12:15 - webapp - DEBUG - Querying database: SELECT * FROM orders WHERE status='pending' 2023-04-06 08:15:42 - webapp - ERROR - Database connection error on query: SELECT * FROM users WHERE username='admin'. 2023-04-06 08:15:42 - webapp - ERROR - Retry 1/3 failed. Connection refused by db-server:5432. 2023-04-06 08:15:43 - webapp - ERROR - Retry 2/3 failed. Connection refused by db-server:5432. 2023-04-06 08:15:44 - webapp - ERROR - Retry 3/3 failed. Giving up. 2023-04-06 08:15:44 - webapp - CRITICAL - Unable to handle request due to database failure. Shutting down web server.
Configure logging with basicConfig() at the start of your program:
import logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", filename="app.log" )
level — minimum severity to captureformat — structure of each log linefilename — write to file instead of console📈 Severity levels (low to high):
DEBUG = 10INFO = 20WARNING = 30ERROR = 40CRITICAL = 50In real applications, use getLogger(__name__) instead of the root logger. This creates a logger named after your module:
import logging logger = logging.getLogger(__name__) def process_order(order_id): logger.info(f"Processing order {order_id}") try: validate(order_id) except ValueError as e: logger.error(f"Invalid order {order_id}: {e}") raise
| print() | logging | |
|---|---|---|
| 📈 Severity levels | ❌ None | ✅ DEBUG, INFO, WARNING, ERROR, CRITICAL |
| 📄 Output to file | ❌ Manual redirect | ✅ Built-in filename parameter |
| ⏱️ Timestamps | ❌ Must add manually | ✅ Automatic via format |
| 🚫 Toggle on/off | ❌ Delete or comment out | ✅ Change level to hide messages |
| 🎓 Production use | ❌ Never | ✅ Industry standard |
print() for quick debugging during development. Use logging for everything else.logging.exception within except blocks to include the stack trace automaticallylogging module allows you to insert variable data into your log messagesexcept blocks to avoid unintended side effects and make your error handling more precisefinally blocks or context managers (with statements)except Exception: clauses, which can catch more than you intend and obscure programming errorsPython's philosophy on errors and exceptions is encapsulated in the mantra:
“
It's easier to ask for forgiveness
than permission.
”
This approach, often abbreviated as "EAFP," is deeply ingrained in Python's design,
encouraging the use of try-except blocks for handling potential errors.
try/exceptelse for code that runs only when try succeedsfinally for cleanup that always runsraise to throw your own exceptions for validation & control flowException — name them with "Error" suffixassert is for debugging, not production validationlogging module instead of print for production appsNext up: SOLID Principles