0% found this document useful (0 votes)
4 views70 pages

Chapter 03 Object Oriented Programming and Exceptions in Python

Uploaded by

Aya
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
4 views70 pages

Chapter 03 Object Oriented Programming and Exceptions in Python

Uploaded by

Aya
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 70

Ministry of Higher Education and Scientific Research

University of Oum El Bouaghi


Faculty of Exact Sciences and Natural and Life Sciences
Department of Mathematics and Computer Science

Chapter 03:
Object Oriented Programming
1
1st Master - AI & Data Science

Dr. ZERROUGUI S
What is Object-Oriented Programming (OOP)?
Object-Oriented Programming (OOP) is a
programming paradigm that structures a program
by bundling related data and functions together into
objects. OOP is based on four core principles:
Encapsulation, Abstraction, Inheritance, and
Polymorphism. These principles provide a
framework for organizing and designing software in
a modular and reusable way. 2
What is Object-Oriented Programming (OOP)?
Encapsulation: The bundling of data (attributes) and methods (functions) that
operate on that data within a single unit, usually called a class.
Abstraction: Hiding the internal details and showing only essential features of
the object.
Inheritance: The mechanism by which one class can inherit the attributes and
methods of another class.
Polymorphism: The ability to use the same method or function in different
contexts with different types of data.
In OOP, you work with objects, which are instances of classes. A class defines
the blueprint for creating objects, while objects represent concrete instances of
3
that blueprint, each with its own unique data.
Classes and Objects
•A class is like a blueprint for creating objects. It defines a
set of attributes and methods that the objects created from
the class will have.
•An object is an instance of a class. Each object has its
own attributes (data) and methods (behavior).

In this example, the Car class has two attributes (brand, model)
and one method (drive). The objects car1 and car2 are instances of
the Car class with different attribute values. 4
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model

def drive(self):
print(f"{self.brand} {self.model} is driving.")

# Creating objects
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")

car1.drive() # Output: Toyota Corolla is driving.


car2.drive() # Output: Honda Civic is driving. 5
Attributes and Methods
Attributes are variables that belong to a class or an object. They
represent the state or properties of the object.
Methods are functions that are defined inside a class and describe
the behaviors of an object.

There are two types of attributes:


Instance attributes: These belong to individual objects.
Class attributes: These belong to the class itself and are shared
among all objects of that class.
6
Attributes and Methods
Attributes are variables that belong to a class or an object. They
represent the state or properties of the object.
Methods are functions that are defined inside a class and describe
the behaviors of an object.

There are two types of attributes:


Instance attributes: These belong to individual objects.
Class attributes: These belong to the class itself and are shared
among all objects of that class.
7
class Dog:
species = "Canine" # Class attribute

def __init__(self, name, age):


self.name = name # Instance attribute
self.age = age # Instance attribute

def bark(self):
print(f"{self.name} is barking!")

In this example, species is a class attribute, while name


and age are instance attributes. 8
Constructor (__init__ method)
The constructor is a special method in Python, called __init__(),
which is automatically called when an object is created. It is used to
initialize the object's attributes.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

person1 = Person("Alice", 30)


print(person1.name) # Output: Alice
print(person1.age) # Output: 30 9
Defining a Class
A class is a user-defined data structure in Python that serves as a blueprint
for creating objects. A class encapsulates data (attributes) and functions
(methods) that operate on that data.
Basic Class Structure
The simplest way to define a class in Python is by using the class keyword
followed by the class name (by convention, class names are written in
CamelCase).
In this example, Car is a class with
no attributes or methods. We can
class Car: now use this class to create objects,
pass # Empty class although the class currently doesn't
10
do anything.
Defining Attributes and Methods
Classes can have two primary components:
Attributes: Variables associated with the class and its objects.
Methods: Functions that define the behaviors of the objects created from the class.
class Car:
def __init__(self, brand, model):
self.brand = brand # Instance attribute
self.model = model # Instance attribute

