Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
4 views

Python Programming Unit-3

This document provides an overview of exceptions in Python, detailing their phases, types, and handling mechanisms. It explains how exceptions are raised, categorized, and managed using try-except blocks, including examples of various built-in exceptions and custom exceptions. Additionally, it covers context management, assertions, and the importance of proper exception handling in programming.

Uploaded by

venkatasai012345
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

Python Programming Unit-3

This document provides an overview of exceptions in Python, detailing their phases, types, and handling mechanisms. It explains how exceptions are raised, categorized, and managed using try-except blocks, including examples of various built-in exceptions and custom exceptions. Additionally, it covers context management, assertions, and the importance of proper exception handling in programming.

Uploaded by

venkatasai012345
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 56

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.

Types of Exceptions in Python:


Python has a wide range of built-in exceptions, which can be broadly categorized into the
following types:
S.No Category Exception
1 Syntax & Parsing Errors SyntaxError, IndentationError, TabError
2 Arithmetic Errors ZeroDivisionError,OverflowError, FloatingPointError
3 Name & Attribute Errors NameError, UnboundLocalError, AttributeError
4 Type & Value Errors TypeError, ValueError
5 Index & Key Errors IndexError, KeyError
6 File & I/O Errors FileNotFoundError, IOError, PermissionError
7 Import Errors ImportError, ModuleNotFoundError
8 System Errors SystemError, SystemExit, KeyboardInterrupt
9 Custom Exceptions User-defined exceptions

1.Syntax and Parsing Errors:


These occur when Python detects incorrect syntax.
SyntaxError – Raised when there is a syntax mistake.
Ex:
if True
print("Hello") # Missing colon (:) causes SyntaxError

IndentationError – Raised when incorrect indentation is used.


def func():
print("Hello") # Incorrect indentation

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

OverflowError – Raised when a number exceeds the maximum limit.


import math
print(math.exp(1000)) # OverflowError

FloatingPointError – Rarely occurs, but related to floating-point calculations.

3. Name and Attribute Errors:


These occur due to issues with variable names or object attributes.
NameError – Raised when using an undefined variable.
print(x) # NameError: x is not defined

UnboundLocalError – Raised when accessing a local variable before assignment.


def func():
print(x) # UnboundLocalError: x is referenced before assignment
x = 10
AttributeError – Raised when accessing an undefined attribute of an object.
class A:
pass
obj = A()
print(obj.attr) # AttributeError

4. Type and Value Errors:


These occur due to incompatible operations.

TypeError – Raised when an operation is performed on incompatible types.


print(5 + "hello") # TypeError

ValueError – Raised when a function receives an argument of the correct type but invalid
value.
int("abc") # ValueError

5. Index and Key Errors:


These occur when accessing elements of sequences or dictionaries.
IndexError – Raised when accessing an out-of-range index.
lst = [1, 2, 3]
print(lst[5]) # IndexError

KeyError – Raised when trying to access a non-existent key in a dictionary.


d = {"a": 1}
print(d["b"]) # KeyError

6. File and I/O Errors:


These occur during file operations.
FileNotFoundError – Raised when trying to open a non-existent file.
open("non_existent.txt") # FileNotFoundError
IOError – General input/output error.
PermissionError – Raised when there is no permission to access a file.
IsADirectoryError – Raised when a directory is opened as a file.

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

raise CustomError ("This is a custom exception!")

Detecting and Handling Exceptions:


Python provides mechanisms to detect and handle exceptions using try-except blocks. This
ensures that errors don’t crash the program, and instead, they can be handled gracefully.
Detecting Exceptions:
Python detects an exception when an error occurs during execution and stops running the code
unless handled.
Example: Unhandled exception
print(10 / 0) # ZeroDivisionError
Output (crashes the program):
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

• To prevent this crash, we use exception handling

Handling Exceptions using try-except:


The try block contains the code that might raise an exception. If an error occurs, the except
block executes.
Syntax:
try:
# Code that might raise an exception
except Exception:
# Code to handle the exception

• 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.

Basic Example: Handling ZeroDivisionError


