Python Programming Unit-3
Python Programming Unit-3
Exceptions
Exceptions in Python:
Exceptions can be described as action or event that is taken outside of the normal flow of
control because of errors. This action comes in two distinct phases, the first being the error
which causes an exception to occur, and the second being the detection (and possible
resolution) phase.
The first phase takes place when an exception condition (sometimes referred to as exceptional
condition) occurs. Upon detection of an error and recognition of the exception condition, the
interpreter performs an operation called raising an exception. Raising is also known as
triggering, throwing, or generating, and is the process whereby the interpreter makes it known
to the current control flow that something is wrong. Python also supports the ability of the
programmer's to raise exceptions.
The second phase is where exception handling takes place. Once an exception is raised, a
variety of actions can be invoked in response to that exception. These can range anywhere from
ignoring the error, logging the error but otherwise taking no action, performing some corrective
measures and aborting the program, or alleviating the problem to allow for resumption of
execution. Any of these actions represents a continuation, or an alternative branch of control.
The key is that the programmer can dictate how the program operates when an error occurs.
TabError – Raised when mixed spaces and tabs are used for indentation.
2. Arithmetic Errors:
These occur during numeric operations.
ZeroDivisionError – Raised when dividing by zero.
print(10 / 0) # ZeroDivisionError
ValueError – Raised when a function receives an argument of the correct type but invalid
value.
int("abc") # ValueError
7. Import Errors
ImportError – Raised when a module cannot be imported.
import nonexistent_module # ImportError
ModuleNotFoundError – A subclass of ImportError, raised when a module is not found.
8. System-Related Errors:
SystemError – Raised when an internal Python error occurs.
SystemExit – Raised when sys.exit() is called.
KeyboardInterrupt – Raised when the user presses Ctrl+C to stop execution.
9. Custom Exceptions:
You can define your own exceptions by subclassing Exception.
class CustomError(Exception):
pass
• try Block: try block lets us test a block of code for errors. Python will “try” to execute
the code in this block. If an exception occurs, execution will immediately jump to the
except block.
• except Block: except block enables us to handle the error or exception. If the code
inside the try block throws an error, Python jumps to the except block and executes it.
We can handle specific exceptions or use a general except to catch all exceptions.
Output-1:
Enter a number: 0
Error: Cannot divide by zero!
Output-2:
Enter a number: abc
Error: Invalid input! Please enter a number.
Handling Multiple Exceptions in a Single ‘except’ Block
You can catch multiple exceptions in one except block using a tuple.
Example
try:
num = int(input("Enter a number: "))
result = 10 / num
except (ZeroDivisionError, ValueError) as e:
print(f"Error: {e}")
Output-1:
Enter a number: abc
Error: invalid literal for int() with base 10: 'abc'
Output-2:
Enter a number: 0
Error: division by zero
Output-1:
Case 1: Valid Input (5)
Enter a number: 5
Success! The result is 2.0 # No exceptions → else executes.
Output-2:
Case 2: Division by Zero (0)
Enter a number: 0
Error: Cannot divide by zero! # Exception occurred → else skipped
Output-3:
Case 3: Invalid Input (xyz)
Enter a number: xyz
Error: Invalid input! Please enter a number. # Exception occurred → else skipped.
Output-1:
Case 1: Valid Input (5)
Enter a number: 5
Success! The result is 2.0
Execution completed. # else runs, and finally runs.
Output-2:
Case 2: Division by Zero (0)
Enter a number: 0
Error: Cannot divide by zero!
Execution completed. # Exception occurred → else skipped, but finally runs
Output-3:
Case 3: Invalid Input (xyz)
Enter a number: xyz
Error: Invalid input! Please enter a number.
Execution completed. # Exception occurred → else skipped, but finally runs.
Context Management:
Context management in Python is a mechanism that automatically manages resources such as
files, database connections, or network sockets. It ensures that resources are properly acquired
and released (e.g., closing files after reading/writing).
Without context management, you must manually handle resource cleanup. For example:
Without context management:
file = open("example.txt", "w") # Open file
file.write("Hello, World!") # Write data
file.close() # Manually close file
Problem: If an error occurs before file.close(), the file might remain open, causing resource
leaks.
Advantages:
No need to manually call file.close().
Even if an exception occurs, Python ensures the file is closed.
Exceptions as Strings:
In early versions of Python (before Python 2.6), exceptions were sometimes represented as
strings instead of objects. However, this approach has been Outdated and is no longer used in
modern Python (Python 3+).
Raising Exceptions:
In Python, you can manually trigger exceptions using the raise statement. This is useful for
enforcing rules, validating input, and handling errors effectively.
Basic Syntax of raise:
The raise keyword is used to generate an exception.
raise Exception("This is an error message!")
Output
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: This is an error message!
The program stops execution and throws an Exception with the given message.
Output
Traceback (most recent call last):
File "script.py", line 5, in <module>
set_age(-5)
File "script.py", line 3, in set_age
raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!
Using raise Without Arguments
You can use raise without arguments to re-raise the current exception.
Output
Caught ZeroDivisionError, re-raising it...
Traceback (most recent call last):
File "script.py", line 2, in <module>
x = 10 / 0
ZeroDivisionError: division by zero
Output
Traceback (most recent call last):
File "script.py", line 2, in <module>
raise ValueError("Invalid value")
ValueError: Invalid value
The above exception was the direct cause of the following exception:
Assertions:
Assertions in Python are used to validate assumptions in the code during runtime. If an
assumption is false, Python raises an AssertionError, stopping the program execution.
Syntax of assert
assert condition, "Error message (optional)"
y = -5
Output
Traceback (most recent call last):
File "script.py", line 5, in <module>
assert y > 0, "y must be positive"
AssertionError: y must be positive
Assertions in Debugging:
Assertions are mainly used for debugging to catch errors early. However, they should not be
used to handle user inputs or replace proper exception handling.
Standard Exceptions:
All standard/built-in exceptions are derived from the root class Exception. There are currently
two immediate subclasses of Exception: SystemExit and StandardError. All other built-in
exceptions are subclasses of StandardError.
Creating Exceptions:
Python allows developers to define their own exceptions by creating custom exception
classes. This is useful when the built-in exceptions (ValueError, TypeError, etc.) are not
specific enough for a particular situation.
Creating a Custom Exception Class:
A custom exception class should inherit from Python's built-in Exception class.
Example: Basic Custom Exception
class CustomError(Exception): # Inheriting from Exception class
pass
def check_number(num):
if num < 0:
raise NegativeNumberError(num) # Raise custom exception
return num
try:
check_number(-5)
except NegativeNumberError as e:
print(f"Error: {e}") # Handles the custom exception
Output:
Error: Negative value not allowed: -5
class DatabaseError(ApplicationError):
"""Raised when a database error occurs"""
pass
class NetworkError(ApplicationError):
"""Raised when a network issue occurs"""
pass
Output:
Caught a database error: Database connection failed
try:
try:
open("missing_file.txt") # FileNotFoundError occurs
except FileNotFoundError as e:
raise CustomFileError("Failed to open file") from e
except CustomFileError as e:
print(f"Error: {e}")
Output
Error: Failed to open file
try:
Output:
Exception Type: <class 'ZeroDivisionError'>
Exception Message: division by zero
try:
raise ValueError("Invalid input!")
except ValueError as e:
print(f"Error: {e}")
sys.exit(1) # Exiting with status code 1 (indicating error)
try:
open("non_existing_file.txt") # ❌ FileNotFoundError
except FileNotFoundError as e:
sys.stderr.write(f"Error: {e}\n") # Redirecting error to stderr
Using functions promotes the reusability of the code. Thus, the programmer does not have to
spend time or effort on writing the same piece of code over and over again.
A Python function can be called multiple times. Additionally, you can call it from anywhere
in the program.
Tracking large programs becomes convenient if it is divided into smaller modules. Thus,
functions allow modularity.
Python allows programmers to create user-defined functions. Thus, it is easy to modify a
function as per the requirements.
Creating Functions:
We can define a function in Python, using the def keyword. The 'def' keyword must come first,
then the function name, any parameters in parenthesis, and then a colon. The code that needs
to be run is indented in the function body. The 'return' statement is optional for a function to
return a value.
Syntax for Defining a Function:
def function_name(parameters):
"""Optional docstring (description of the function)"""
# Function body (statements to execute)
return value # Optional return statement
Syntax:
function_name(argument1, argument2, ...)
Sum: 8
Output:
Square: 16
Syntax:
def function_name(param1=default_value1, param2=default_value2):
# Function body
return result
• param1=default_value1: If no value is provided, it takes the default.
• You can mix required and default parameters, but default parameters must come after
required parameters.
def square(n):
return n * n
def double(n):
return n * 2
numbers = [1, 2, 3, 4]
doubled_numbers = list(map(double, numbers))
print(doubled_numbers)
Output:
[2, 4, 6, 8]
Explanation:
map(double, numbers) applies the double() function to each item in numbers.
Using filter()
The filter() function filters elements based on a function that returns True or False.
def is_even(n):
return n % 2 == 0
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(is_even, numbers))
print(even_numbers)
Output:
[2, 4, 6]
Explanation:
filter(is_even, numbers) keeps only the numbers where is_even(n) returns True.
Formal Arguments:
Formal arguments are the parameters defined in a function when it is declared. They serve as
placeholders for values that are passed when calling the function.
When a function is called, actual arguments are passed to the function, replacing the formal
arguments.
Output:
Hello, Alice!
Hello, Bob!
Explanation:
name is a formal argument in greet(name).
"Alice" and "Bob" are actual arguments passed when calling greet().
Types of Formal Arguments:
Python allows several types of formal arguments
1. Positional Arguments
2. Default Arguments
3. Keyword Arguments
4. Variable-Length Arguments
1. Positional Arguments:
Arguments that must be passed in the correct order.
Example:
def add(a, b):
return a + b
print(add(3, 5)) # 3 and 5 are actual arguments
2. Default Arguments:
Formal arguments with a default value, used when no actual argument is passed.
def greet(name="Guest"):
print(f"Hello, {name}!")
Syntax:
def function_name(*args):
# Function body
sum_numbers(2, 4, 6)
sum_numbers(1, 3, 5, 7, 9)
Output:
Sum: 12
Sum: 25
Explanation:
• *args collects all positional arguments into a tuple.
• We can use functions like sum(args) to process them.
2. **kwargs – Handling Multiple Keyword Arguments:
The **kwargs parameter allows a function to accept any number of keyword arguments,
storing them in a dictionary.
Syntax:
def function_name(**kwargs):
# Function body
Output:
name: Alice
age: 30
country: USA
name: Bob
profession: Engineer
Explanation:
• **kwargs collects all keyword arguments into a dictionary.
• The function loops through and prints key-value pairs.
Output:
Positional Arguments: (1, 2, 3)
Keyword Arguments: {'name': 'Alice', 'age': 25}
Explanation:
• *args collects (1, 2, 3).
• **kwargs collects {'name': 'Alice', 'age': 25}.
numbers = [2, 4, 6, 8]
print(add_numbers(*numbers)) # Unpacking list into *args
Output:
20
Functional Programming:
Anonymous Functions in Python:
In Python, anonymous functions are functions that do not have a name. These functions are
created using the lambda keyword and are also called lambda functions.
Lambda Function in Python
A lambda function in Python is a small, anonymous function that can have multiple arguments
but only one expression. It is used when you need a short function without defining it explicitly
using def.
Output:
25
Explanation:
• lambda x: x * x defines an anonymous function that squares a number.
• square(5) calls the function and returns 25.
Output:
10
Using map() with Lambda:
Applies a function to all elements in a list.
numbers = [1, 2, 3, 4]
squared = list(map(lambda x: x * x, numbers))
print(squared)
Output:
[1, 4, 9, 16]
Built in Functions:
map() :
The map() function in Python is a built-in higher-order function that allows you to apply a
given function to each item in an iterable (like a list or tuple) and return an iterator with the
transformed results.
Syntax
map(function, iterable, ...)
Basic Example
Applying a function to a list:
def square(x):
return x ** 2
numbers = [1, 2, 3, 4, 5]
# Applying square function to each element
result = map(square, numbers)
print(list(result))
Output:
[1, 4, 9, 16, 25]
Using map() with Lambda Functions:
You can use lambda functions to make the code more concise:
numbers = [1, 2, 3, 4, 5]
print(list(result))
Output:
[1, 4, 9, 16, 25]
list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list(result))
Output:
[5, 7, 9]
reduce() :
The reduce() function in Python is used to apply a function cumulatively to the elements of an
iterable, reducing the iterable to a single value. It is part of the functools module.
Syntax
from functools import reduce
reduce(function, iterable, initializer)
• function: A function that takes two arguments and performs a cumulative operation.
• iterable: The sequence (list, tuple, etc.) whose elements are processed.
• initializer (optional): A starting value that is used before processing the iterable.
Basic Example
Finding the sum of all elements in a list:
Output:
15
Explanation:
1+2=3
3+3=6
6 + 4 = 10
10 + 5 = 15
Using reduce() with an Initial Value
If an initializer is provided, it is used as the first value:
numbers = [1, 2, 3, 4]
# Initial value is 10
result = reduce(lambda x, y: x + y, numbers, 10)
print(result)
Output:
20
Explanation:
Starts with 10
10 + 1 = 11
11 + 2 = 13
13 + 3 = 16
16 + 4 = 20
filter() :
The filter() function in Python is a built-in higher-order function that is used to filter
elements from an iterable based on a given condition. It returns an iterator containing only
the elements for which the function returns True.
Syntax
filter(function, iterable)
numbers = [1, 2, 3, 4, 5, 6]
print(list(result))
Output:
[2, 4, 6]
Explanation:
Only 2, 4, 6 satisfy num % 2 == 0, so they are included in the output
numbers = [1, 2, 3, 4, 5, 6]
Output:
[2, 4, 6]
3. Filtering Strings
Filtering names that start with "A":
print(list(result))
Output:
['Alice', 'Anna']
Recursion:
A function is recursive if it contains a call to itself. In other words, a new invocation of the
same function occurs within that function before it finished.
Recursion is used extensively in language recognition as well as in mathematical
applications that use recursive functions. Earlier in this text, we took a first look at the
factorial function where we defined:
N! = factorial(N) = 1 * 2 * 3 … * N
We can also look at factorial this way:
factorial(N) = N!
= N * (N-1)!
= N * (N-1) * (N-2)!
= N * (N-1) * (N-2) … * 3 * 2 * 1
def factorial(n):
if n == 0 or n == 1:# 0! = 1! = 1
return 1
else:
return (n * factorial(n-1))
Modules:
Modules and Files:
A module allows you to logically organize your Python code. When code gets to be large
enough, the tendency is to break it up into organized pieces which can still interact with each
other at a functioning level. These pieces generally have attributes which have some relation
to each other, perhaps a single class with its member data variables and methods, or maybe a
group of related, yet independently operating functions. This process of associating attributes
from other modules with your module is called importing.
If modules represent a logical way to organize your Python code, then files are a way to
physically organize modules. To that end, each file is considered an individual module, and
vice versa. The file name of a module is the module name appended with the .py file extension.
There are several aspects we need to discuss with regards to what the file structure means to
modules. Unlike other languages in which you import classes, in Python you import modules
or module attributes.
Namespaces:
A namespace in Python is a container that holds names (identifiers like variables, functions,
and classes) mapped to their corresponding objects. It helps avoid name conflicts by ensuring
that names are unique within their respective scopes.
2. Global Namespace:
Includes variables and functions defined at the top level of a module. Accessible throughout
the module but not inside local functions.
Example:
x = 10 # 'x' belongs to the global namespace
def show():
print(x) # Accessing the global variable
show()
3. Local Namespace:
Contains names defined inside a function. Accessible only within that function.
Example:
def my_function():
y = 20 # 'y' belongs to the local namespace
print(y)
my_function()
# print(y) # This will cause an error (y is not defined globally)
def inner():
nonlocal z # Refers to the variable 'z' in the outer function
z += 1
print(z)
inner()
outer()
Namespaces in Modules:
Modules in Python have their own namespaces, meaning variables, functions, and classes
defined inside a module do not interfere with variables in another module or script.
def greet():
return "Greetings from module1!"
import module1
print(module1.message) # Accessing module1’s namespace
print(module1.greet()) # Calling function from module1
Each module maintains its own separate namespace, preventing conflicts when importing
multiple modules.
import sys
print(sys.builtin_module_names)
Example:
Create a file my_module.py:
def greet(name):
return f"Hello, {name}!"
Importing Modules:
In Python, a module is a file containing Python code (functions, classes, or variables) that can
be reused in other programs. To use a module in your program, you need to import it.
Example:
import math # Importing the math module
Here, we access functions and variables from the module using dot notation
(module_name.function_name).
1.2 Importing a Specific Function or Variable:
Instead of importing the whole module, you can import only the required functions or variables.
This method reduces memory usage by importing only what you need.
Example:
from math import *
Example:
Create a file my_module.py:
def greet(name):
return f"Hello, {name}!"
age = 25
This prints a list of all available functions and constants inside math.
2. Module Search Path (sys.path):
When you import a module, Python searches for it in the following order:
import sys
print(sys.path)
If Python can’t find the module, you can add a custom directory using:
sys.path.append('/path/to/module')
import my_module
import importlib
importlib.reload(my_module)
Function Description
import Imports a module
dir(module) Lists all functions, classes, and variables inside a module
help(module) Displays detailed documentation for a module
globals() Returns all globally defined variables, including imported
modules
locals() Returns local variables inside a function or scope
type(module) Returns the type of a module (should be <class 'module'>)
isinstance(obj, type(obj)) Checks if an object is a module
sys.modules Lists all currently imported modules
sys.path Shows the directories where Python searches for modules
importlib.reload(module) Reloads a modified module without restarting Python
1. import Statement:
Used to import a module into the current script.
Example:
import math
print(math.sqrt(16)) # Output: 4.0
Example:
import math
print(dir(math))
Output:
Example:
import math
help(math)
Example:
import math
print(globals()) # Shows all globally defined variables, including 'math'
Example:
def my_function():
x = 10
print(locals()) # Shows {'x': 10}
my_function()
• Useful for inspecting function-level variables.
6. type() – Check the Type of a Module
Returns the data type of an object, including modules.
Example:
import math
print(type(math)) # Output: <class 'module'>
Example:
import math
print(isinstance(math, type(math))) # Output: True
Example:
import sys
print(sys.modules.keys()) # Lists all imported modules
• Helps in debugging and tracking module imports.
Example:
import my_module # Assume this is a user-defined module
import importlib
importlib.reload(my_module)
• Useful when modifying and testing a module without restarting the Python interpreter.
Packages:
A package in Python is a collection of modules organized in a directory structure. It allows you
to group related modules together, making your code more modular, reusable, and manageable.
Benfits:
• Organizes large projects into manageable modules
• Avoids module name conflicts by grouping them in different packages
• Encapsulates functionality for better code reuse
Creating a Package in Python:
Let's create a package "my_package" containing two modules.
Step 1: Create the Package Directory
module2.py
def add(a, b):
return a + b
__init__.py (Optional)
Importing a Package:
Importing a Module from a Package:
import my_package.module1
print(my_package.module1.greet("Alice"))
Importing a Specific Function:
Auto-loaded Modules:
Auto-loaded modules in Python refer to modules that are automatically loaded into the Python
runtime environment without explicit import statements from the user. These modules are
either:
• Built-in modules that load at startup (e.g., sys, builtins)
• Preloaded by certain environments or frameworks (e.g., os in some shells)
• Cached modules that are imported once and reused
This means the second time you import os, it doesn't reload—it just retrieves it from memory.
import importlib
import my_module
import sys
del sys.modules['os'] # Unloads 'os' from memory