def start(self):
print(f"The {self.brand} {self.model} is
11
starting.")
Creating and Using Objects
An object is an instance of a class. Once a class is defined, you can create
objects from it, and each object can have its own values for the class's
attributes.
Creating Objects
You create an object by calling the class as if it were a function.
# Creating an object of the class Car
my_car = Car("Toyota", "Corolla")

Here, my_car is an object (instance) of the class Car. It has the


attributes brand set to "Toyota" and model set to "Corolla". 12
Accessing Attributes and Methods
You can access an object's attributes and methods using dot notation.
print(my_car.brand) # Output: Toyota
print(my_car.model) # Output: Corolla

my_car.start() # Output: The Toyota Corolla is starting.

In this example, we access the brand and model attributes of the


my_car object, and we call its start() method.

13
Modifying Object State
You can modify an object’s attributes directly:
my_car.model = "Camry" # Changing the model
my_car.start() # Output: The Toyota Camry is starting.

Here, we changed the model attribute of the my_car object to


"Camry" and then called the start() method again.

14
The self Parameter
In Python, the self parameter refers to the current instance of the class. It is
used to access instance attributes and methods from within the class.
Every method in a class must take self as its first parameter (though this is
implicit when you call the method).
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model

def start(self):
15
print(f"{self.brand} {self.model} is starting.")
The self Parameter
In the example, the self.brand and self.model refer to the specific attributes
of the object that calls the method. When we create a new object like
my_car, self refers to my_car.

class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model

def start(self):
16
print(f"{self.brand} {self.model} is starting.")
Example Without self (Incorrect)
Without self, Python wouldn’t know which object's attributes and methods
to use.
If we omit self, Python will raise an error because it doesn’t understand
which object's attributes are being referred to.
def start():
print(f"{brand} {model} is starting.") # This will
raise an error

In this case, brand and model are undefined unless self is used. 17
Special Methods (Magic Methods)
Python classes include some special methods, also known as magic
methods or dunder methods (short for "double underscore"). These
methods are called automatically by Python in certain situations, such as
when you create an object or use operators.

The __init__() Method (Constructor)


The __str__() Method
defines how an object is represented as a string. It is called when
you use the print() function on an object or when the object is
converted to a string. 18
Special Methods (Magic Methods)
The __str__() Method
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __str__(self):
return f"{self.name} is {self.age} years old."

person1 = Person("Alice", 30)


print(person1) # Output: Alice is 30 years old.

Without the __str__() method, print(person1) would output something like


<__main__.Person object at 0x7f9d8a0b3b50> (the default string representation of
19
an object).
Special Methods (Magic Methods)
The __repr__() Method
is used for a string representation of the object aimed at developers and
debugging. By convention, __repr__() should return a string that can be
used to recreate the object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __repr__(self):
return f"Person(name='{self.name}', age={self.age})"

person1 = Person("Alice", 30)


print(repr(person1)) # Output: Person(name='Alice', age=30) 20
Operator Overloading with Magic Methods

You can use special methods to define how objects of your class
should behave when used with operators such as +, -, *, ==, etc.
For example, the __add__() method can be used to overload the +
operator.

21
Operator Overloading with Magic Methods
class Vector:
def __init__(self, x, y):
self.x = x In this example, we’ve
self.y = y overloaded the + operator
so that it can add two
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
Vector objects.

def __repr__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)

v3 = v1 + v2 # Calls the __add__ method


print(v3) # Output: Vector(4, 6) 22
Encapsulation is one of the four fundamental principles of Object-
Oriented Programming (OOP). It refers to the bundling of data
(attributes) and methods (functions) that operate on that data within
a single unit, i.e., a class.
Encapsulation restricts direct access to some of an object’s
components, which is essential for:
• Protecting the internal state of an object.
• Preventing unintended interference with data.
• Making code easier to maintain and debug.

23
In Python, encapsulation is primarily implemented through the use
of access modifiers that define how the attributes and methods of a
class can be accessed. Python provides three types of access
modifiers:
Public (accessible from anywhere)
Private (accessible only within the class)
Protected (accessible within the class and its subclasses)