try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
Output:
Cannot divide by zero!
Handling Multiple Exceptions:
In Python, you can handle multiple exceptions in different ways to ensure your program runs
smoothly even when unexpected errors occur.

Handling Multiple Exceptions Separately


You can use multiple except blocks to handle different exceptions individually.
Syntax:
try:
# Code that might raise an exception
except Exception1:
# Code to handle the exception1
except Exception2:
# Code to handle the exception2
.
.
Example:
try:
num = int(input("Enter a number: ")) # Can cause ValueError
result = 10 / num # Can cause ZeroDivisionError
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
except ValueError:
print("Error: Invalid input! Please enter a number.")

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

• ‘e’ is a variable that stores the exception object.


• It contains details about the error (error message).
• We can print e to see the specific error message.

Using a Generic Exception Handler (Exception)


If you don’t know the exact exception, you can catch all exceptions using ‘Exception’.
Example:
try:
num = int(input("Enter a number: "))
result = 10 / num
except Exception as e:
print(f"An unexpected error occurred: {e}")
Output:
Enter a number: xyz
An unexpected error occurred: invalid literal for int() with base 10: 'xyz'

Using ‘else’ and ‘finally’ with Multiple Exceptions in Python:


When handling exceptions in Python, you can use the else and finally blocks to execute code
under specific conditions:
else Block: Executes if no exceptions occur in the try block.
finally Block: Always executes, regardless of whether an exception occurs or not.

Role of ‘else’ Block:


The else block runs only if no exceptions are raised in the try block. If an exception occurs, the
else block is skipped.
Example
try:
num = int(input("Enter a number: ")) # May raise ValueError
result = 10 / num # May raise ZeroDivisionError
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
except ValueError:
print("Error: Invalid input! Please enter a number.")
else:
print(f"Success! The result is {result}")

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.

Role of ‘finally’ Block:


The finally block always runs, whether an exception occurs or not.
It is commonly used for cleanup tasks (e.g., closing files, releasing resources).
Example:
try:
num = int(input("Enter a number: "))
result = 10 / num
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
except ValueError:
print("Error: Invalid input! Please enter a number.")
else:
print(f"Success! The result is {result}")
finally:
print("Execution completed.") # This always runs

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.

Using with Statement (Built-in Context Manager)


Python provides the with statement to manage resources automatically.
Example: File Handling with ‘with’
with open("example.txt", "w") as file:
file.write("Hello, World!")

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+).

Old Approach: Raising Exceptions as Strings


In older versions of Python, exceptions were sometimes raised using string literals, like this:

raise "This is an error!" # ❌ Invalid in Python 3+

Problem: In Python 3+, this will result in a TypeError:


TypeError: exceptions must derive from BaseException

Correct Approach: Raising Exceptions as Objects


In modern Python (Python 3+), all exceptions must be instances of the BaseException class or
its subclasses.
Example: Raising an Exception as an Object

raise ValueError("This is an error message!") # ✅ Correct approach

Here, ValueError is a built-in exception, and "This is an error message!" is passed as an


argument to it.

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.

Raising Built-in Exceptions


Python provides many built-in exception classes (like ValueError, TypeError,
ZeroDivisionError). You can use raise to trigger these exceptions.

Example: Raising ValueError for Invalid Input


def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative!") # Raising an exception
print(f"Age set to {age}")

set_age(-5) # This will raise ValueError

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.

Example: Re-raising an Exception


try:
x = 10 / 0 # This causes ZeroDivisionError
except ZeroDivisionError:
print("Caught ZeroDivisionError, re-raising it...")
raise # Re-raises the same 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

Raising Exceptions with from (Exception Chaining)


Python allows you to chain exceptions using raise ... from ....
Example: Chaining Exceptions
try:
raise ValueError("Invalid value")
except ValueError as e:
raise TypeError("Type issue occurred") from e

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:

Traceback (most recent call last):


File "script.py", line 4, in <module>
raise TypeError("Type issue occurred") from e
TypeError: Type issue occurred

# It helps track the root cause of errors.

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)"

• If condition is True, the program continues normally.