24
Access Modifiers in Python
Public Attributes and Methods
By default, all attributes and methods in Python are public, meaning they
can be accessed and modified directly from outside the class.
class Car:
def __init__(self, brand, model):
Here, both brand and
self.brand = brand # Public attribute
model are public
self.model = model # Public attribute
attributes and can be
accessed and modified
my_car = Car("Toyota", "Corolla")
from outside the class.
print(my_car.brand) # Output: Toyota
my_car.model = "Camry" # Modify the model directly
print(my_car.model) # Output: Camry 25
Access Modifiers in Python

Private Attributes and Methods


To restrict access to certain attributes or methods, you can make them
private. In Python, private attributes or methods are denoted by prefixing
them with two underscores (__).
Private members cannot be accessed or modified directly from outside the
class. This protects the internal state of the object from external
modification.

26
Private Attributes and Methods
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private attribute

def deposit(self, amount): In this example, __balance is a


if amount > 0: private attribute and cannot be
self.__balance += amount accessed directly outside the
class. Instead, the method
def get_balance(self): get_balance() is used to retrieve
return self.__balance # Accessing the private attribute
its value.
# Create a BankAccount object
account = BankAccount("Alice", 1000)
# Accessing the private balance attribute directly will raise an
error
# print(account.__balance) # This will raise an AttributeError

# Accessing balance via the public method


print(account.get_balance()) # Output: 1000
27
Access Modifiers in Python

Protected Attributes and Methods


Protected members are those that should not be accessed from
outside the class but can be accessed in derived classes
(subclasses). In Python, protected attributes or methods are
denoted by a single underscore (_).
Although technically accessible from outside the class, the single
underscore signals to developers that the attribute or method is
intended for internal use only.
28
Protected Attributes and Methods

class Animal:
def __init__(self, name):
self._name = name # Protected attribute

class Dog(Animal):
def display_name(self):
print(f"The dog's name is {self._name}")

dog = Dog("Buddy")
dog.display_name() # Output: The dog's name is Buddy

Here, _name is a protected attribute that can be accessed within the


subclass Dog. It can also be accessed outside the class, though it is
generally discouraged. 29
Getters and Setters
To control access to private or protected attributes, OOP provides getters and setters. These are methods
used to retrieve (get) or modify (set) the values of private/protected attributes.

class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary

def get_salary(self):
return self.__salary
With this method, you ensure
def set_salary(self, new_salary):
if new_salary > 0: that salary changes are validated
self.__salary = new_salary before they are applied.

30
Getters and Setters
Using @property and @setter Decorators
In Python, you can use the @property decorator to define getters and
setters in a more Pythonic way.

31
Getters and Setters
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary

@property
def salary(self):
return self.__salary # Getter method

@salary.setter
def salary(self, new_salary):
if new_salary > 0:
self.__salary = new_salary # Setter method with validation

employee = Employee("Alice", 5000)


print(employee.salary) # Calls the getter, Output: 5000
employee.salary = 6000 # Calls the setter
print(employee.salary) # Output: 6000 32
Inheritance
Inheritance allows a class to inherit attributes and methods from another class. This promotes code reuse
and the creation of a hierarchical structure of classes.

Single Inheritance
In single inheritance, a subclass inherits from one parent class.

class Animal:
def __init__(self, name):
self.name = name

def speak(self):
print(f"{self.name} makes a sound.") dog = Dog("Buddy")
dog.speak() # Output: Buddy makes a
class Dog(Animal): # Dog inherits from Animal sound.
def bark(self): dog.bark() # Output: Buddy barks.
print(f"{self.name} barks.") 33
Multilevel Inheritance
In multilevel inheritance, a class inherits from a parent class, and then another class inherits from that
child class. This creates a chain of inheritance.
class Animal:
def __init__(self, name):
self.name = name

def speak(self): puppy = Puppy("Charlie")


print(f"{self.name} makes a sound.") puppy.speak() # Output: Charlie makes
a sound.
class Dog(Animal): # Dog inherits from Animal puppy.bark() # Output: Charlie
def bark(self): barks.
print(f"{self.name} barks.") puppy.play() # Output: Charlie is
playing.
class Puppy(Dog): # Puppy inherits from Dog
def play(self):
print(f"{self.name} is playing.") 34
Hierarchical Inheritance
In hierarchical inheritance, multiple subclasses inherit from a single parent class.

class Animal:
def __init__(self, name):
self.name = name dog = Dog("Buddy")
dog.speak() # Output: Buddy makes a
def speak(self): sound.
print(f"{self.name} makes a sound.") dog.bark() # Output: Buddy barks.

class Dog(Animal): # Dog inherits from Animal cat = Cat("Whiskers")


def bark(self): cat.speak() # Output: Whiskers makes
print(f"{self.name} barks.") a sound.
cat.meow() # Output: Whiskers meows.
class Cat(Animal): # Cat inherits from Animal
def meow(self):
print(f"{self.name} meows.") 35
Multiple Inheritance
In multiple inheritance, a class inherits from more than one parent class.
This allows the subclass to inherit the behaviors of multiple parent classes.

class Canine:
def bark(self):
print("Barking...")

class Pet:
def play(self):
print("Playing...") dog = Dog()
dog.bark() # Output: Barking...
class Dog(Canine, Pet): # Inherits from both dog.play() # Output: Playing...
Canine and Pet dog.wag_tail() # Output: Wagging tail.
def wag_tail(self):
36
print("Wagging tail.")
Method Overriding
Method overriding allows a subclass to provide its own implementation of a
method that is already defined in its parent class. When a method is
overridden in the child class, the new method will be executed instead of the
parent class method for that class’s objects.
class Animal:
def speak(self):
print("The animal makes a sound.") dog = Dog()
dog.speak() # Output: The dog barks.
class Dog(Animal):
def speak(self): # Overriding the parent
class method
print("The dog barks.")
37
Using super() to Call Parent Class Methods
If you want to extend the behavior of the parent class method instead of completely
overriding it, you can use the super() function to call the parent class method inside the
child class method.
class Animal:
def speak(self):
print("The animal makes a sound.")
dog = Dog()
class Dog(Animal): dog.speak()
def speak(self): # Output:
super().speak() # Call the parent class method # The animal makes a sound.
print("The dog barks.") # The dog barks.

In this example, the super().speak() calls the speak() method from the Animal class,
allowing the child class Dog to build upon the parent class’s behavior.
38
The isinstance() and issubclass() Functions
Python provides two built-in functions, isinstance() and issubclass(), that are useful when
working with inheritance.
isinstance()
The isinstance() function checks if an object is an instance of a specific class or a subclass
of that class.
dog = Dog("Buddy")
print(isinstance(dog, Dog)) # Output: True
print(isinstance(dog, Animal)) # Output: True
print(isinstance(dog, Cat)) # Output: False

In this example, dog is an instance of the Dog class, and since Dog inherits from
Animal, dog is also an instance of the Animal class.
39
The isinstance() and issubclass() Functions
issubclass()
The issubclass() function checks if a class is a subclass of another class.

print(issubclass(Dog, Animal)) # Output: True


print(issubclass(Cat, Dog)) # Output: False

Here, Dog is a subclass of Animal, so issubclass(Dog, Animal) returns True.


However, Cat is not a subclass of Dog, so issubclass(Cat, Dog) returns False.

40
Polymorphism
Polymorphism is one of the core principles of Object-Oriented Programming (OOP).
The term "polymorphism" is derived from the Greek words "poly" (many) and "morph"
(form), meaning "many forms." In OOP, polymorphism allows objects of different classes
to be treated as objects of a common superclass. It enables a single interface to represent
different underlying forms (data types).

Polymorphism enhances the flexibility and maintainability of code by allowing the same
method to behave differently based on the object that invokes it. This is particularly
useful when dealing with a hierarchy of classes that share a common interface or
superclass.

41
Polymorphism
Types of Polymorphism in Python
Polymorphism in Python can be broadly categorized into two types:
1. Compile-Time Polymorphism (Static Polymorphism)
2. Run-Time Polymorphism (Dynamic Polymorphism)

However, it's important to note that Python does not support compile-time
polymorphism in the traditional sense (like method overloading found in languages
such as Java or C++). Instead, Python achieves polymorphism primarily through
run-time mechanisms.