• If condition is False, an AssertionError is raised with the optional error message.

Example: Using assert for Input Validation


x = 10

assert x > 0, "x must be positive" # ✅ Passes, no error

y = -5

assert y > 0, "y must be positive" # ❌ Fails, raises AssertionError

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.

Example: Checking a List of Numbers


numbers = [1, 2, 3, -4, 5]
for num in numbers:

assert num > 0, f"Invalid number: {num}" # Fails at -4


Output
Traceback (most recent call last):
File "script.py", line 4, in <module>
assert num > 0, f"Invalid number: {num}"
AssertionError: Invalid number: -4

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

# Raising the custom exception


raise CustomError("This is a custom exception")
Output:
Traceback (most recent call last):
File "script.py", line 6, in <module>
raise CustomError("This is a custom exception")
__main__.CustomError: This is a custom exception

Adding a Custom Message:


You can extend the exception class to customize the error message.
Example: Custom Exception with __init__
class AgeError(Exception):
def __init__(self, age, message="Age must be between 0 and 120"):
self.age = age
self.message = message
super().__init__(self.message) # Call Exception class constructor

# Raising the custom exception


age = 150
if age > 120 or age < 0:
raise AgeError(age)
Output:
Traceback (most recent call last):
File "script.py", line 10, in <module>
raise AgeError(age)
__main__.AgeError: Age must be between 0 and 120

Using try-except with Custom Exceptions:


You can catch and handle custom exceptions like any other exception.

Example: Handling a Custom Exception


class NegativeNumberError(Exception):
def __init__(self, value):
super().__init__(f"Negative value not allowed: {value}")

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

Creating a Custom Exception Hierarchy:


You can create a hierarchy of custom exceptions for better organization.
Example: Multiple Custom Exceptions
class ApplicationError(Exception):
"""Base class for other exceptions"""
pass

class DatabaseError(ApplicationError):
"""Raised when a database error occurs"""
pass

class NetworkError(ApplicationError):
"""Raised when a network issue occurs"""
pass

# Raising different custom exceptions


try:
raise DatabaseError("Database connection failed")
except DatabaseError as e:
print(f"Caught a database error: {e}")
except NetworkError as e:
print(f"Caught a network error: {e}")

Output:
Caught a database error: Database connection failed

Using raise from for Exception Chaining:


You can chain exceptions to show the root cause of an error.
Example: Chaining Exceptions
class CustomFileError(Exception):
pass

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

Exceptions and the sys Module:


Python's sys module provides functions and variables that interact with the Python runtime
environment. When handling exceptions, the sys module helps capture, analyze, and display
error details.

Using sys.exc_info() to Get Exception Details:


The function sys.exc_info() returns a tuple containing three values:

• Exception Type (class of the exception).


• Exception Value (error message).
• Traceback Object (stack trace).
Example: Getting Exception Details
import sys

try:

x = 10 / 0 # ❌ This raises ZeroDivisionError


except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
print(f"Exception Type: {exc_type}")
print(f"Exception Message: {exc_value}")

Output:
Exception Type: <class 'ZeroDivisionError'>
Exception Message: division by zero

Using sys.exit() to Gracefully Terminate a Program:


sys.exit() stops program execution immediately.
It can return a custom exit status (default is 0, indicating success).

Example: Exiting a Program on Error


import sys

try:
raise ValueError("Invalid input!")
except ValueError as e:
print(f"Error: {e}")
sys.exit(1) # Exiting with status code 1 (indicating error)

print("This will not be printed!") # Never executes


Output:
Error: Invalid input!

#The program stops execution after sys.exit(1)

Redirecting Error Messages to a File (sys.stderr):


By default, exceptions are printed to the console (sys.stderr).
We can redirect them to a file for logging.

Example: Writing Errors to a Log File


import sys

try:

open("non_existing_file.txt") # ❌ FileNotFoundError
except FileNotFoundError as e:
sys.stderr.write(f"Error: {e}\n") # Redirecting error to stderr

Output (written to stderr)


Error: [Errno 2] No such file or directory: 'non_existing_file.txt'