42
Polymorphism
Compile-Time Polymorphism (Static Polymorphism)
Method Overloading: In languages like Java and C++, compile-time polymorphism is achieved through
method overloading, where multiple methods can have the same name but different parameters.
Python's Stance: Python does not support method overloading in the traditional sense. If multiple methods
with the same name are defined in a class, the last definition overwrites the previous ones.
Alternative in Python: To achieve similar functionality, Python uses default arguments or variable-length
arguments.
class MathOperations:
def add(self, a, b, c=0):
return a + b + c

math = MathOperations()
print(math.add(2, 3)) # Output: 5
print(math.add(2, 3, 4)) # Output: 9 43
Polymorphism
Compile-Time Polymorphism (Static Polymorphism)
Method Overloading: Python uses default arguments or variable-length arguments

default arguments

class MathOperations:
def add(self, a, b, c=0):
return a + b + c

math = MathOperations()
print(math.add(2, 3)) # Output: 5
print(math.add(2, 3, 4)) # Output: 9
44
Compile-Time Polymorphism (Static Polymorphism)
Method Overloading: Python uses default arguments or variable-length arguments.
Using Variable-Length Arguments

class Calculator:
def multiply(self, *args):
result = 1
for num in args:
result *= num
return result

calc = Calculator()
print(calc.multiply(2, 3)) # Output: 6
print(calc.multiply(2, 3, 4)) # Output: 24
print(calc.multiply(2, 3, 4, 5)) # Output: 120 45
Compile-Time Polymorphism (Static Polymorphism)
Method Overloading: Python uses default arguments or variable-length arguments.
Using Type Checking
class Example:
def process(self, a, b=None):
if b is not None:
print(f"Processing two arguments: {a} and {b}")
else:
print(f"Processing one argument: {a}")

example = Example()
example.process(5) # Output: Processing one argument: 5
example.process(5, 10) # Output: Processing two arguments: 5 and 10

46
Polymorphism
Run-Time Polymorphism (Dynamic Polymorphism)

Method Overriding: Run-time polymorphism is achieved through method overriding,


where a subclass provides a specific implementation of a method that is already defined in
its superclass.
Duck Typing is a concept related to polymorphism that emphasizes an object's behavior
over its actual type. In Python, this means that if an object implements certain methods or
behaviors, it can be used in a context that expects those behaviors, regardless of the object's
class.

47
class Shape:
def area(self):
pass # Abstract method

class Rectangle(Shape):
def __init__(self, width, height):
Run-Time Polymorphism (Dynamic self.width = width
self.height = height
Polymorphism)
def area(self):
Method Overriding example return self.width * self.height

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return 3.1416 * self.radius ** 2
Output
shapes = [Rectangle(3, 4), Circle(5)]
Area: 12
Area: 78.54 for shape in shapes:
print(f"Area: {shape.area()}")
48
Polymorphism The saying goes, "If it walks like a duck and it quacks
Duck Typing like a duck, then it must be a duck."

class Cat:
def speak(self):
return "Meow"

class Dog:
def speak(self):
return "Bark"

def animal_speak(animal):
print(animal.speak())

animal_speak(Cat()) # Output: Meow


animal_speak(Dog()) # Output: Bark
49
Abstract Classes
An abstract class is a class that cannot be instantiated directly. It typically contains one or more abstract
methods, which are methods that are declared but contain no implementation. Subclasses of the abstract class
must implement these abstract methods.
In Python, abstract classes are defined using the ABC (Abstract Base Class) module from the abc package. The
abstract methods are marked with the @abstractmethod decorator.
from abc import ABC, abstractmethod

class Animal(ABC): # Inheriting from ABC makes this an abstract


class
@abstractmethod
def sound(self):
pass # Abstract method has no implementation

animal = Animal() # This will raise an error: TypeError: Can't instantiate


abstract class Animal with abstract methods sound 50
Abstract Classes
class Dog(Animal):
def sound(self):
return "Bark"

class Cat(Animal):
def sound(self):
return "Meow"

dog = Dog()
cat = Cat()

print(dog.sound()) # Output: Bark


print(cat.sound()) # Output: Meow

51
Abstraction vs. Encapsulation
While both abstraction and encapsulation aim to simplify code and hide
unnecessary details, they are fundamentally different concepts:
Abstraction focuses on hiding the complexity by exposing only the essential
features of an object. It is concerned with the "what" an object does.
Encapsulation is about bundling the data and methods that operate on the
data into a single unit (class) and restricting access to some of the object's
components. It is concerned with the "how" the data is accessed and
modified.

52
MRO (Method Resolution Order) and super() Function
class A:
def say_hello(self):
print("Hello from A")

In the case of multiple inheritance, Python needs a way to class B(A):


determine the order in which it should search the parent def say_hello(self):
classes for a method or attribute. This is known as the print("Hello from B")
Method Resolution Order (MRO). Python follows a
class C(A):
specific algorithm called C3 linearization to determine the
def say_hello(self):
MRO. print("Hello from C")
You can view the MRO of a class using the __mro__
attribute or the mro() method. class D(B, C): # Multiple inheritance
pass

d = D()
d.say_hello() # Output: Hello from B
print(D.__mro__) # Shows the method resolution order

Hello from B 53
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
E-Library Management System

Create a simple e-library management system with


different types of library members, each having
unique permissions and actions.

54
E-Library Management System
•Library:
The Library class should manage a collection of books and support adding,
borrowing, and returning books.
•Books:
Each book should have a title, author, and availability status.
Books can be borrowed if they are available and returned when borrowed.
•Members:
Implement a base class Member to represent general library members.
Create subclasses for different types of members:
StandardMember: Can borrow books but has a borrowing limit.
PremiumMember: Can borrow books without a borrowing limit and receives
recommendations.
•Polymorphism and Method Overriding:
Use polymorphism by defining a borrow() method in each member class, where
each type of member can borrow books according to their specific rules.
55
Errors and Exceptions
56
Error vs Exception
Errors
Errors are issues in code that prevent the program from running correctly. They are usually
syntax or logical mistakes made by the programmer.
Types:
Syntax Errors: Occur when there’s a mistake in the structure or rules of Python code.
These are detected by the Python interpreter before execution.
Logical Errors: Occur when the program runs without crashing, but produces incorrect
results due to faulty logic. These are often harder to detect because the code appears to
function.