#Benefit: Useful for logging errors instead of printing them.


Functions and Functional Programming
Function in Python:
A function is one of the most significant parts of Python. A function in Python is a block of
reusable code that performs a specific task. Functions help make the code more organized,
modular, and reusable. One of the primary advantages of Python function is that it can be called
any time if they are defined. Functions are the basic building blocks of a Python program. There
is no restriction on the number of times you can call a function in Python. Thus, the modularity
and segmentation of the program improve. The advantages of using functions in a program are-

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

Components of a Function Definition:


def: The keyword used to define a function.
function_name: The name of the function (should be meaningful).
parameters: Optional inputs the function can accept.
"""docstring""": A string that describes what the function does (optional but
recommended).
return: Returns a value from the function (optional).

Calling a Function in Python:


After creating a function in Python we can call it by using the name of the functions Python
followed by parenthesis containing parameters of that particular function

Syntax:
function_name(argument1, argument2, ...)

Example 1: A Simple Function Without Parameters


def greet():
print("Hello! Welcome to Python.")

# Calling the function


greet()
Output:
Hello! Welcome to Python.
Example 2: Function with Parameters
def add(a, b):
sum_result = a + b
return sum_result # Returning the sum

# Calling the function


result = add(5, 3)
print("Sum:", result)
Output:

Sum: 8

Example 3: Function with Return Statement


def square(number):
return number * number # Returning the square

# Calling the function


result = square(4)
print("Square:", result)

Output:
Square: 16

Function with Default Parameters in Python:


A function with default parameters allows you to define default values for parameters. If an
argument is not passed while calling the function, the function uses the default value.

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.

Example of Function with Default Parameters


def greet(name="Guest"):
print(f"Hello, {name}!")

# Calling the function


greet("Alice") # Passing an argument
greet() # No argument passed, uses default value
Output:
Hello, Alice!
Hello, Guest!

Passing Functions as Arguments in Python:


In Python, functions are first-class objects, meaning they can be assigned to variables, passed
as arguments to other functions, and even returned from functions. This makes Python a highly
flexible language, allowing higher-order functions and functional programming concepts.

Example: Passing a Function as an Argument


Let's pass a function to another function.

def square(n):
return n * n

def apply_function(func, value):


return func(value)
result = apply_function(square, 5)
print("Result:", result)
Output:
Result: 25
Explanation:
• square(n) is a function that squares a number.
• apply_function(func, value) takes a function (func) and a value (value) and applies the
function to the value.
• We pass square as an argument to apply_function, which then executes square(5).

Using Built-in Functions like map() and filter()


Using map()
The map() function applies a function to every element in an iterable.

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.

Syntax of Formal Arguments


def function_name(formal_arg1, formal_arg2, ...):
# Function body

• formal_arg1, formal_arg2, etc., are formal arguments.


• These arguments receive values when the function is called.

Example of Formal Arguments


def greet(name): # 'name' is a formal argument
print(f"Hello, {name}!")
greet("Alice") # "Alice" is an actual argument
greet("Bob") # "Bob" is an actual argument

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}!")

greet("Alice") # Uses "Alice"


greet() # Uses default value "Guest"

3️. Keyword Arguments:


Actual arguments are passed using parameter names, making order irrelevant.

def info(name, age):


print(f"Name: {name}, Age: {age}")

info(age=25, name="John") # Order does not matter


4. Variable-Length Arguments:
Python provides two special types of variable-length arguments that allow functions to accept
an arbitrary number of arguments:

*args (Non-Keyword Variable-Length Arguments) – For multiple positional arguments.


**kwargs (Keyword Variable-Length Arguments) – For multiple keyword arguments.

1. *args – Handling Multiple Positional Arguments:


The *args parameter allows a function to accept any number of positional arguments and stores
them in a tuple.

Syntax:
def function_name(*args):
# Function body

Example: Using *args


def sum_numbers(*args):
total = sum(args)
print(f"Sum: {total}")

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

Example: Using **kwargs


def display_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")

display_info(name="Alice", age=30, country="USA")


display_info(name="Bob", profession="Engineer")

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.