Example of a Syntax Error: print("Hello World" # Missing closing parenthesis

def calculate_area(radius):
return 2 * 3.14159 * radius # Incorrect formula for area of a circle
Example of a Logical Error:
print(calculate_area(5)) # Runs but gives wrong result 57
Error vs Exception
Exceptions
Exceptions are issues that occur during the execution of code. They disrupt the normal flow of a program but
can be caught and handled by the program, allowing it to continue running.
Categories of Exceptions:
Built-in Exceptions: Python has numerous built-in exceptions (e.g., ZeroDivisionError, ValueError,
TypeError).
User-defined Exceptions: Programmers can define their own exceptions for custom scenarios by
inheriting from the Exception class.
Exception Handling: Exceptions can be managed using try, except, else, and finally blocks, allowing the
program to handle or recover from errors gracefully.

try:
print(10 / 0)
Example of an Exception: except ZeroDivisionError as e:
print("Error:", e)
58
Error vs Exception
Key Differences

Aspect Error Exception


Due to syntax or logical issues in During runtime, often from
Occurs
code unexpected input or conditions
Typically causes program Can be caught and handled with
Handled
termination try/except blocks
Python interpreter or static Handled dynamically during
Detected By
analysis tools program execution
ValueError, ZeroDivisionError,
Examples SyntaxError, IndentationError
TypeError

59
Basic Exception Handling with try and except
•Using try and except: Wrap code that may fail in a try block and use except to handle
specific exceptions.
•Multiple Exceptions: Handle different exceptions individually to provide customized
responses.

try:
num = int(input("Enter a number: "))
result = 10 / num
print("Result:", result)
except ValueError:
print("Please enter a valid integer.")
except ZeroDivisionError:
print("Cannot divide by zero.")

60
Using else and finally
• else Block: Executes only if no exceptions are raised in the try block.
• finally Block: Always executes, ideal for cleanup actions (e.g., closing files).

try:
file = open("example.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found.")
else:
print(content)
finally:
print("Closing file.")
file.close()

61
Raising Exceptions
raise Keyword: Used to trigger exceptions manually, often with custom error
messages.
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero!")
return a / b

try:
print(divide(10, 0))
except ValueError as e:
print("Error:", e)

Exercise:
Create a function that raises a TypeError if input types aren’t as expected (e.g., strings instead of numbers).
62
Custom Exception Classes
Defining Custom Exceptions: Inherit from Exception to create meaningful,
application-specific error messages.
class NegativeNumberError(Exception):
def __init__(self, value):
self.value = value
super().__init__(f"Negative value not allowed: {self.value}")

def square_root(num):
if num < 0:
raise NegativeNumberError(num)
return num ** 0.5

try:
print(square_root(-4))
except NegativeNumberError as e:
print(e)

63
Exercise: Create a custom PermissionError exception that triggers if a user tries to access a restricted area.
Best Practices in Error Handling
Keep try Blocks Small: Only place risky code in try blocks, not the entire
function.

Use Specific Exceptions: Handle specific errors for better debugging and clarity.

Log Errors: Use logging for error messages instead of print statements for
production code.

64
Best Practices in Error Handling
logging
import logging

logging.basicConfig(level=logging.ERROR)

try:
result = 10 / 0
except ZeroDivisionError:
logging.error("Attempted to divide by zero.")

65
Testing Error Scenarios
Assertions: Use assert to ensure assumptions in code, which can simplify
debugging.Testing Frameworks: Write test cases that simulate exceptions
to verify error handling.

def add_positive_numbers(a, b):


assert a > 0 and b > 0, "Both numbers must be positive"
return a + b

try:
print(add_positive_numbers(-1, 5))
except AssertionError as e:
print("AssertionError:", e)

66
The most common exceptions in Python
SyntaxError: Occurs when there’s a syntax error in the code.
print("Hello" # Missing closing parenthesis

TypeError: Raised when an operation is performed on an inappropriate type.

result = "hello" + 5 # Can't add a string and an integer

ValueError: Raised when a function receives an argument of the right type but inappropriate value.
number = int("abc") # Cannot convert a string that is not a number

NameError: Raised when trying to use a variable or function that has not been defined.

print(variable) # 'variable' is not defined

IndexError: Raised when an index is out of range for a sequence (like a list).
my_list = [1, 2, 3]
67
print(my_list[5]) # Index 5 is out of range
The most common exceptions in Python
KeyError: Raised when trying to access a dictionary key that doesn’t exist.
my_dict = {"name": "Alice"}
print(my_dict["age"]) # 'age' key does not exist
AttributeError: Raised when trying to access an attribute or method that doesn’t exist on an object.
my_list = [1, 2, 3]
my_list.append_new(4) # 'append_new' is not a method of list
ZeroDivisionError: Raised when attempting to divide by zero.
result = 10 / 0 # Cannot divide by zero

FileNotFoundError: Raised when trying to access a file that doesn’t exist.

file = open("non_existent_file.txt", "r") # File not found

ImportError / ModuleNotFoundError: Raised when a module or its function cannot be imported


(module doesn’t exist or is misspelled).
import non_existent_module # No module named 'non_existent_module' 68
The most common exceptions in Python
RuntimeError: Raised for generic errors that don’t fall into any specific category.
# This is a general exception and can be raised by specific libraries for unexpected behavior raise
RuntimeError("An unknown error occurred")

RecursionError: Raised when the maximum recursion depth is exceeded.


def recursive_function():
return recursive_function()
recursive_function() # Exceeds maximum recursion depth

StopIteration: Raised to signal the end of an iterator.


my_iter = iter([1, 2, 3])
next(my_iter)
next(my_iter)
next(my_iter)
next(my_iter) # Raises StopIteration
69
Any questions ?

70

You might also like