3. Mixing *args and **kwargs:


A function can use both *args and **kwargs together.
def full_details(*args, **kwargs):
print("Positional Arguments:", args)
print("Keyword Arguments:", kwargs)

full_details(1, 2, 3, name="Alice", age=25)

Output:
Positional Arguments: (1, 2, 3)
Keyword Arguments: {'name': 'Alice', 'age': 25}

Explanation:
• *args collects (1, 2, 3).
• **kwargs collects {'name': 'Alice', 'age': 25}.

4. Using *args and **kwargs in Function Calls:


You can unpack lists or dictionaries using * and ** while calling a function.

Example: Unpacking a List into *args


def add_numbers(*args):
return sum(args)

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.

Syntax of a Lambda Function


lambda arguments: expression

• lambda: Keyword to define a lambda function.


• arguments: Inputs (like parameters in a normal function).
• expression: A single expression that is evaluated and returned.

Example of a Lambda Function


square = lambda x: x * x
print(square(5))

Output:
25

Explanation:
• lambda x: x * x defines an anonymous function that squares a number.
• square(5) calls the function and returns 25.

Lambda Function with Multiple Arguments


add = lambda a, b: a + b
print(add(3, 7))

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, ...)

• function: The function to apply to each element.


• iterable: The sequence (list, tuple, set, etc.) that the function will be applied to.
• Additional iterables can be passed for functions that take multiple arguments.

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]

# Using lambda inside map()


result = map(lambda x: x ** 2, numbers)

print(list(result))

Output:
[1, 4, 9, 16, 25]

Using map() with Multiple Iterables


You can use map() with multiple iterables if the function takes more than one argument:

def add(x, y):


return x + y

list1 = [1, 2, 3]
list2 = [4, 5, 6]

# Applying add function to both lists element-wise


result = map(add, list1, list2)

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:

from functools import reduce


numbers = [1, 2, 3, 4, 5]

# Reduce function to sum all numbers


result = reduce(lambda x, y: x + y, numbers)
print(result)

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)

• function: A function that returns True or False (predicate function).


• iterable: The sequence (list, tuple, set, etc.) that will be filtered.
1. Basic Example
Filtering even numbers from a list:

numbers = [1, 2, 3, 4, 5, 6]

# Function to check even numbers


def is_even(num):
return num % 2 == 0

result = filter(is_even, numbers)

print(list(result))

Output:
[2, 4, 6]

Explanation:
Only 2, 4, 6 satisfy num % 2 == 0, so they are included in the output

Using filter() with Lambda Functions


You can replace the function with a lambda expression:

numbers = [1, 2, 3, 4, 5, 6]

# Filtering even numbers using lambda


result = filter(lambda x: x % 2 == 0, numbers)
print(list(result))

Output:
[2, 4, 6]
3. Filtering Strings
Filtering names that start with "A":

names = ["Alice", "Bob", "Anna", "Charlie"]

# Function to check if name starts with 'A'


result = filter(lambda name: name.startswith("A"), names)

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

We can now see that factorial is recursive because factorial(N) = N * factorial(N-1). In


other words, to get the value of factorial(N), one needs to calculate factorial(N-1).
Furthermore, to find factorial(N-1), one needs to computer factorial(N-2), and so on.
We now present the recursive version of the factorial function:

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.

Types of Namespaces in Python:


Python provides different namespaces based on the scope of variables and functions. These
include:
1. Built-in Namespace:
Contains Python’s built-in functions and exceptions (e.g., print(), len(), int(), Exception).
Accessible from anywhere in the program.
Example:
print(len("Hello")) # 'len' belongs to the built-in namespace

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)

4. Enclosing (Nonlocal) Namespace


Present in nested functions.
Used when a function inside another function tries to access variables from the outer function.
Example:
def outer():
z = 30 # Enclosing namespace

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.

Example of Namespaces in Modules


Consider a module module1.py:

essage = "Hello from module1" # Global namespace of module1

def greet():
return "Greetings from module1!"

Now, in another script:

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.

Built-in Modules in Python:


Python comes with a rich standard library that includes many built-in modules. These modules
provide useful functions for different tasks such as math operations, file handling, system
management, networking, and more.

1. Mathematics & Numbers:


Module Purpose
math Provides mathematical functions (e.g., sqrt(), pi, sin())
random Generates random numbers (e.g., randint(), choice(), shuffle())
statistics Provides statistical functions (e.g., mean(), median(),
variance())
decimal High-precision decimal arithmetic
fractions Support for rational number arithmetic

2. System & OS Interaction:


Module Purpose
os Interact with the operating system (e.g., file handling, process
management)
sys Provides system-specific functions and parameters
platform Retrieves system and hardware details
shutil High-level file operations (copy, move, remove)
subprocess Runs system commands

3. Date & Time Handling:


Module Purpose
datetime Manipulate dates and times
time Provides time-related functions (e.g., sleep, timestamp)
calendar Work with calendars

4. File Handling & Data Storage:


Module Purpose
io Handles file streams and I/O operations
csv Read and write CSV files
json Work with JSON data
pickle Serialize and deserialize Python objects
5. Networking & Internet:
Module Purpose
socket Low-level networking (TCP, UDP)
http HTTP handling (requests, responses)
urllib Work with URLs
email Email handling

6. Regular Expressions & String Handling:


Module Purpose
re Regular expressions
string String utilities
textwrap Text formatting

7. Multithreading & Multiprocessing:


Module Purpose
threading Run multiple threads in parallel
multiprocessing Execute multiple processes

8. Debugging & Logging:


Module Purpose
logging Record logs for debugging
traceback Get detailed error traceback

9. Cryptography & Security:


Module Purpose
hashlib Secure hash algorithms (SHA, MD5)
secrets Generate secure random numbers

How to List All Built-in Modules


You can list all built-in modules available in Python using:

import sys
print(sys.builtin_module_names)

will output a list of all built-in modules.


User-defined Modules:
These are Python files created by users to reuse code.

Example:
Create a file my_module.py:

def greet(name):
return f"Hello, {name}!"

Import and use it in another file:


import my_module
print(my_module.greet("Alice")) # Output: Hello, Alice!

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.

1. Different Ways to Import Modules:

1.1 Import the Entire Module


You can import a module using the import keyword.

Example:
import math # Importing the math module

print(math.sqrt(25)) # Using the sqrt() function from math


print(math.pi) # Accessing the constant pi

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.

from math import sqrt, pi

print(sqrt(36)) # Directly using sqrt() without math.sqrt()


print(pi) # Directly using pi

This method reduces memory usage by importing only what you need.

1.3 Importing a Module with an Alias (Renaming a Module):


You can rename a module using the as keyword.
Example:
import numpy as np # numpy is a large module, so we use a short alias

arr = np.array([1, 2, 3])


print(arr)

This method is useful when working with long module names.


1.4 Importing Everything from a Module (Not Recommended):
Using from module_name import *, you can import all functions and variables from a module.

Example:
from math import *

print(sin(90)) # No need to use math.sin()


print(factorial(5))
Note:
• This can cause namespace conflicts if multiple modules have functions with the same
name.
• It makes debugging harder since it's unclear which module a function comes from.

1.5 Importing a Custom (User-Defined) Module


You can create your own Python module and import it into another script.

Example:
Create a file my_module.py:

def greet(name):
return f"Hello, {name}!"

age = 25

Import and use it in another script:


import my_module

print(my_module.greet("Alice")) # Output: Hello, Alice!


print(my_module.age) # Output: 25

1.6 Using dir() to List Module Contents


You can check all available functions and attributes in a module using dir(module_name).
Example:
import math
print(dir(math))

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:

• The current directory


• The directories listed in sys.path
• Standard library directories

To check where Python looks for modules:

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')

3. Re-importing Modules Using importlib:


By default, Python does not reload a module once it has been imported. If you modify a module
and want to reload it without restarting Python, use importlib:

import my_module
import importlib
importlib.reload(my_module)

Module Built-in Functions:


Python provides several built-in functions related to modules to help with importing, reloading,
inspecting, and managing modules efficiently.

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

2. dir() – List Module Contents


Returns a list of all attributes, functions, and classes inside a module.

Example:
import math
print(dir(math))
Output:

['__doc__', '__loader__', '__name__', '__package__', '__spec__',


'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil',
'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'exp',
'factorial', 'floor', 'gcd', 'hypot', 'inf', 'isfinite', 'log', 'pi', 'pow', ...]

• Useful for checking all available functions inside a module.


3. help() – Get Documentation About a Module
Displays detailed documentation of a module.

Example:
import math
help(math)

• This prints the module description, functions, and usage.

4. globals() – Get Global Namespace


Returns a dictionary of all global variables, including imported modules.

Example:
import math
print(globals()) # Shows all globally defined variables, including 'math'

• Helps in debugging and checking all global objects.

5. locals() – Get Local Namespace


Returns a dictionary of all local variables within a function or scope.

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'>

• Confirms that math is a module.

7. isinstance() – Check if an Object is a Module


Used to verify if an object is an instance of a module.

Example:
import math
print(isinstance(math, type(math))) # Output: True

• Useful for checking module types dynamically.

8. sys.modules – List All Imported Modules


Returns a dictionary of all imported modules in the current program.

Example:
import sys
print(sys.modules.keys()) # Lists all imported modules
• Helps in debugging and tracking module imports.

9. sys.path – Check Module Search Path


Returns a list of directories where Python searches for modules.
Example:
import sys
print(sys.path) # Shows paths where Python looks for modules
• Helps troubleshoot module import errors.

10. reload() – Reload a Modified Module


Python does not reload a module once imported unless explicitly instructed using
importlib.reload().

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.

A package is just a directory that:


• Contains multiple modules (Python files)
• Has an __init__.py file (optional in Python 3.3+, but still used)

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

my_package/ # Package name


│── __init__.py # Makes this a package (can be empty or contain setup code)
│── module1.py # First module
│── module2.py # Second module

Step 2: Add Modules to the Package


module1.py
def greet(name):
return f"Hello, {name}!"

module2.py
def add(a, b):
return a + b

__init__.py (Optional)

# __init__.py can be empty or define package-level behavior

from .module1 import greet


from .module2 import add

Importing a Package:
Importing a Module from a Package:

import my_package.module1
print(my_package.module1.greet("Alice"))
Importing a Specific Function:

from my_package.module2 import add


print(add(5, 10)) # Output: 15

Importing Everything (__init__.py Controls It)

from my_package import greet, add


print(greet("Bob")) # Output: Hello, Bob!
print(add(2, 3)) # Output: 5

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

Categories of Auto-Loaded Modules:


1. Built-in Modules (Always Available)
Some modules are always available in Python because they are part of the core interpreter.
Examples include:

builtins → Contains default Python functions (print(), len(), etc.)


sys → Provides access to system-related functionality
warnings → Handles warnings system-wide

Example: builtins Module (Auto-Loaded)


# No need to import "builtins" manually
print(len("Hello")) # 'len' is part of 'builtins'
2. Modules Preloaded in Interactive Shells:
Certain Python environments automatically load modules for convenience.
For example:
• Jupyter auto-loads os and sys for system operations
• Some custom Python shells pre-import debugging tools

Example: Check Preloaded Modules


import sys
print(sys.modules.keys()) # Lists all currently loaded modules

3. Cached Modules (sys.modules)


Once a module is imported, Python caches it in sys.modules. This prevents reloading every
time you import the same module.

Example: Check Cached Modules


import os # First import loads it
import sys

print("os" in sys.modules) # Output: True (cached in memory)

This means the second time you import os, it doesn't reload—it just retrieves it from memory.

How to Control Auto-Loading:


Manually Reload a Module (importlib.reload())
If a module has been modified and cached, you can force a reload:

import importlib
import my_module

importlib.reload(my_module) # Forces reloading of 'my_module'


Preventing Automatic Module Loading:
If a module is preloaded but not needed, you can remove it from sys.modules:

import sys
del sys.modules['os'] # Unloads 'os' from memory

You might also like