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

Python Notes Units1-5

The document provides an overview of numeric types in Python, including int, float, and complex data types, along with examples of arithmetic operations. It also covers set operations, including creation, modification, and built-in functions, as well as various mathematical operations like union and intersection. Additionally, it introduces lists in Python, detailing their creation, element access, and methods for adding, removing, and reversing elements.

Uploaded by

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

Python Notes Units1-5

The document provides an overview of numeric types in Python, including int, float, and complex data types, along with examples of arithmetic operations. It also covers set operations, including creation, modification, and built-in functions, as well as various mathematical operations like union and intersection. Additionally, it introduces lists in Python, detailing their creation, element access, and methods for adding, removing, and reversing elements.

Uploaded by

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

UNIT 1

1. Numeric Types in Python (int, Float, and Complex Data Types)


Introduction

In Python, “Numbers” is a category that comprises different types of numeric data.


There are three numeric types in Python:
● int
● float
● complex
Python Integer
Python int is the whole number, including negative numbers but not fractions. In
Python, there is no limit to how long an integer value can be.
Example 1: Creating int and checking type

num = -8
# print the data type
print(type(num))

Output:
<class 'int'>
Example 2: Performing arithmetic Operations on int type

a=5
b=6
c=a+b
print("Addition:",c)
c=a-b
print("Subtraction:",c)
c = a // b
print("Division:",c)
c=a*b
print("Multiplication:",c)
c=a%b
print("Modulus:",c)
c = a ** b
print("Exponent:",c)

Output:
Addition: 11
Subtraction: 3
Division: 4
Multiplication: 15
Modulus: 0
Exponent: 36
Python Float
This is a real number with a floating-point representation. It is specified by a decimal
point. Optionally, the character e or E followed by a positive or negative integer may
be appended to specify scientific notation.
Examples : 0.5 and -7.823457.
They can be created directly by entering a number with a decimal point, or by using
operations such as division on integers. Extra zeros present at the number’s end are
ignored automatically.
Example 1: Creating float and checking type

num = -8.7844216
# print the data type
print(type(num))
Output:
<class 'float'>
Example 2: Performing arithmetic Operations on the float type

a = 5.5
b = 3.2
c=a+b
print("Addition:", c)
c = a-b
print("Subtraction:", c)
c = a/b
print("Division:", c)
c = a*b
print("Multiplication:", c)
Output
Addition: 8.7
Subtraction: 2.3
Division: 1.71875
Multiplication: 17.6
Python Complex
A complex number is a number that consists of real and imaginary parts. For
example, 2 + 3j is a complex number where 2 is the real component, and 3 multiplied
by j is an imaginary part.
Example 1: Creating Complex and checking type

num = 6 + 9j
print(type(num))

Output:
<class 'complex'>
Example 2: Performing arithmetic operations on complex type

a = 1 + 5j
b = 2 + 3j
c=a+b
print("Addition:",c)
Output:
Addition: (3+8j)
Type Conversion in Python
We can convert one number into the other form by two methods:
i) Using Arithmetic Operations:
We can use operations like addition, and subtraction to change the type of number
implicitly(automatically), if one of the operands is float. This method is not working
for complex numbers.
Example: Type conversion using arithmetic operations
a = 1.6
b=5
c=a+b
print(c)
Output:
6.6
ii) Using built-in functions
We can also use built-in functions like int(), float() and complex() to convert into
different types explicitly.
Example: Type conversion using built-in functions
a=2
print(float(a))
b = 5.6
print(int(b))
Output:
2.0
5
__________________________________________________________________________

2. SET Operations
Introduction to Python Sets
A set is a collection of unique data, meaning that elements within a set cannot be
duplicated.
For instance, if we need to store information about student IDs, a set is suitable since
student IDs cannot have duplicates.
Create a Set in Python
● In Python, we create sets by placing all the elements inside curly braces {},
separated by commas.
● A set can have any number of items and they may be of different types
(integer, float, tuple, string, etc.).
Example,
student_id = {112, 114, 116, 118, 115}
print('Student ID:', student_id)
vowel_letters = {'a', 'e', 'i', 'o', 'u'}
print('Vowel Letters:', vowel_letters)
mixed_set = {'Hello', 101, -2, 'Bye'}
print('Set of mixed data types:', mixed_set)
Output
Student ID: {112, 114, 115, 116, 118}
Vowel Letters: {'u', 'a', 'e', 'i', 'o'}
Set of mixed data types: {'Hello', 'Bye', 101, -2}

Create an Empty Set in Python


● Empty curly braces {} will make an empty dictionary in Python.
Example, empty_set = set()

Add and Update Set Items in Python


Sets are mutable. However, since they are unordered, indexing has no meaning.
We cannot access or change an element of a set using indexing or slicing. The set
data type does not support it.

Add Items to a Set in Python


In Python, we use the add() method to add an item to a set.
For example,
numbers = {21, 34, 54, 12}
print('Initial Set:',numbers)
numbers.add(32)
print('Updated Set:', numbers)
Output
Initial Set: {34, 12, 21, 54}
Updated Set: {32, 34, 12, 21, 54}

Update Python Set


The update() method is used to update the set with items other collection types
(lists, tuples, sets, etc).
For example,
companies = {'Lacoste', 'Ralph Lauren'}
tech_companies = ['apple', 'google', 'apple']
companies.update(tech_companies)
print(companies)
Output:
{'google', 'apple', 'Lacoste', 'Ralph Lauren'}

Remove an Element from a Set


We use the discard() method to remove the specified element from a set. For
example,
languages = {'Swift', 'Java', 'Python'}
print('Initial Set:',languages)
removedValue = languages.discard('Java')
print('Set after remove():', languages)
Output
Initial Set: {'Python', 'Swift', 'Java'}
Set after remove(): {'Python', 'Swift'}

Built-in Functions with Set

Function Description

all() Returns True if all elements of the set are true (or if the set is empty).

any() Returns True if any element of the set is true. If the set is empty, returns
False.

len() Returns the length (the number of items) in the set.

max() Returns the largest item in the set.


min() Returns the smallest item in the set.

sorted() Returns a new sorted list from elements in the set

sum() Returns the sum of all elements in the set.

Iterate Over a Set in Python


fruits = {"Apple", "Peach", "Mango"}
for fruit in fruits:
print(fruit)
Output
Mango
Peach
Apple
Find Number of Set Elements
We can use the len() method to find the number of elements present in a Set. For
example,
even_numbers = {2,4,6,8}
print('Set:',even_numbers)
print('Total Elements:', len(even_numbers))
Output
Set: {8, 2, 4, 6}
Total Elements: 4

Python Set Operations


Python Set provides different built-in methods to perform mathematical set
operations like union, intersection, subtraction, and symmetric difference.
Union of Two Sets
The union of two sets A and B includes all the elements of sets A and B.

We use the | operator or the union() method to perform the set union operation.
For example,
A = {1, 3, 5}
B = {0, 2, 4}
print('Union using |:', A | B)
print('Union using union():', A.union(B))
Output
Union using |: {0, 1, 2, 3, 4, 5}
Union using union(): {0, 1, 2, 3, 4, 5}

Set Intersection
The intersection of two sets A and B include the common elements between set A
and B.

In Python, we use the & operator or the intersection() method to perform the set
intersection operation.
For example,
A = {1, 3, 5}
B = {1, 2, 3}
print('Intersection using &:', A & B)
print('Intersection using intersection():', A.intersection(B))
Output
Intersection using &: {1, 3}
Intersection using intersection(): {1, 3}

Difference between Two Sets


The difference between two sets A and B include elements of set A that are not
present on set B.

We use the - operator or the difference() method to perform the difference between
two sets.
For example,
A = {2, 3, 5}
B = {1, 2, 6}
print('Difference using &:', A - B)
print('Difference using difference():', A.difference(B))
Output
Difference using &: {3, 5}
Difference using difference(): {3, 5}

Set Symmetric Difference


The symmetric difference between two sets A and B includes all elements of A and B
without the common elements.

In Python, we use the ^ operator or the symmetric_difference() method to perform


symmetric differences between two sets.
For example,
A = {2, 3, 5}
B = {1, 2, 6}
print('using ^:', A ^ B)
print('using symmetric_difference():', A.symmetric_difference(B))
Output
using ^: {1, 3, 5, 6}
using symmetric_difference(): {1, 3, 5, 6}

Check if two sets are equal


We can use the == operator to check whether two sets are equal or not.
For example,
A = {1, 3, 5}
B = {3, 5, 1}
if A == B:
print('Set A and Set B are equal')
else:
print('Set A and Set B are not equal')
Output
Set A and Set B are equal
Other Python Set Methods
Method Description
add() Adds an element to the set
clear() Removes all elements from the set
copy() Returns a copy of the set
difference() Returns the difference of two or more sets as a new set
discard() Removes an element from the set if it is a member.
intersection() Returns the intersection of two sets as a new set
isdisjoint() Returns True if two sets have a null intersection
issubset() Returns True if another set contains this set
issuperset() Returns True if this set contains another set
pop() Removes and returns an arbitrary set element.
remove() Removes an element from the set.
symmetric_difference() Returns the symmetric difference of two sets as a new set
union() Returns the union of sets in a new set
update() Updates the set with the union of itself and others
___________________________________________________________________________

3. Lists in Python
i) Introduction to Lists
ii) Creating a List in Python
iii) Accessing elements from the List
iv) Getting the size of Python list
v) Taking Input of a Python List
vi) Adding Elements to a Python List - append(), insert(), extend()
vii) Reversing a List - reverse(), reversed()
viii) Removing Elements from the List - remove(), pop()
ix) Slicing of a List
x) List Comprehension
xi) List Methods

i) Introduction to Lists
● Python Lists are just like dynamically sized arrays, declared in other languages.
● A list is a collection of things, enclosed in [ ] and separated by commas.
● The list is a sequence data type which is used to store the collection of data.
● Lists are the simplest containers that are an integral part of the Python
language.
● Lists are always heterogeneous. A single list may contain DataTypes like
Integers, Strings, as well as Objects.
● Lists are mutable, and hence, they can be altered even after their creation.

ii) Creating a List in Python


Lists in Python can be created by just placing the sequence inside the square
brackets[]. Unlike Sets, a list doesn’t need a built-in function for its creation of a list.
Note: Unlike Sets, the list may contain mutable elements.

Example 1: Creating a list in Python

# Creating a List
List = []
print("Blank List: ")
print(List)
# Creating a List of numbers
List = [10, 20, 14]
print("\nList of numbers: ")
print(List)
# Creating a List of strings and accessing using index
List = ["Geeks", "For", "Geeks"]
print("\nList Items: ")
print(List[0])
print(List[2])
# Creating a List with mixed type of values Having numbers and
strings
List = [1, 2, 'Geeks', 4, 'For', 6, 'Geeks']
print("\nList with the use of Mixed Values: ")
print(List)
Output
Blank List: []
List of numbers: [10, 20, 14]
List Items:
Geeks
Geeks
List with the use of Mixed Values: [1, 2, 'Geeks', 4, 'For', 6, 'Geeks']

iii) Accessing elements from the List


Use the index operator [ ] to access an item in a list. The index must be an integer.
Nested lists are accessed using nested indexing.

Example 1: Accessing elements from list using index number

List = ["Geeks", "For", "Geeks"]


print("Accessing a element from the list")
print(List[0])
print(List[2])
List = [['Geeks', 'For'], ['Geeks']]
print("Accessing a element from a Multi-Dimensional list")
print(List[0][1])
print(List[1][0])
List = [1, 2, 'Geeks', 4, 'For', 6, 'Geeks']
print("Accessing element using negative indexing")
print(List[-1])
print(List[-3])

Output
Accessing a element from the list
Geeks
Geeks
Accessing a element from a Multi-Dimensional list
For
Geeks
Accessing element using negative indexing
Geeks
For
iv) Getting the size of Python list
● len() is used to get the length of the list.

v) Taking Input of a Python List


We can take the input of a list of elements as string, integer, float, etc. But the
default one is a string.
Example:

# store integers in a list using map, split and strip functions


n = int(input("Enter the size of list : "))
List1 = list(map(int, input("Enter the integer elements:").strip().split()))[:n]
print('The list is:', List1)

Output:
Enter the size of list : 4
Enter the integer elements: 6 3 9 10
The list is: [6, 3, 9, 10]

vi) Adding Elements to a Python List

Using insert() method


● The append() method only works for the addition of elements at the end of
the List.
● For the addition of elements at the desired position, insert() method is used.
● The insert() method requires two arguments(position, value).

Example
List = [1,2,3,4]
print("Initial List: ")
print(List)
List.insert(3, 12)
List.insert(0, 'Geeks')
print("\nList after performing Insert Operation: ")
print(List)
Output
Initial List:
[1, 2, 3, 4]
List after performing Insert Operation:
['Geeks', 1, 2, 3, 12, 4]

Reversing a List
Method 1: A list can be reversed by using the reverse() method in Python.

mylist = [1, 2, 3, 4, 5]
mylist.reverse()
print(mylist)
Output
[ 5, 4, 3, 2, 1]

Method 2: Using the reversed() function:


The reversed() function returns a reverse iterator, which can be converted to a list
using the list() function.
my_list = [1, 2, 3, 4, 5]
reversed_list = list(reversed(my_list))
print(reversed_list)
Output
[5, 4, 3, 2, 1]

Removing Elements from the List


Method 1: Using remove() method
Elements can be removed from the List by using the built-in remove() function.
Remove() method only removes one element at a time, to remove a range of
elements, the iterator is used.

Example 1:
List = [1, 2, 3, 4, 5, 6,7, 8, 9, 10, 11, 12]
print("Initial List: ")
print(List)
List.remove(5)
List.remove(6)
print("\nList after Removal of two elements: ")
print(List)
Output
Initial List:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
List after Removal of two elements:
[1, 2, 3, 4, 7, 8, 9, 10, 11, 12]

Method 2: Using pop() method


pop() function can also be used to remove and return an element from the list, but
by default it removes only the last element of the list, to remove an element from a
specific position of the List, the index of the element is passed as an argument to the
pop() method.
List = [1, 2, 3, 4, 5]
List.pop()
print("\nList after popping an element: ")
print(List)
List.pop(2)
print("\nList after popping a specific element: ")
print(List)
Output
List after popping an element:
[1, 2, 3, 4]
List after popping a specific element:
[1, 2, 4]

Slicing of a List
We can get substrings and sublists using a slice. To print a specific range of elements
from the list, we use the Slice operation. Slice operation is performed on Lists with
the use of a colon(:).

● To print elements from beginning to a range, use: [: Index]


● To print elements from beginning to negative range, use: [:-Index]
● To print elements from a specific Index till the end, use [Index:]
● To print elements from a specific negative Index till the end, use [-Index:]
● To print the whole list in reverse order, use [::-1]

Example
List = ['G', 'E', 'E', 'K', 'S', 'F','O', 'R', 'G', 'E', 'E', 'K', 'S']
print("Initial List: ")
print(List)
Sliced_List = List[3:8]
print("\nSlicing elements in a range 3-8: ")
print(Sliced_List)
Sliced_List = List[5:]
print("\nElements sliced from 5th element till the end: ")
print(Sliced_List)

Output
Initial List:
['G', 'E', 'E', 'K', 'S', 'F', 'O', 'R', 'G', 'E', 'E', 'K', 'S']
Slicing elements in a range 3-8:
['K', 'S', 'F', 'O', 'R']
Elements sliced from 5th element till the end:
['F', 'O', 'R', 'G', 'E', 'E', 'K', 'S']

List Comprehension
Python List comprehensions are used for creating new lists from other iterables like
tuples, strings, arrays, lists, etc. A list comprehension consists of brackets containing
the expression, which is executed for each element along with the for loop to iterate
over each element.

Syntax:
newList = [ expression(element) for element in oldList if condition ]

Example:
# Below list contains square of all odd numbers from range 1 to 10
odd_square = [x ** 2 for x in range(1, 11) if x % 2 == 1]
print(odd_square)

Output
[1, 9, 25, 49, 81]

List Methods
Function Description
Append() Add an element to the end of the list
Extend() Add all elements of a list to another list
Insert() Insert an item at the defined index
Remove() Removes an item from the list
Clear() Removes all items from the list
Index() Returns the index of the first matched item
Count() Returns the count of the number of items passed as an argument
Sort() Sort items in a list in ascending order
Reverse() Reverse the order of items in the list
copy() Returns a copy of the list
pop() Removes and returns the item at the specified index.
_____________________________________________________________________
4. Tuples in Python
Introduction
Tuple is a collection of objects separated by commas.
Tuple is immutable.
Tuples are defined by enclosing values in parentheses () separated by commas.

Creating Python Tuples


Create Tuples using Round Brackets ()
var = ("Geeks", "for", "Geeks")
print(var)

Output:
('Geeks', 'for', 'Geeks')

Accessing Values in Python Tuples


Tuples in Python provide two ways by which we can access the elements of a tuple.

#Using a positive index


var = ("Geeks", "for", "Geeks")
print("Value in Var[0] = ", var[0])
print("Value in Var[1] = ", var[1])
print("Value in Var[2] = ", var[2])

Output:
Value in Var[0] = Geeks
Value in Var[1] = for
Value in Var[2] = Geeks

#Using a negative index


var = (1, 2, 3)
print("Value in Var[-1] = ", var[-1])
print("Value in Var[-2] = ", var[-2])
print("Value in Var[-3] = ", var[-3])

Output:
Value in Var[-1] = 3
Value in Var[-2] = 2
Value in Var[-3] = 1

Different Operations Related to Tuples


Below are the different operations related to tuples in Python:
● Concatenation
● Nesting
● Repetition
● Slicing
● Deleting
● Finding the length
● Multiple Data Types with tuples
● Conversion of lists to tuples
● Tuples in a Loop

Concatenation of Python Tuples


To Concatenate Python Tuples, we will use plus operators(+).
tuple1 = (0, 1, 2, 3)
tuple2 = ('python', 'geek')
print(tuple1 + tuple2)

Output:
(0, 1, 2, 3, 'python', 'geek')

Nesting of Python Tuples


A nested tuple in Python means a tuple inside another tuple.
tuple1 = (0, 1, 2, 3)
tuple2 = ('python', 'geek')
tuple3 = (tuple1, tuple2)
print(tuple3)

Output :
((0, 1, 2, 3), ('python', 'geek'))

Repetition Python Tuples


We can create a tuple of multiple same elements from a single element in that tuple.

tuple3 = ('python',)*3
print(tuple3)

Output:
('python', 'python', 'python')

Slicing Tuples in Python


Slicing a Python tuple means dividing a tuple into small tuples using the indexing
method.
tuple1 = (0 ,1, 2, 3)
print(tuple1[1:])
print(tuple1[::-1])
print(tuple1[2:4])

Output:
(1, 2, 3)
(3, 2, 1, 0)
(2, 3)

Deleting a Tuple in Python


In this example, we are deleting a tuple using ‘del’ keyword. The output will be in the
form of error because after deleting the tuple, it will give a NameError.
tuple3 = ( 0, 1)
del tuple3
print(tuple3)

Output:
NameError: name 'tuple3' is not defined

Finding the Length of a Python Tuple


To find the length of a tuple, we can use Python’s len() function and pass the tuple as
the parameter.
tuple2 = ('python', 'geek')
print(len(tuple2))

Output:
2

Multiple Data Types With Tuple


Tuples in Python are heterogeneous in nature. This means tuples support elements
with multiple datatypes.
tuple_obj = ("immutable",True,23)
print(tuple_obj)

Output :
('immutable', True, 23)

Converting a List to a Tuple


We can convert a list in Python to a tuple by using the tuple() constructor and
passing the list as its parameters.
list1 = [0, 1, 2]
print(tuple(list1))
print(tuple('python'))

Output:
Tuples take a single parameter which may be a list, string, set, or even a
dictionary(only keys are taken as elements), and converts them to a tuple.
(0, 1, 2)
('p', 'y', 't', 'h', 'o', 'n')

Tuples in a Loop
We can also create a tuple with a single element in it using loops.
tup = ('geek',)
n=5
for i in range(int(n)):
tup = (tup,)
print(tup)

Output:
(('geek',),)
((('geek',),),)
(((('geek',),),),)
((((('geek',),),),),)
(((((('geek',),),),),),)

_____________________________________________________________________

5. Dictionaries in Python

A Python dictionary is a data structure that stores the value in key:value pairs.

Example:
Dict = {1: 'Geeks', 2: 'For', 3: 'Geeks'}
print(Dict)
Output:
{1: 'Geeks', 2: 'For', 3: 'Geeks'}

Python Dictionary Syntax

dict_var = {key1 : value1, key2 : value2, …..}

What is a Dictionary in Python?


● Dictionaries in Python is a data structure, used to store values in key:value
format.
● This makes it different from lists, tuples, and arrays as in a dictionary each key
has an associated value.
● Dictionaries are ordered and can not contain duplicate keys.

How to Create a Dictionary


● In Python, a dictionary can be created by placing a sequence of elements
within curly {} braces, separated by a ‘comma’.
● The dictionary holds pairs of values, one being the Key and the other
corresponding pair element being its Key:value.
● Values in a dictionary can be of any data type and can be duplicated, whereas
keys can’t be repeated and must be immutable.
● Dictionary keys are case sensitive, the same name but different cases of Key
will be treated distinctly.
● A dictionary can also be created by the built-in function dict().

Example
Dict = {1: 'Geeks', 2: 'For', 3: 'Geeks'}
print("\nDictionary with the use of Integer Keys: ")
print(Dict)
Dict = {'Name': 'Geeks', 1: [1, 2, 3, 4]}
print("\nDictionary with the use of Mixed Keys: ")
print(Dict)
Dict = dict({1: 'Geeks', 2: 'For', 3: 'Geeks'})
print("\nDictionary with the use of dict(): ")
print(Dict)
Output
Dictionary with the use of Integer Keys:
{1: 'Geeks', 2: 'For', 3: 'Geeks'}
Dictionary with the use of Mixed Keys:
{'Name': 'Geeks', 1: [1, 2, 3, 4]}
Dictionary with the use of dict():
{1: 'Geeks', 2: 'For', 3: 'Geeks'}

Nested Dictionaries
Dict = {1: 'Geeks', 2: 'For',
3: {'A': 'Welcome', 'B': 'To', 'C': 'Geeks'}}
print(Dict)

Output:
{1: 'Geeks', 2: 'For', 3: {'A': 'Welcome', 'B': 'To', 'C': 'Geeks'}}

This example includes a top-level dictionary with keys 1, 2, and 3. The value
associated with key 3 is another dictionary with keys ‘A,’ ‘B,’ and ‘C.’ This showcases
how Python dictionaries can be nested to create hierarchical data structures.
Adding Elements to a Dictionary
The addition of elements can be done in multiple ways. One value at a time can be
added to a Dictionary by defining value along with the key e.g. Dict[Key] = ‘Value’.
Example
Dict = {}
print("Empty Dictionary: ")
print(Dict)
Dict[0] = 'Geeks'
Dict[2] = 'For'
Dict[3] = 1
print("\nDictionary after adding 3 elements: ")
print(Dict)

Output:
Empty Dictionary:
{}
Dictionary after adding 3 elements:
{0: 'Geeks', 2: 'For', 3: 1}

Accessing Elements of a Dictionary


● To access the items of a dictionary refer to its key name. Key can be used
inside square brackets.
● There is also a method called get() that will also help in accessing the element
from a dictionary. This method accepts a key as argument and returns the
value.
Example
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
print("Accessing a element using key:")
print(Dict['name'])
print("Accessing a element using key:")
print(Dict[1])
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
print("Accessing a element using get:")
print(Dict.get(3))

Output:
Accessing a element using key:
For
Accessing a element using key:
Geeks
Accessing a element using get:
Geeks

Accessing an Element of a Nested Dictionary


To access the value of any key in the nested dictionary, use indexing [] syntax.

Dict = {'Dict1': {1: 'Geeks'},


'Dict2': {'Name': 'For'}}
print(Dict['Dict1'])
print(Dict['Dict1'][1])
print(Dict['Dict2']['Name'])

Output:
{1: 'Geeks'}
Geeks
For

Deleting Elements using ‘del’ Keyword


The items of the dictionary can be deleted by using the del keyword as given below.
Example:
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
print("Dictionary =")
print(Dict)
del(Dict[1])
print("Data after deletion Dictionary=")
print(Dict)

Output
Dictionary ={1: 'Geeks', 'name': 'For', 3: 'Geeks'}
Data after deletion Dictionary={'name': 'For', 3: 'Geeks'}

Dictionary Methods

Method Description
dict.clear() Remove all the elements from the dictionary
dict.copy() Returns a copy of the dictionary
dict.get(key, default = “None”) Returns the value of specified key
dict.items() Returns a list containing a tuple for each key value pair
dict.keys() Returns a list containing dictionary’s keys
dict.update(dict2) Updates dictionary with specified key-value pairs
dict.values() Returns a list of all the values of dictionary
pop() Remove the element with specified key
popItem() Removes the last inserted key-value pair
dict.setdefault(key,default= “None”) set the key to the default value if the key is
not specified in the dictionary
dict.has_key(key) Returns true if the dictionary contains the specified key.

Example:
dict1 = {1: "Python", 2: "Java", 3: "Ruby", 4: "Scala"}
dict2 = dict1.copy()
print(dict2)
dict1.clear()
print(dict1)
print(dict2.get(1))
print(dict2.items())
print(dict2.keys())
dict2.pop(4)
print(dict2)
dict2.popitem()
print(dict2)
dict2.update({3: "Scala"})
print(dict2)
print(dict2.values())
Output:
{1: 'Python', 2: 'Java', 3: 'Ruby', 4: 'Scala'}
{}
Python
dict_items([(1, 'Python'), (2, 'Java'), (3, 'Ruby'), (4, 'Scala')])
dict_keys([1, 2, 3, 4])
{1: 'Python', 2: 'Java', 3: 'Ruby'}
{1: 'Python', 2: 'Java'}
{1: 'Python', 2: 'Java', 3: 'Scala'}
dict_values(['Python', 'Java', 'Scala'])
_____________________________________________________________________
UNIT 2
1. While Loop in Python
i) Introduction
ii) Python While Loop Syntax
iii) Using else statement with While Loop
iv) Infinite While Loop in Python

i) Introduction
In Python, a while loop is used to execute a block of statements repeatedly until a
given condition is satisfied. When the condition becomes false, the line immediately
after the loop in the program is executed.

ii) Python While Loop Syntax:


while expression:
statement(s)
All the statements indented by the same number of character spaces after a
programming construct are considered to be part of a single block of code. Python
uses indentation as its method of grouping statements.

Example of Python While Loop


The given Python code uses a ‘while' loop to print “Hello Geek” three times by
incrementing a variable called ‘count' from 1 to 3.
count = 0
while (count < 3):
count = count + 1
print("Hello")
Output
Hello
Hello
Hello

iii) Using else statement with While Loop


The else clause is only executed when Wer while condition becomes false. If We
break out of the loop, or if an exception is raised, it won’t be executed.

Syntax of While Loop with else statement:


while condition:
# execute these statements
else:
# execute these statements

Examples of While Loop with else statement:


count = 0
while (count < 3):
count = count + 1
print("Hello")
else:
print("In Else Block")

Output
Hello
Hello
Hello
In Else Block

iv) Infinite While Loop in Python


If we want a block of code to execute an infinite number of times, we can use the
while loop in Python to do so.
The code uses a ‘while' loop with the condition (count == 0). This loop will only run as
long as count is equal to 0. Since count is initially set to 0, the loop will execute
indefinitely because the condition is always true.

count = 0+U
while (count == 0):
print("Hello Geek")
_____________________________________________________________________

2. For Loop in Python

For loops are used for sequential traversal. For example: traversing a list or string or
array etc. In Python, there is a “for in” loop which is similar to the foreach loop in
other languages. Let us learn how to use for loops in Python for sequential traversals
with examples.
For Loop Syntax:

for iterator_var in sequence:+


statements(s)
It can be used to iterate over a range and iterators.

Example:

The code uses a Python for loop that iterates over the values from 0 to 3 (not
including 4), as specified by the range(0, n) construct. It will print the values of ‘i' in
each iteration of the loop.

n=4
for i in range(0, n):
print(i)

Output
0
1
2
3

Example with List, Tuple, String, and Dictionary Iteration Using for Loops in Python

print("List Iteration")
l = ["Red", "Green", "Blue"]
for i in l:
print(i)
print("\nTuple Iteration")
t = ("Red", "Green", "Blue")
for i in t:
print(i)
print("\nString Iteration")
s = "Hello"
for i in s:
print(i)
print("\nDictionary Iteration")
d = dict()
d['xyz'] = 123
d['abc'] = 345
for i in d:
print("%s %d" % (i, d[i]))
print("\nSet Iteration")
set1 = {1, 2, 3, 4, 5, 6}
for i in set1:
print(i)

Output
List Iteration
Red
Green
Blue

Tuple Iteration
Red
Green
Blue

String Iteration
H
e
l
l
o

Dictionary Iteration
xyz 123
abc 345

Set Iteration
1
2
3
4
5
6

Iterating by the Index of Sequences


We can also use the index of elements in the sequence to iterate. The key idea is to
first calculate the length of the list and then iterate over the sequence within the
range of this length.
list = ["Red", "Green", "Blue"]
for index in range(len(list)):
print(list[index])

Output
Red
Green
Blue

Using else Statement with for Loop in Python

We can also combine the else statement with for loop like in while loop. But as there
is no condition in for loop based on which the execution will terminate so the else
block will be executed immediately after for block finishes execution.

list = ["Red", "Green", "Blue"]


for index in range(len(list)):
print(list[index])
else:
print("Inside Else Block")

Output
Red
Green
Blue
Inside Else Block

Nested Loops in Python

Python programming language allows one loop inside another loop which is called a
nested loop.
Nested Loops Syntax:

for iterator_var in sequence:


for iterator_var in sequence:
statements(s)
statements(s)

A final note on loop nesting is that we can put any type of loop inside of any other
type of loops in Python. For example, a for loop can be inside a while loop or vice
versa.

Example: This Python code uses nested ‘for' loops to create a triangular pattern of
numbers. It iterates from 1 to 4 and, in each iteration, prints the current number
multiple times based on the iteration number. The result is a pyramid-like pattern of
numbers.

for i in range(1, 5):


for j in range(i):
print(i, end=' ')
print()
Output
1
22
333
4444
_____________________________________________________________________
3. If Statement
Python - if, elif, else Conditions

By default, statements in the script are executed sequentially from the first to the
last. If the processing logic requires so, the sequential flow can be altered in two
ways:

Python uses the if keyword to implement decision control. Python's syntax for
executing a block conditionally is as below:

Syntax:
if [boolean expression]:
statement1
statement2
...
statementN
Any Boolean expression evaluating to True or False appears after the if keyword. Use
the : symbol and press Enter after the expression to start a block with an increased
indent. One or more statements written with the same level of indent will be
executed if the Boolean expression evaluates to True.
To end the block, decrease the indentation. Subsequent statements after the block
will be executed out of the if condition. The following example demonstrates the if
condition.
Example: if Condition
price = 50
if price < 100:
print("price is less than 100")
Output
price is less than 100
In the above example, the expression price < 100 evaluates to True, so it will execute
the block. The if block starts from the new line after : and all the statements under
the if condition starts with an increased indentation, either space or tab. Above, the
if block contains only one statement. The following example has multiple statements
in the if condition.

Example: Multiple Statements in the if Block


price = 50
quantity = 5
if price*quantity < 500:
print("price*quantity is less than 500")
print("price = ", price)
print("quantity = ", quantity)

Output
price*quantity is less than 500
price = 50
quantity = 5

Above, the if condition contains multiple statements with the same indentation. If all
the statements are not in the same indentation, either space or a tab then it will
raise an IdentationError.
Example: Invalid Indentation in the Block
price = 50
quantity = 5
if price*quantity < 500:
print("price is less than 500")
print("price = ", price)
print("quantity = ", quantity)
Output
print("quantity = ", quantity)
^
IdentationError: unexpected indent

The statements with the same indentation level as if condition will not consider in
the if block. They will consider out of the if condition.

Example: Out of Block Statements


price = 50
quantity = 5
if price*quantity < 100:
print("price is less than 500")
print("price = ", price)
print("quantity = ", quantity)
print("No if block executed.")
Output
No if block executed.

The following example demonstrates multiple if conditions.


Example: Multiple if Conditions
price = 100
if price > 100:
print("price is greater than 100")
if price == 100:
print("price is 100")0
if price < 100:
print("price is less than 100")
Output
price is 100
Notice that each if block contains a statement in a different indentation, and that's
valid because they are different from each other.
Note
It is recommended to use 4 spaces or a tab as the default indentation level for more
readability.

else Condition
Along with the if statement, the else condition can be optionally used to define an
alternate block of statements to be executed if the boolean expression in the if
condition evaluates to False.
Syntax:
if [boolean expression]:
statement1
statement2
...
statementN
else:
statement1
statement2
...
statementN
As mentioned before, the indented block starts after the : symbol, after the boolean
expression. It will get executed when the condition is True. We have another block
that should be executed when the if condition is False. First, complete the if block by
a backspace and write else, put add the : symbol in front of the new block to begin it,
and add the required statements in the block.
Example: else Condition
price = 50
if price >= 100:
print("price is greater than 100")
else:
print("price is less than 100")
Output
price is less than 100

In the above example, the if condition price >= 100 is False, so the else block will be
executed. The else block can also contain multiple statements with the same
indentation; otherwise, it will raise the IndentationError.

Note that We cannot have multiple else blocks, and it must be the last block.

elif Condition
Use the elif condition is used to include multiple conditional expressions after the if
condition or between the if and else conditions.
Syntax:
if [boolean expression]:
[statements]
elif [boolean expresion]:
[statements]
elif [boolean expresion]:
[statements]
else:
[statements]
The elif block is executed if the specified condition evaluates to True.

Example: if-elif Conditions


price = 100
if price > 100:
print("price is greater than 100")
elif price == 100:
print("price is 100")
elif price < 100:
print("price is less than 100")
Output
price is 100

In the above example, the elif conditions are applied after the if condition. Python
will evalute the if condition and if it evaluates to False then it will evalute the elif
blocks and execute the elif block whose expression evaluates to True. If multiple elif
conditions become True, then the first elif block will be executed.

The following example demonstrates if, elif, and else conditions.

Example: if-elif-else Conditions


price = 50
if price > 100:
print("price is greater than 100")
elif price == 100:
print("price is 100")
else price < 100:
print("price is less than 100")
Output
price is less than 100

All the if, elif, and else conditions must start from the same indentation level,
otherwise it will raise the IndentationError.

Example: Invalid Indentation


price = 50
if price > 100:
print("price is greater than 100")
elif price == 100:
print("price is 100")
else price < 100:
print("price is less than 100")

Output
elif price == 100:
^
IdentationError: unindent does not match any outer indentation level

Nested if, elif, else Conditions


Python supports nested if, elif, and else condition. The inner condition must be with
increased indentation than the outer condition, and all the statements under the one
block should be with the same indentation.

Example: Nested if-elif-else Conditions


price = 50
quantity = 5
amount = price*quantity
if amount > 100:
if amount > 500:
print("Amount is greater than 500")
else:
if amount <= 500 and amount >= 400:
print("Amount is between 400 and 500")
elif amount <= 400 and amount >= 300:
print("Amount is between 300 and 400")
else:
print("Amount is between 200 and 300")
elif amount == 100:
print("Amount is 100")
else:
print("Amount is less than 100")

Output
Amount is between 200 and 500
____________________________________________________________________
4. Functions in Python
Python Functions

Python Functions is a block of statements that return the specific task. The idea is to
put some commonly or repeatedly done tasks together and make a function so that
instead of writing the same code again and again for different inputs, we can do the
function calls to reuse code contained in it over and over again.

Some Benefits of Using Functions


● Increase Code Readability
● Increase Code Reusability

Python Function Declaration


The syntax to declare a function is:

Types of Functions in Python


● Built-in library function: These are Standard functions in Python that are
available to use.
● User-defined function: We can create our own functions based on our
requirements.

Creating a Function in Python


We can define a function in Python, using the def keyword. We can add any type of
functionalities and properties to it as we require. By the following example, we can
understand how to write a function in Python. In this way we can create Python
function definition by using def keyword.

# A simple Python function


def fun():
print("Welcome to GFG")

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.
Example:

# A simple Python function


def fun():
print("Welcome to GFG")

# Driver code to call a function


fun()

Output:
Welcome to GFG

Python Function with Parameters


If We have experience in C/C++ or Java then We must be thinking about the return
type of the function and data type of arguments. That is possible in Python as well
(specifically for Python 3.5 and above).

Python Function Syntax with Parameters


def function_name(parameter: data_type) -> return_type:
# body of the function
return expression

Example

def add(num1: int, num2: int) -> int:


num3 = num1 + num2
return num3

# Driver code
num1, num2 = 5, 15
ans = add(num1, num2)
print(f"The addition of {num1} and {num2} results {ans}.")

Output:

The addition of 5 and 15 results 20.


Types of Python Function Arguments
Python supports various types of arguments that can be passed at the time of the
function call. In Python, we have the following function argument types in Python:

i) Default argument
ii) Keyword arguments (named arguments)
iii) Positional arguments
iv) Arbitrary arguments (variable-length arguments *args and **kwargs)

Default Arguments
A default argument is a parameter that assumes a default value if a value is not
provided in the function call for that argument. The following example illustrates
Default arguments to write functions in Python.

# Python program to demonstrate default arguments


def myFun(x, y=50):
print("x: ", x)
print("y: ", y)
# Driver code
myFun(10)

Output:
x: 10
y: 50

Keyword Arguments

The idea is to allow the caller to specify the argument name with values so that the
caller does not need to remember the order of parameters.

# Python program to demonstrate Keyword Arguments


def student(firstname, lastname):
print(firstname, lastname)

# Keyword arguments
student(firstname='Arun', lastname='Kumar')
student(lastname='Kumar', firstname='Arun')
Output:

Arun Kumar
Arun Kumar

Positional Arguments
We used the Position argument during the function call so that the first argument (or
value) is assigned to name and the second argument (or value) is assigned to age. By
changing the position, or if We forget the order of the positions, the values can be
used in the wrong places, as shown in the Case-2 example below, where 27 is
assigned to the name and Suraj is assigned to the age.

def nameAge(name, age):


print("Hi, I am", name)
print("My age is ", age)

# Driver code
print("Case-1:")
nameAge("Suraj", 27)
print("\nCase-2:")
nameAge(27, "Suraj")

Output:
Case-1:
Hi, I am Suraj
My age is 27
Case-2:
Hi, I am 27
My age is Suraj

Arbitrary Keyword Arguments


In Python Arbitrary Keyword Arguments, *args, and **kwargs can pass a variable
number of arguments to a function using special symbols. There are two special
symbols:
*args in Python (Non-Keyword Arguments)
**kwargs in Python (Keyword Arguments)

Example 1: Variable length non-keywords argument


# Python program to illustrate variable number of arguments
def myFun(*argv):
for arg in argv:
print(arg)

myFun('Hello', 'Welcome', 'to', 'Our Home')

Output:
Hello
Welcome
to
Our Home

Example 2: Variable length keyword arguments


# Python program to illustrate variable number of keyword arguments
def myFun(**kwargs):
for key, value in kwargs.items():
print("%s == %s" % (key, value))
# Driver code
myFun(first='Geeks', mid='for', last='Geeks')

Output:
first == Geeks
mid == for
last == Geeks

Nested Functions
A function that is defined inside another function is known as the inner function or
nested function. Nested functions can access variables of the enclosing scope. Inner
functions are used so that they can be protected from everything happening outside
the function.
# Python program to demonstrate nested functions
def f1():
s = 'Welcome’'
def f2():
print(s)
f2()

# Driver's code
f1()

Output:
Welcome

Anonymous Functions in Python


In Python, an anonymous function means that a function is without a name. As we
already know the def keyword is used to define the normal functions and the lambda
keyword is used to create anonymous functions.

# Python code to illustrate the cube of a number using lambda function


def cube(x): return x*x*x

cube_v2 = lambda x : x*x*x

print(cube(7))
print(cube_v2(7))
Output:
343
343

Recursive Functions in Python


Recursion in Python refers to when a function calls itself. There are many instances
when We have to build a recursive function to solve Mathematical and Recursive
Problems.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)

print(factorial(4))

Output
24
_____________________________________________________________________
5. Exception Handling
Exception Handling

Introduction

Error in Python can be of two types.


● Syntax errors
● Exceptions
Errors are problems in a program due to which the program will stop the execution.
Exceptions are raised when some internal events occur which change the normal
flow of the program.

Different Types of Exceptions in Python:

In Python, there are several built-in Python exceptions that can be raised when an
error occurs during the execution of a program. Here are some of the most common
types of exceptions in Python:
● SyntaxError: This exception is raised when the interpreter encounters a syntax
error such as a misspelled keyword, a missing colon, or an unbalanced
parenthesis.
● TypeError: This exception is raised when an operation is applied to an object
of the wrong type, such as adding a string to an integer.
● NameError: This exception is raised when a variable or function name is not
found in the current scope.
● IndexError: This exception is raised when an index is out of range.
● KeyError: This exception is raised when a key is not found in a dictionary.
● ValueError: This exception is raised when a function is called with an invalid
argument or input, such as trying to convert a string to an integer.
● IOError: This exception is raised when an I/O operation, such as reading or
writing a file fails.
● ZeroDivisionError: This exception is raised when an attempt is made to divide
a number by zero.
● ImportError: This exception is raised when an import statement fails to find or
load a module.

Difference between Syntax Error and Exceptions


Syntax Error: As the name suggests this error is caused by the wrong syntax in the
code. It leads to the termination of the program.
Example:
amount = 10000
if(amount > 2999)
print("We are eligible to purchase")
Exceptions: Exceptions are raised when the program is syntactically correct, but the
code results in an error. This error does not stop the execution of the program,
however, it changes the normal flow of the program.
Example:
marks = 10000
a = marks / 0
print(a)

In the above example, the ZeroDivisionError will be raised as we are trying to divide a
number by 0.

Try and Except Statement – Catching Exceptions


● Try and except statements are used to catch and handle exceptions in Python.
● The statements that can raise exceptions are kept inside the try clause.
● The statements that handle the exception are written inside the except clause.

Example: Here we are trying to access the array element whose index is out of
bound and handle the corresponding exception.
a = [1, 2, 3]
try:
print ("Second element = "+a[1])
print ("Fourth element = "+a[3])
except:
print ("An error occurred")

Output
Second element = 2
An error occurred

In the above example, the statements that can cause the error are placed inside the
try statement (second print statement in our case). The second print statement tries
to access the fourth element of the list which is not there and this throws an
exception. This exception is then caught by the except statement.

Catching Specific Exception


A try statement can have more than one except clause, to specify handlers for
different exceptions. But one handler only will be executed. For example, we can add
IndexError in the above code.

The general syntax for adding specific exceptions are –

try:
# statement(s)
except IndexError:
# statement(s)
except ValueError:
# statement(s)

Example: Catching specific exceptions in the Python

def fun(a):
if a < 4:
b = a/(a-3)
print("Value of b = ", b)
try:
fun(3)
fun(5)
except ZeroDivisionError:
print("ZeroDivisionError Occurred and Handled")
except NameError:
print("NameError Occurred and Handled")

Output
ZeroDivisionError Occurred and Handled
If We comment on the line fun(3), the output will be
NameError Occurred and Handled
The output above is so because as soon as python tries to access the value of b,
NameError occurs.

Try with Else Clause


In Python, We can also use the else clause on the try-except block which must be
present after all the except clauses. The code enters the else block only if the try
clause does not raise an exception.

Example
def AbyB(a , b):
try:
c = ((a+b) / (a-b))
except ZeroDivisionError:
print ("a/b result in 0")
else:
print (c)
AbyB(2.0, 3.0)
AbyB(3.0, 3.0)

Output:
-5.0
a/b result in 0
Finally Keyword in Python
Python provides a keyword finally, which is always executed after the try and except
blocks. The final block always executes after the normal termination of the try block
or after the try block terminates due to some exception.

Syntax:
try:
# Some Code....
except:
# optional block
# Handling of exception (if required)
else:
# execute if no exception
finally:
# Some code .....(always executed)

Example:
try:
k = 5//0
print(k)
except ZeroDivisionError:
print("Can't divide by zero")
finally:
print('This is always executed')

Output:
Can't divide by zero
This is always executed

Raising Exception
The raise statement allows the programmer to force a specific exception to occur.
The sole argument in raise indicates the exception to be raised. This must be either
an exception instance or an exception class (a class that derives from Exception).

try:
raise NameError("Hi there")
except NameError:
print ("An exception")
raise

The output of the above code will simply line printed as “An exception” but a
Runtime error will also occur in the last due to the raise statement in the last line. So,
the output on Wer command line will look like
Traceback (most recent call last):
File "/home/d6ec14ca595b97bff8d8034bbf212a9f.py", line 5, in <module>
raise NameError("Hi there") # Raise Error
NameError: Hi there

Advantages of Exception Handling:


i) Improved program reliability: By handling exceptions properly, We can prevent
Wer program from crashing or producing incorrect results due to unexpected errors
or input.
ii) Simplified error handling: Exception handling allows We to separate error
handling code from the main program logic, making it easier to read and maintain
Wer code.
iii) Cleaner code: With exception handling, we can avoid using complex conditional
statements to check for errors, leading to cleaner code.
iv) Easier debugging: When an exception is raised, the Python interpreter prints a
traceback making it easier to debug Wer code.

Disadvantages of Exception Handling:


i) Performance overhead: Exception handling can be slower as the interpreter has to
perform additional work to catch and handle the exception.
ii) Increased code complexity: Exception handling can make Wer code more
complex, especially if We have to handle multiple types of exceptions.
iii) Possible security risks: Improperly handled exceptions can potentially reveal
sensitive information or create security vulnerabilities in Wer code.
_____________________________________________________________________
Unit 3
1. Classes and Objects
Classes in Python:
● Python is an object oriented programming language.
● A class is a user-defined data type that contains both the data itself and the
methods that may be used to manipulate it.
● Classes serve as a template to create objects.
● Almost everything in Python is an object, with its properties and methods.
● A Class is like an object constructor, or a "blueprint" for creating objects.

Creating Classes in Python


In Python, a class can be created by using the keyword class, followed by the class
name.
The syntax to create a class is given below.
class ClassName:
#statement
In Python, we must notice that each class is associated with a documentation string
which can be accessed by using <class-name>.__doc__. A class contains a statement
suite including fields, constructor, function, etc.
Example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
# This is a method of the Person class that prints a greeting message
print("Hello, my name is " + self.name)

Name and age are the two properties of the Person class. Additionally, it has a
function called greet that prints a greeting.

Objects in Python:
An object is a particular instance of a class with unique characteristics and functions.
After a class has been established, We may make objects based on it. By using the
class constructor, We may create an object of a class in Python. The object's
attributes are initialised in the constructor, which is a special procedure with the
name __init__.
Syntax:
# Declare an object of a class
object_name = Class_Name(arguments)
Example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
print("Hello, my name is " + self.name)

# Create a new instance of Person class and assign it to variable person1


person1 = Person("Ayan", 25)
person1.greet()
Output:
"Hello, my name is Ayan"

The self-parameter
The self-parameter refers to the current instance of the class and accesses the class
variables. We can use anything instead of self, but it must be the first parameter of
any function which belongs to the class.

_ _init_ _ method
In order to make an instance of a class in Python, a specific function called __init__ is
called. Although it is used to set the object's attributes, it is often referred to as a
constructor.

The self-argument is the only one required by the __init__ method. This argument
refers to the newly generated instance of the class. To initialise the values of each
attribute associated with the objects, We can declare extra arguments in the __init__
method.

Class and Instance Variables


All instances of a class exchange class variables. They function independently of any
class methods and may be accessed through the use of the class name. Here's an
illustration:
class Person:
count = 0 # This is a class variable

def __init__(self, name, age):


self.name = name # This is an instance variable
self.age = age
Person.count += 1 # Accessing the class variable using the name of the
class
person1 = Person("Ayan", 25)
person2 = Person("Bobby", 30)
print(Person.count)

Output:
2
Whereas, instance variables are specific to each instance of a class. They are
specified using the self-argument in the __init__ method.

Here's an illustration:
class Person:
def __init__(self, name, age):
self.name = name # This is an instance variable
self.age = age
person1 = Person("Ayan", 25)
person2 = Person("Bobby", 30)
print(person1.name)
print(person2.age)

Output:
Ayan
30

Class variables are created separately from any class methods and are shared by all
class copies. Every instance of a class has its own instance variables, which are
specified in the __init__ method utilising the self-argument.

_____________________________________________________________________
2. Super() Function .
Introduction:
The super() function in Python is a built-in function that allows We to call methods
from a parent (or superclass) in a child (or subclass) context. This function is primarily used
in object-oriented programming to ensure that inherited methods or constructors (such as
__init__()) from a parent class are called in a subclass, allowing We to extend or customize
the behavior of parent classes without losing their functionality.
In simpler terms, super() is used to give access to methods and properties of a
parent class from within a child class. It helps in situations where multiple inheritance or
method overriding is involved, allowing We to avoid repetitive code.

Benefits:
❖ Inheritance and Code Reusability: By using super(), We can reuse the methods and
properties of a parent class in a child class, reducing the need for redundant code.
❖ Avoids Direct Parent Class Reference: The super() function allows We to refer to the
parent class without explicitly naming it, making the code more flexible, especially
when dealing with multiple inheritance.
❖ Supports Multiple Inheritance: When working with multiple inheritance, super()
helps resolve the "diamond problem" by following the method resolution order
(MRO) and ensuring the correct class is called in the inheritance chain.
❖ Ensures Consistent Initialization: In the case of overriding the __init__() method,
super() helps ensure that the initialization of the parent class happens, preventing
errors caused by skipping necessary initializations.
❖ Increases Maintainability: Using super() simplifies modifications to a class hierarchy.
If We rename or refactor parent classes, We don’t need to change subclass code
since super() dynamically refers to the parent class.

How super() Works


In Python, super() returns a temporary object of the superclass that allows We to call its
methods. It works dynamically, meaning it follows the method resolution order (MRO),
which can be checked using the __mro__ attribute or the mro() method.
For example:
class A:
def method(self):
print("Method in class A")
class B(A):
def method(self):
print("Method in class B")
super().method() # Calls method from class A
obj = B()
obj.method()

Output:
Method in class B
Method in class A

Example:
Let's take a more detailed example involving the use of super() with the __init__() method
in a simple inheritance scenario.
Example: Using super() in Class Initialization
# Base or Parent class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
# Subclass or Child class
class Dog(Animal):
def __init__(self, name, breed):
# Calling the parent class's __init__ method using super()
super().__init__(name)
self.breed = breed
def speak(self):
return f"{self.name}, the {self.breed}, barks."

# Subclass or Child class


class Cat(Animal):
def speak(self):
return f"{self.name} meows."

# Creating objects of Dog and Cat


dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")

# Accessing methods
print(dog.speak()) # Output: Buddy, the Golden Retriever, barks.
print(cat.speak()) # Output: Whiskers meows.

Explanation:
Animal Class: This is the parent class with an __init__() method that initializes the name
attribute. The speak() method provides a general message.
Dog Class: This subclass of Animal has its own __init__() method to initialize both the name
and the breed attributes. It uses super() to call the __init__() method of the Animal class. It
also overrides the speak() method to provide a custom message for dogs.
Cat Class: This subclass overrides the speak() method but doesn't need to override __init__()
since it only relies on the name attribute inherited from Animal.

Using super() in Multiple Inheritance


In cases of multiple inheritance, the super() function helps resolve the method call
sequence based on the method resolution order (MRO). Here’s an example:

class A:
def method(self):
print("A's method")

class B(A):
def method(self):
print("B's method")
super().method()

class C(A):
def method(self):
print("C's method")
super().method()

class D(B, C):


def method(self):
print("D's method")
super().method()

obj = D()
obj.method()
Output:
D's method
B's method
C's method
A's method

In this example:
The class D inherits from both B and C.
The super() function ensures that methods are called in the proper order, adhering
to the method resolution order (MRO).
We can check the MRO by calling D.mro() to see the order in which classes are
looked at.
_____________________________________________________________________
3. inheritance Single inheritance and Multiple inheritance
Inheritance in Python

Introduction
● One of the core concepts in object-oriented programming (OOP) languages is
inheritance.
● It is a mechanism that allows We to create a hierarchy of classes that share a
set of properties and methods by deriving a class from another class.
● Inheritance is the capability of one class to derive or inherit the properties
from another class.
● Inheritance allows We to inherit the properties of a class, i.e., base class to
another, i.e., derived class.

Benefits of inheritance are:

The benefits of Inheritance in Python are as follows:


● It represents real-world relationships well.
● It provides the reusability of a code. We don’t have to write the same code
again and again. Also, it allows us to add more features to a class without
modifying it.
● It is transitive in nature, which means that if class B inherits from another class
A, then all the subclasses of B would automatically inherit from class A.
● Inheritance offers a simple, understandable model structure.
● Less development and maintenance expenses result from an inheritance.

Python Inheritance Syntax

The syntax of simple inheritance in Python is as follows:


Class BaseClass:
{Body}
Class DerivedClass(BaseClass):
{Body}

Creating a Parent Class


A parent class is a class whose properties are inherited by the child class. Let’s create
a parent class called Person which has a Display method to display the person’s
information.
# A Python program to demonstrate inheritance
class Person(object):
# Constructor
def __init__(self, name, id):
self.name = name
self.id = id
# To check if this person is an employee
def Display(self):
print(self.name, self.id)
# Driver code
emp = Person("Satyam", 102) # An Object of Person
emp.Display()

Output:
Satyam 102

Creating a Child Class


A child class is a class that drives the properties from its parent class. Here Emp is
another class that is going to inherit the properties of the Person class(base class).

class Emp(Person):
def Print(self):
print("Emp class called")
Emp_details = Emp("Mayank", 103)
# calling parent class function
Emp_details.Display()
# Calling child class function
Emp_details.Print()

Output:
Mayank 103
Emp class called

Example of Inheritance in Python


Let us see an example of simple Python inheritance in which a child class is inheriting
the properties of its parent class. In this example, ‘Person’ is the parent class, and
‘Employee’ is its child class.
# A Python program to demonstrate inheritance
class Person(object):
def __init__(self, name):
self.name = name
def getName(self):
return self.name
def isEmployee(self):
return False
# Inherited or Subclass
class Employee(Person):
def isEmployee(self):
return True
# Driver code
emp = Person("Manoj")
print(emp.getName(), emp.isEmployee())
emp = Employee("Anand")
print(emp.getName(), emp.isEmployee())

Output:
Manoj False
Anand True

Different types of Python Inheritance

There are 5 different types of inheritance in Python. They are as follows:

Single inheritance: When a child class inherits from only one parent class, it is called
single inheritance.
Multiple inheritance: When a child class inherits from multiple parent classes, it is
called multiple inheritances.
Multilevel inheritance: When we have a child and grandchild relationship. This
means that a child class will inherit from its parent class, which in turn is inheriting
from its parent class.
Hierarchical inheritance: More than one derived class can be created from a single
base.
Hybrid inheritance: This form combines more than one form of inheritance.
Basically, it is a blend of more than one type of inheritance.
# Python example to show the working of multiple inheritance

class Base1(object):
def __init__(self):
self.str1 = "Class1"
print("Base1")

class Base2(object):
def __init__(self):
self.str2 = "Class2"
print("Base2")

class Derived(Base1, Base2):


def __init__(self):
# Calling constructors of Base1 and Base2 classes
Base1.__init__(self)
Base2.__init__(self)
print("Derived")
def printStrs(self):
print(self.str1, self.str2)

ob = Derived()
ob.printStrs()

Output:
Base1
Base2
Derived
Class1 Class2

# A Python program to demonstrate Multilevel Inheritance

class Base(object):
# Constructor
def __init__(self, name):
self.name = name
# To get name
def getName(self):
return self.name

# Inherited or Sub class


class Child(Base):
# Constructor
def __init__(self, name, age):
Base.__init__(self, name)
self.age = age
# To get name
def getAge(self):
return self.age

# Inherited or Sub class


class GrandChild(Child):
# Constructor
def __init__(self, name, age, address):
Child.__init__(self, name, age)
self.address = address
# To get address
def getAddress(self):
return self.address

# Driver code
g = GrandChild("Geek1", 23, "Noida")
print(g.getName(), g.getAge(), g.getAddress())

Output:

Geek1 23 Noida

The super() Function


The super() function is a built-in function that returns the objects that represent the
parent class. It allows to access the parent class’s methods and attributes in the child
class.

Example: super() function with simple Python inheritance


In this example, we created the object ‘obj’ of the child class. When we called the
constructor of the child class ‘Student’, it initialized the data members to the values
passed during the object creation. Then using the super() function, we invoked the
constructor of the parent class.

# parent class
class Person():
def __init__(self, name, age):
self.name = name
self.age = age

def display(self):
print(self.name, self.age)

# child class
class Student(Person):
def __init__(self, name, age):
self.sName = name
self.sAge = age
# inheriting the properties of parent class
super().__init__("Rahul", age)

def displayInfo(self):
print(self.sName, self.sAge)

obj = Student("Mayank", 23)


obj.display()
obj.displayInfo()

Output:
Rahul 23
Mayank 23

Adding Properties
One of the features that inheritance provides is inheriting the properties of the
parent class as well as adding new properties of our own to the child class. Let us see
this with an example:
# parent class
class Person():
def __init__(self, name, age):
self.name = name
self.age = age

def display(self):
print(self.name, self.age)

# child class
class Student(Person):
def __init__(self, name, age, dob):
self.sName = name
self.sAge = age
self.dob = dob
# inheriting the properties of parent class
super().__init__("Rahul", age)

def displayInfo(self):
print(self.sName, self.sAge, self.dob)

obj = Student("Mayank", 23, "16-03-2000")


obj.display()
obj.displayInfo()
Output:
Here we can see that we added a new property to the child class, i.e., date of birth
(dob).
Rahul 23
Mayank 23 16-03-2000
_____________________________________________________________________

4. Command Line Arguments


Command Line Arguments in Python

The arguments that are given after the name of the program in the command line
shell of the operating system are known as Command Line Arguments. Python
provides various ways of dealing with these types of arguments. The three most
common are:

Using sys.argv
Using getopt module
Using argparse module

Using sys.argv

The sys module provides functions and variables used to manipulate different parts
of the Python runtime environment. This module provides access to some variables
used or maintained by the interpreter and to functions that interact strongly with the
interpreter.
One such variable is sys.argv which is a simple list structure. It’s main purpose are:

It is a list of command line arguments.


len(sys.argv) provides the number of command line arguments.
sys.argv[0] is the name of the current Python script.

Example: Let’s suppose there is a Python script for adding two numbers and the
numbers are passed as command-line arguments.

# Python program to demonstrate


# command line arguments
import sys
# total arguments
n = len(sys.argv)
print("Total arguments passed:", n)
# Arguments passed
print("\nName of Python script:", sys.argv[0])
print("\nArguments passed:", end = " ")
for i in range(1, n):
print(sys.argv[i], end = " ")

# Addition of numbers
Sum = 0
# Using argparse module
for i in range(1, n):
Sum += int(sys.argv[i])
print("\n\nResult:", Sum)

Output:
python-command-line-arguments

Using getopt module

Python getopt module is similar to the getopt() function of C. Unlike sys module
getopt module extends the separation of the input string by parameter validation. It
allows both short, and long options including a value assignment. However, this
module requires the use of the sys module to process input data properly. To use
getopt module, it is required to remove the first element from the list of command-
line arguments.

Syntax: getopt.getopt(args, options, [long_options])


Parameters:
args: List of arguments to be passed.
options: String of option letters that the script want to recognize. Options that
require an argument should be followed by a colon (:).
long_options: List of string with the name of long options. Options that require
arguments should be followed by an equal sign (=).
Return Type: Returns value consisting of two elements: the first is a list of (option,
value) pairs. The second is the list of program arguments left after the option list was
stripped.

Example:
# Python program to demonstrate
# command line arguments
import getopt, sys
# Remove 1st argument from the
# list of command line arguments
argumentList = sys.argv[1:]
# Options
options = "hmo:"
# Long options
long_options = ["Help", "My_file", "Output="]

try:
# Parsing argument
arguments, values = getopt.getopt(argumentList, options, long_options)
# checking each argument
for currentArgument, currentValue in arguments:
if currentArgument in ("-h", "--Help"):
print ("Displaying Help")

elif currentArgument in ("-m", "--My_file"):


print ("Displaying file_name:", sys.argv[0])

elif currentArgument in ("-o", "--Output"):


print (("Enabling special output mode (% s)") % (currentValue))

except getopt.error as err:


# output error, and return with an error code
print (str(err))
Output:
python-command-line-arguments

Using argparse module

Using argparse module is a better option than the above two options as it provides a
lot of options such as positional arguments, default value for arguments, help
message, specifying data type of argument etc.

Note: As a default optional argument, it includes -h, along with its long version –help.

Example 1: Basic use of argparse module.


# Python program to demonstrate
# command line arguments
import argparse
# Initialize parser
parser = argparse.ArgumentParser()
parser.parse_args()
Output:

python-command-line-arguments
Example 2: Adding description to the help message.

# Python program to demonstrate


# command line arguments
import argparse
msg = "Adding description"
# Initialize parser
parser = argparse.ArgumentParser(description = msg)
parser.parse_args()

Output:
python-command-line-arguments

Example 3: Defining optional value


# Python program to demonstrate
# command line arguments
import argparse
# Initialize parser
parser = argparse.ArgumentParser()
# Adding optional argument
parser.add_argument("-o", "--Output", help = "Show Output")
# Read arguments from command line
args = parser.parse_args()
if args.Output:
print("Displaying Output as: % s" % args.Output)

Output:
python-command-line-arguments
_____________________________________________________________________

5. Purpose of Importing Modules in Python.

Introduction :
In Python, modules are files containing Python code (functions, classes, variables,
etc.) that can be imported and reused across different programs or parts of a program.
Python has a rich standard library of built-in modules, and We can also create custom
modules. Importing modules is an essential feature that allows We to structure Wer code,
avoid duplication, and use a vast array of pre-written functionalities.

Modules enable modular programming, where the code is divided into separate,
manageable pieces. A module is simply a Python file (.py) that can contain functions,
variables, or classes.

Benefits of Importing Modules


❖ Code Reusability: By defining functions, classes, and variables in one module, We
can reuse them across different scripts and projects without rewriting the code.
❖ Organization and Modularity: It helps in keeping code organized by dividing it into
logical units. Large projects can be divided into smaller modules, making it easier to
manage, understand, and debug.
❖ Namespace Separation: Modules allow We to define variables and functions in
different namespaces, preventing naming conflicts between different parts of Wer
program.
❖ Maintainability: Since functionalities are segregated into different modules,
updating and maintaining the code becomes easier.
❖ Access to Built-In Libraries: Python's standard library provides a wide range of
modules like math, os, random, datetime, etc., which can be used directly in Wer
program without having to write the functionality Werself.
❖ Community Modules: Python has a rich ecosystem of third-party libraries (available
through Python Package Index (PyPI)) that We can use to perform complex tasks like
data analysis (Pandas, NumPy), web development (Flask, Django), and more.

Types of Imports in Python


Importing the whole module: This gives access to everything in the module but requires
prefixing with the module name.
import math
print(math.sqrt(16))
Importing specific functions or classes: This gives access to specific parts of the module.
from math import sqrt
print(sqrt(16))
Using an alias for a module: This makes it easier to use modules with long names.
import numpy as np
print(np.array([1, 2, 3]))
Importing all contents of a module: This is generally discouraged as it can lead to conflicts
with existing names in Wer program.
from math import *
print(sqrt(16))
Example:
Here’s an example that demonstrates the benefits of importing and using modules in
Python.
# custom_module.py
def greet(name):
return f"Hello, {name}!"
def add(a, b):
return a + b

Now, we can import this custom module into another Python script.
# main_program.py
import custom_module
# Using the functions from the custom module
print(custom_module.greet("Alice"))
print(f"2 + 3 = {custom_module.add(2, 3)}")
Output:
Hello, Alice!
2+3=5
In addition, We can use standard library modules. Here’s an example of using the math
and datetime modules:
import math
from datetime import datetime
# Using math module
number = 25
print(f"The square root of {number} is {math.sqrt(number)}")
# Using datetime module to get current time
current_time = datetime.now()
print(f"Current date and time is: {current_time}")
Output:
The square root of 25 is 5.0
Current date and time is: 2024-09-26 12:34:56.789123

_____________________________________________________________________
Unit 4
1. Steps Used in Reading Binary Files, Benefits of Using Binary Files in Python
Introduction
Binary files are files that contain data in a format that is not human-readable, as opposed to
text files which store data as readable characters. Binary files can include images, audio,
video, and various data formats used in applications. Reading binary files in Python allows
We to work with complex data structures efficiently and with more precision than text files.
Benefits of Using Binary Files
● Efficiency: Binary files consume less space compared to text files as they store data
in raw bytes without encoding or extra formatting.
● Faster Access: Binary data can be written and read faster since there is no need for
encoding or decoding between text and byte representations.
● Data Integrity: Binary files preserve the original data without losing any detail or
precision (especially useful for images, audio, videos, and floating-point numbers).
● Non-Text Data Support: Binary files allow storage of non-textual data such as
images, audio, videos, and serialised objects.
● Portability: Binary data is easier to transfer between systems without conversions,
avoiding issues such as encoding errors.

Steps:
Open the Binary File:
Use open() function with the 'rb' (read binary) mode to open a binary file.with
open('file.bin', 'rb') as file:
Read the Data:
Use methods like read(), readline(), or readlines() to read data from the binary file.
The data read will be in bytes.
binary_data = file.read()

Process the Data:


Depending on the content, We may need to convert or process the byte data into a
human-readable format or handle it appropriately (e.g., loading an image or audio).
Close the File:
After reading the file, ensure that We close it using close() or, better, use a with
statement for automatic closure.

Example Program

with open('example_image.jpg', 'rb') as binary_file:


binary_data = binary_file.read()
print("Binary Data (first 20 bytes):", binary_data[:20])
some_binary_data = b'\x00\x01\x02\x03\x04\x05'
with open('output.bin', 'wb') as binary_output:
binary_output.write(some_binary_data)
print("Binary data written to output.bin")

Explanation of Example:
The image file 'example_image.jpg' is opened in binary mode ('rb'). We read the
entire content of the file into the binary_data variable as raw bytes. We can manipulate or
process these bytes as needed, such as displaying them, decoding them, or writing them to
another file. In the example, we create a small chunk of binary data (some_binary_data) and
write it to 'output.bin'. The binary data is written to the file without any transformation,
ensuring efficiency and integrity.

2. Text Strings in Python


Introduction
In Python, a string is a sequence of characters used to represent text. Strings are an
essential data type and can contain letters, numbers, symbols, and even spaces. They are
enclosed in either single quotes ('), double quotes ("), or triple quotes (''' or """ for multi-line
strings). Strings in Python are immutable, meaning once a string is created, its content
cannot be changed. Any operation that modifies a string will result in a new string object
being created, leaving the original string unchanged. Python strings natively support
Unicode, allowing them to handle text from different languages and special symbols. Strings
are used to handle text-based information like file paths, messages, user input,
configuration data, and much more.
Benefits of Using Strings in Python:
1. Ease of Use:
○ Creating and working with strings in Python is simple. The language provides
a wide range of built-in methods to manipulate strings efficiently.
2. Immutability:
○ Immutability ensures data integrity, as it avoids accidental modification of
strings. This can reduce bugs and make the code more predictable.
3. Rich Functionality:
○ Python provides numerous methods and operators for strings, such as
concatenation (+), repetition (*), slicing, and various built-in functions (e.g.,
lower(), upper(), replace()).
4. Support for Unicode:
○ With native Unicode support, Python strings can represent characters from
virtually any language, including emojis and special characters.
5. String Formatting:
○ Python offers flexible and powerful string formatting techniques such as f-
strings and the format() method to make working with dynamic or complex
text easy.
6. Escape Sequences:
○ Strings in Python can handle special characters like newlines (\n), tabs (\t),
and quotes (\' or \") using escape sequences.
Steps to Work with Strings in Python
1.Creating Strings:
● A string is created by enclosing characters within single (') or double quotes ("), for
example:
greeting = 'Hello'
name = "Alice”
● Multi-line strings can be created using triple quotes (''' or """), for example:
multi_line_string = """This is a
multi-line string."""
2.Accessing Elements of a String:
Indexing: Each character in a string has an index, starting from 0 for the first
character. For example:
first_char = greeting[0] # 'H'
Slicing: A substring can be obtained using slicing. The syntax is string[start:end],
where start is inclusive, and end is exclusive:
substring = greeting[1:4] # 'ell'
3.Common String Methods: Python offers several built-in methods for string manipulation:
lower(): Converts all characters to lowercase.
upper(): Converts all characters to uppercase.
strip(): Removes leading and trailing whitespace.
replace(old, new): Replaces all occurrences of old with new.
find(sub): Returns the index of the first occurrence of sub in the string, or -1 if not
found.
split(separator): Splits the string into a list of substrings based on the separator.
4.String Concatenation:
Strings can be concatenated using the + operator or by using string formatting. For
example:
full_message = greeting + " " + name # 'Hello Alice'
5.String Formatting:
F-strings: Introduced in Python 3.6, f-strings are a concise and readable way to
format strings:
age = 30
message = f"{name} is {age} years old."
format() Method: A more traditional way to format strings:
message = "{} is {} years old.".format(name, age)
6.Escape Sequences:
Python uses backslashes (\) for escape sequences to represent special characters like
newlines (\n) or tabs (\t):
new_line_example = "Hello\nWorld" # 'Hello' and 'World' on separate lines
7.Concatenation and Repetition:
We can concatenate multiple strings using the + operator or repeat them using the *
operator:
repeated_string = "Hello " * 3 # 'Hello Hello Hello '
8.Checking String Properties:
We can check certain properties of strings using methods like startswith(),
endswith(), and isalpha():
greeting.startswith("He") # True
Example:
greeting = "Hello, World!"
name = "Alice"
multi_line_string = """This is a
multi-line string example."""
first_char = greeting[0] # Access the first character: 'H'
substring = greeting[7:12] # Slice a substring: 'World'
print(f"First character: {first_char}")
print(f"Substring: {substring}")
lowercase_greeting = greeting.lower() # Convert to lowercase
uppercase_greeting = greeting.upper() # Convert to uppercase
print(f"Lowercase: {lowercase_greeting}")
print(f"Uppercase: {uppercase_greeting}")
personalized_greeting = greeting + " " + name # Concatenate strings
print(f"Concatenated string: {personalized_greeting}")
age = 30
message = f"{name} is {age} years old."
print(f"Formatted message: {message}")
escaped_string = "This is a \"quote\" inside a string.\nAnd a new line."
print(escaped_string)
repeated_string = "Hello " * 3 # Repeat the string 3 times
print(f"Repeated string: {repeated_string}")
words = greeting.split() # Split into a list of words
print(f"Words: {words}")
___________________________________________________________________________
3. Python With Relational Databases.
Introduction
❖ Relational Databases: Relational databases store data in tables consisting of rows
and columns, allowing for relationships between different data sets. Common
relational database management systems (RDBMS) include MySQL, PostgreSQL,
SQLite, and Oracle.
❖ Python Database Connectivity: Python provides several libraries and modules for
connecting and interacting with relational databases. The most commonly used
libraries include:
❖ SQLite: Built-in support via the sqlite3 module.
❖ MySQL: Accessible using libraries like mysql-connector-python or
PyMySQL.
❖ PostgreSQL: Supported through libraries like psycopg2.
❖ CRUD Operations: The basic operations performed on databases are Create, Read,
Update, and Delete (CRUD). These operations can be performed using SQL
commands.
Benefits of Using Python with Relational Databases
● Data Integrity: Relational databases enforce data integrity through constraints,
ensuring accuracy and consistency of data.
● Structured Data Storage: Data is organized in a structured way, making it easier to
retrieve, manipulate, and analyze.
● Powerful Query Language: SQL (Structured Query Language) allows complex queries
and data manipulation with ease.
● Scalability: Relational databases can handle large volumes of data and support
complex transactions.
● Interoperability: Python's extensive libraries allow integration with various RDBMS,
making it flexible for different applications.
● Community and Support: Both Python and relational databases have strong
communities, providing support and extensive documentation.
Steps:
Install Required Libraries:
● Depending on the database, install the necessary library using pip. For example, for
SQLite:
pip install sqlite3 # No installation needed as it's built-in
● For MySQL:
pip install mysql-connector-python
Establish a Connection: Use the appropriate library to connect to the database.
import sqlite3 # For SQLite
conn = sqlite3.connect('example.db') # Create or connect to a database
Create a Cursor Object: The cursor object allows We to execute SQL commands.
cursor = conn.cursor()
Execute SQL Commands: Use the cursor to execute SQL commands for creating tables,
inserting data, querying, updating, or deleting records.
cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
Fetch Data: Use the cursor to fetch results from queries.
cursor.execute('SELECT * FROM users')
rows = cursor.fetchall() # Fetch all results
Commit Changes: If We perform any write operations (INSERT, UPDATE, DELETE),
remember to commit the changes to the database.
conn.commit()
Close the Connection:
Always close the cursor and connection when done to free up resources.
cursor.close()
conn.close()
Example:
import sqlite3
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER
)
''')
cursor.execute('INSERT INTO users (name, age) VALUES (?, ?)', ('Alice', 30))
cursor.execute('INSERT INTO users (name, age) VALUES (?, ?)', ('Bob', 25))
conn.commit()
cursor.execute('SELECT * FROM users')
rows = cursor.fetchall()
for row in rows:
print(row) # Output: (1, 'Alice', 30), (2, 'Bob', 25)
cursor.close()
conn.close()
___________________________________________________________________________
4. Web Services Used For Automation - SOAP Web Services and Restful Web Services
What are Web Services?
Web services enable communication between applications over the internet. They provide a
standardised way of interacting with systems, allowing them to exchange data and invoke
operations, irrespective of the programming language or platform used.
Types of Web Services:
● SOAP (Simple Object Access Protocol): A protocol that uses XML for message format
and typically operates over HTTP or SMTP.
● REST (Representational State Transfer): An architectural style that uses HTTP
requests to perform CRUD (Create, Read, Update, Delete) operations on resources.
Benefits of Using Web Services for Automation
● Interoperability: Enable communication between systems written in different
programming languages.
● Efficiency: Automate repetitive tasks and workflows, reduce manual intervention,
and improve productivity.
● Scalability: Web services can scale to handle large, distributed systems.
● Ease of Integration: Connect different software systems, devices, and databases
seamlessly.
SOAP Web Services
What is SOAP?
SOAP is a protocol that defines a strict standard for communication over the web
using XML for message formatting.
Features:
○ Protocol-based: SOAP operates over various protocols such as HTTP, SMTP, and
FTP.
○ WSDL (Web Services Description Language): Defines the structure of a SOAP
service, detailing the operations and data types.
○ Heavyweight: It involves more overhead due to strict formatting and message size
(since XML is verbose).
○ Built-in Security: SOAP has better security features (e.g., WS-Security) to handle
authentication, encryption, and transactions.
SOAP in Python
i) Connecting to a SOAP Service
Python's zeep library is widely used for interacting with SOAP web services.
SOAP services are accessed using their WSDL URL. The WSDL defines the service operations
and types.
from zeep import Client
# Load the WSDL
client = Client('http://www.example.com/service?wsdl')
response = client.service.SomeMethod(param1='value1', param2='value2')
print(response)
ii) Handling SOAP Responses
The response from the SOAP service is typically an XML structure, which zeep converts into
a Python object.
response = client.service.SomeMethod(param1='value1')
print(response) # Automatically converts the XML response to Python data types
iii) Error Handling
SOAP services may return fault messages (SOAP Faults) if the request fails.
try:
response = client.service.SomeMethod(param1='value1')
except Exception as e:
print(f"Error: {e}")
iv) Authentication in SOAP Services
SOAP services often require authentication, such as Basic Authentication or WS-Security.
from requests.auth import HTTPBasicAuth
from zeep import Client, Transport
import requests
session = requests.Session()
session.auth = HTTPBasicAuth('username', 'password')
transport = Transport(session=session)
client = Client('http://www.example.com/service?wsdl', transport=transport)
v) Automating Tasks with SOAP
We can use a task scheduler or a loop to make periodic SOAP requests to automate
workflows.
Example: Checking Stock Prices via SOAP:

def get_stock_price(stock_symbol):
response = client.service.GetStockPrice(symbol=stock_symbol)
return response
# Periodic checks (using schedule library)
import schedule
import time
def job():
price = get_stock_price('AAPL')
print(f"Apple Stock Price: {price}")
schedule.every(10).minutes.do(job)
while True:
schedule.run_pending()
time.sleep(1)
RESTful Web Services
What is REST?
REST (Representational State Transfer) is an architectural style where web services
are designed around resources, represented as URLs (Uniform Resource Identifiers).
It is a stateless protocol that uses standard HTTP methods (GET, POST, PUT, DELETE).
Features:
○ Lightweight: REST uses lightweight JSON or XML for data interchange,
reducing overhead compared to SOAP.
○ Stateless: Each request from the client to the server must contain all
information needed to understand and process the request.
○ Scalable and Cacheable: REST services support scalability and caching,
enhancing performance.
○ Widely Used: Most modern APIs, such as those from Google, Twitter, or
GitHub, use REST.
REST in Python
Python’s requests library is widely used for interacting with RESTful services.
Performing HTTP Methods
RESTful APIs use HTTP methods to interact with resources:
● GET: Retrieve information from the server.
● POST: Create a new resource.
● PUT: Update an existing resource.
● DELETE: Remove a resource.
Example: Performing REST API Requests
GET Request (Retrieve Data):
import requests
response = requests.get('https://api.example.com/data')
POST Request (Submit Data):
data = {'key': 'value'}
response = requests.post('https://api.example.com/data', json=data)
PUT Request (Update Resource):
update_data = {'key': 'new_value'}
response = requests.put('https://api.example.com/data/1', json=update_data)
DELETE Request (Delete Resource):
response = requests.delete('https://api.example.com/data/1')
Handling JSON Responses
Most REST APIs return JSON-formatted data. The requests library makes it easy to parse
JSON responses:
Example:
response = requests.get('https://api.example.com/data')
json_data = response.json() # Converts JSON to Python dictionary
print(json_data['key'])
Authentication in REST APIs
Many REST APIs require authentication:
from requests.auth import HTTPBasicAuth
Response = requests.get('https://api.example.com/data',
auth=HTTPBasicAuth('username', 'password'))
Error Handling in REST APIs
Check HTTP status codes and handle errors:
response = requests.get('https://api.example.com/data')
if response.status_code == 200:
print("Success")
elif response.status_code == 404:
print("Resource not found")
else:
print(f"Failed with status code: {response.status_code}")
Automating REST API Requests
We can automate periodic REST API requests using the schedule library:
Example: Automating Weather Data Retrieval:
import requests
import schedule
import time
def get_weather():
Response = requests.get('https://api.weatherapi.com/v1/current.json?
key=WeR_API_KEY&q=London')
weather_data = response.json()
print(f"Temperature: {weather_data['current']['temp_c']}°C")
schedule.every(10).minutes.do(get_weather)
while True:
schedule.run_pending()
time.sleep(1)
SOAP vs. REST for Automation
SOAP Advantages:
● Built-in Security: SOAP provides more robust security features like WS-Security.
● Standardized Protocol: It is highly standardized, which makes it suitable for
enterprise-level applications.
● Reliability: SOAP has better built-in error handling and is preferred in environments
where reliability is critical.
REST Advantages:
● Lightweight: REST uses less bandwidth and resources as it supports JSON, making it
faster for web services with mobile applications.
● Scalability: REST is stateless, which makes it more scalable for cloud-based
applications.
● Ease of Use: REST APIs
___________________________________________________________________________
5. NoSQL Databases: Introduction & Characteristics
Introduction to NoSQL Databases:
● NoSQL (Not Only SQL) databases are designed to handle large volumes of
unstructured or semi-structured data that traditional relational databases struggle to
manage.
● Unlike traditional SQL databases, NoSQL databases don't rely on fixed schemas or
complex join operations.
● Common use cases include big data applications, real-time web apps, IoT systems,
and content management systems.
● Examples of NoSQL databases include MongoDB, Cassandra, Redis, CouchDB, and
Amazon DynamoDB.
Characteristics of NoSQL Databases:
1. Scalability:
○ Horizontally scalable (scale-out by adding more servers) unlike relational
databases that scale vertically (scale-up by adding more resources to a single
server).
2. Schema Flexibility:
○ No predefined schema, allowing storage of different kinds of data in the same
database.
○ Ideal for unstructured or semi-structured data (e.g., JSON, XML, key-value
pairs).
3. Distributed Architecture:
○ NoSQL databases are often designed to work across distributed systems,
ensuring data is replicated and available even in the event of server failures.
4. High Availability:
○ Fault-tolerant with automatic recovery mechanisms. Data is often replicated
across multiple nodes to ensure availability.
5. Efficient Handling of Large Datasets:
○ Optimized for handling high volumes of data and transactions, making them
suitable for Big Data applications.
6. Eventual Consistency:
○ Many NoSQL systems prefer eventual consistency over strong consistency,
which means that updates to the database will eventually propagate to all
nodes.
7. Data Models:
○ NoSQL databases can have various data models, including:
■ Document-based (e.g., MongoDB): Data stored in JSON-like
documents.
■ Key-value (e.g., Redis): Data stored as key-value pairs.
■ Column-family (e.g., Cassandra): Data stored in column families
rather than rows.
■ Graph-based (e.g., Neo4j): Designed for handling data that can be
represented as graphs.
Steps Used in Managing Unstructured Data Within NoSQL Databases
1. Data Modeling:
○ Identify the structure: NoSQL allows for dynamic schema, so understanding
the data model (documents, key-value, graph, etc.) is key.
○ Organize unstructured data: Unstructured data such as text, images, and
videos are usually stored in a flexible format (e.g., JSON or BSON).
2. Data Ingestion:
○ Extract and load data: Use APIs or connectors to ingest data from various
sources like social media, IoT devices, or log files.
○ Handling large data: Utilize NoSQL features like sharding (partitioning large
datasets) for efficient ingestion and processing.
3. Data Storage:
○ Choose the appropriate database type: Depending on the nature of the
unstructured data, select a NoSQL model:
■ Document store (e.g., MongoDB) for JSON-like documents.
■ Key-value store (e.g., Redis) for simple, fast access.
■ Column-family store (e.g., Cassandra) for large datasets with dynamic
attributes.
■ Graph store (e.g., Neo4j) for relationship-driven data.
○ Data replication: Configure replication to ensure data availability and fault
tolerance.
4. Data Indexing:
○ Create efficient indexes: Create appropriate indexes on frequently queried
fields to improve performance.
○ Custom indexing strategies: Use indexing strategies suitable for unstructured
data (like text indexing for search engines).
5. Data Querying:
○ Use flexible query languages: NoSQL databases provide query languages
suited to their data models. For example, MongoDB uses queries resembling
JSON syntax.
○ Aggregation and filtering: Use built-in capabilities to filter and aggregate
unstructured data efficiently (e.g., MongoDB's aggregation pipeline).
6. Data Replication & Sharding:
○ Sharding: NoSQL databases support horizontal scaling via sharding,
distributing large datasets across multiple nodes.
○ Replication: Ensure data redundancy and fault tolerance by replicating data
across multiple nodes.
7. Data Backup & Recovery:
○ Regular backups: Since NoSQL databases handle large datasets, backups are
critical to avoid data loss.
○ Automated recovery: Configure automatic recovery systems to handle server
or node failures.
8. Data Security:
○ Implement access control: Define role-based access and permissions for
users.
○ Data encryption: Encrypt sensitive data, especially when dealing with
unstructured data like personal information.
Example: Managing Unstructured Data with MongoDB
1. Ingesting Data:
○ Unstructured data like log files or social media posts are loaded into
MongoDB using a client library.
2. Data Storage:
○ The data is stored in BSON (binary JSON) format, allowing flexible fields and
structure. Different documents in the same collection can have varied fields.
3. Data Indexing:
○ Indexes are created on frequently queried fields like timestamps or user IDs
to speed up retrieval.
4. Data Querying:
○ MongoDB’s query language is used to find, filter, or aggregate data. Queries
are structured similarly to JSON objects.
5. Replication & Sharding:
○ The dataset is split across multiple nodes (sharding), and MongoDB ensures
that copies (replicas) of the data exist to ensure high availability.
This method ensures efficient storage, retrieval, and management of unstructured data at a
large scale using NoSQL databases.
__________________________________________________________________________
Unit 5
1. Handling Files, Read and Write Files On Disk in Python
Python provides a simple and efficient way to handle files on disk. This is done using built-in
functions to read from and write to files. These operations are typically performed with the
open() function, which returns a file object that allows various file-handling methods.
i) Opening a File:
● To work with files, We first need to open them. Python’s open() function is used for
this.
Syntax:
file_object = open("file_name", "mode")
○ file_name: Name of the file We want to open.
○ mode: Specifies what We want to do with the file. The common modes are:
■ 'r': Open for reading (default mode).
■ 'w': Open for writing (will overwrite the file if it exists, or create a new
file).
■ 'a': Open for appending (data will be added to the end of the file).
■ 'b': Binary mode (used with 'rb', 'wb' for reading and writing binary
data).
■ 'x': Create a new file and open it for writing (fails if the file already
exists).
ii) Reading from a File
After opening a file, We can use various methods to read data from the file.
Example:
file = open("example.txt", "r")
content = file.read()
print(content)
file.close()
Reading Line by Line:
with open("example.txt", "r") as file:
for line in file:
print(line.strip())
# strip() removes the extra newline characters
Methods for Reading:
● read(size): Reads the entire file or the specified number of characters.
● readline(): Reads one line at a time.
● readlines(): Reads all lines in the file and returns them as a list.
iii) Writing to a File
To write data to a file, We can open it in 'w', 'a', or 'x' mode. If the file does not exist, it will
be created.
Example:
# Writing to a file
with open("example.txt", "w") as file:
file.write("This is a new line.\n")
file.write("Another line of text.\n")
Appending to a File:
# Appending to a file
with open("example.txt", "a") as file:
file.write("Appending a new line.\n")
Methods for Writing:
● write(string): Writes a single string to the file.
● writelines(list): Writes a list of strings to the file (We need to include the newline
characters \n explicitly).
iv) Closing a File
It's important to close a file after We are done with it to free up system resources and
ensure the file is saved correctly.
Closing a file manually:

file = open("example.txt", "r")


# Perform file operations
file.close()
Using the with statement: It is a good practice to use the with statement, as it
automatically closes the file when We are done.
with open("example.txt", "r") as file:
content = file.read()
# No need to call file.close() explicitly.
v) Working with Binary Files
In Python, binary files such as images, audio, or any non-text files can be handled using the
'b' mode.
Example: Reading and Writing Binary Files
# Reading a binary file
with open("image.jpg", "rb") as binary_file:
data = binary_file.read()
# Writing to a binary file
with open("new_image.jpg", "wb") as binary_file:
binary_file.write(data)
vi) Reading from One File and Writing to Another
This is a common use case where We need to copy the contents of one file to another.
# Reading from 'input.txt' and writing to 'output.txt'
with open("input.txt", "r") as infile:
content = infile.read()
with open("output.txt", "w") as outfile:
outfile.write(content)
Key Points to Remember:
● Always close files after reading or writing to free up system resources.
● Use the with statement to handle files efficiently without manually closing them.
● Be cautious with the 'w' mode, as it will overwrite any existing data in the file.
● Use the 'a' mode to append data to an existing file without overwriting the original
content.

2. Programs and Processes in Python


In computing, a program is a set of instructions that tells the computer what to do, while a
process is a program in execution. In Python, understanding how to manage programs and
processes is crucial for tasks like multitasking, executing external programs, and parallel
processing.
i) Programs in Python:
● A program in Python is simply a script (a .py file) that contains instructions written in
Python.
● Programs can be executed from a command line or via an integrated development
environment (IDE).
Example of a Python Program:
# hello_world.py
print("Hello, World!")
When this script is run, Python interprets the code and produces the output:
$ python hello_world.py
Hello, World!
ii) Processes in Python:
A process is an instance of a program that is being executed. Each process has its own
memory space and system resources (CPU time, memory, etc.). Python provides several
ways to manage processes and execute system commands using modules like os,
subprocess, and multiprocessing.
Key Concepts:
● Single Process: The default state of any Python program running as a single process.
● Multithreading: Involves multiple threads (smaller units of a process) running within
a single process to perform tasks concurrently.
● Multiprocessing: Running multiple processes simultaneously, often used to utilize
multiple CPU cores.
iii) Running External Programs Using Python
Python's subprocess module is used to execute external programs or system commands. It
allows We to spawn new processes, connect to their input/output/error pipes, and obtain
their return codes.
Example: Running a Command with subprocess.run()
import subprocess
# Running an external command
subprocess.run(["ls", "-l"]) # For Unix-based systems
# subprocess.run(["dir"], shell=True) # For Windows systems
Capturing the Output of a Command:
result = subprocess.run(["echo", "Hello, Python!"], capture_output=True, text=True)
print(result.stdout) # Output: Hello, Python!
iv) Creating and Managing Processes
Using the multiprocessing Module:
The multiprocessing module in Python allows We to create and manage multiple processes,
making it easier to parallelize tasks that benefit from running concurrently.
Example: Creating Multiple Processes
import multiprocessing
# Function to be executed by the process
def worker():
print("Worker process is running...")
# Creating a process
process = multiprocessing.Process(target=worker)
# Starting the process
process.start()
# Waiting for the process to finish
process.join()
Explanation:
● multiprocessing.Process() creates a new process.
● start() begins the execution of the process.
● join() ensures the main program waits for the process to complete before
proceeding.
v) Sharing Data Between Processes
When working with multiple processes, it's important to share data between them,
especially when processes need to communicate.
Using Queues:
A queue can be used to share data between processes.
import multiprocessing
def worker(queue):
queue.put("Data from worker")
if __name__ == "__main__":
queue = multiprocessing.Queue()
process = multiprocessing.Process(target=worker, args=(queue,))
process.start()
process.join()
# Retrieve data from the queue
print(queue.get()) # Output: Data from worker
vi) Process Pooling: Executing Tasks in Parallel
The Pool class in multiprocessing allows We to manage a pool of worker processes and
distribute tasks across them.
Example: Using a Pool to Execute Tasks in Parallel

import multiprocessing
def square(x):
return x * x
if __name__ == "__main__":
# Create a pool of worker processes
with multiprocessing.Pool(4) as pool:
# Map a function to a list of inputs
results = pool.map(square, [1, 2, 3, 4, 5])
print(results) # Output: [1, 4, 9, 16, 25]
In this example:
● The Pool(4) creates a pool of 4 processes.
● pool.map() distributes the task of squaring each number across the available
processes.
vii) Differences Between Threads and Processes
● Threads share the same memory space and are lightweight, but Python's Global
Interpreter Lock (GIL) prevents true parallel execution of threads.
● Processes have separate memory spaces and can run in parallel, making them
suitable for CPU-bound tasks.
For CPU-bound tasks, multiprocessing is preferable because it avoids the limitations of
Python’s GIL by using separate processes.
viii) CPU-Bound vs. I/O-Bound Tasks
CPU-Bound Task (using multiprocessing):
For tasks that require heavy computation, multiprocessing can distribute the workload
across multiple CPU cores.
import multiprocessing
def calculate_square(x):
return x * x
if __name__ == "__main__":
with multiprocessing.Pool() as pool:
result = pool.map(calculate_square, range(10**6))
print(result[:5]) # First five results
I/O-Bound Task (using threading):
For tasks like reading or writing to files or making network requests, threading can be more
efficient.
import threading
def download_data(url):
print(f"Downloading data from {url}")
# Create threads for downloading from multiple URLs
threads = [threading.Thread(target=download_data, args=(url,)) for url in ["url1",
"url2", "url3"]]
# Start the threads
for thread in threads:
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
___________________________________________________________________________
3. Handling Directories in Python
Python provides several built-in modules to work with directories (folders) and file paths,
allowing We to create, delete, list, or change directories programmatically. The most
commonly used modules for handling directories are:
● os module: Contains basic functions for interacting with the operating system,
including directory manipulation.
● os.path module: Provides functions for manipulating file paths in a platform-
independent way.
● pathlib module: Introduced in Python 3.4, this module provides an object-oriented
approach to working with files and directories.
i). Creating and Deleting Directories
We can create a directory using the mkdir() function from the os or pathlib module.
Example Using os:
import os
os.mkdir("my_directory")
To delete an empty directory, use rmdir().
Example Using os:
import os
os.rmdir("my_directory")
Note: Both os.rmdir() will throw an error if the directory is not empty. We need to delete
the contents first or use a more robust method like shutil.rmtree().
ii) Listing Files and Directories
Using os.listdir()
To get a list of all files and directories in a directory, use os.listdir().
Example:
import os
items = os.listdir(".")
print(items)
Using os.scandir() (More Efficient)
os.scandir() returns an iterator that yields DirEntry objects with more detailed information
about each file or directory.
Example:
import os
with os.scandir(".") as entries:
for entry in entries:
print(entry.name, entry.is_file(), entry.is_dir())
iii) Checking If a Directory Exists
Using os.path.exists()
To check whether a directory exists, use os.path.exists().
Example:
import os
if os.path.exists("my_directory"):
print("Directory exists")
else:
print("Directory does not exist")
iv) Changing the Current Working Directory
The working directory is the directory in which Wer Python script is currently operating. To
change the working directory, use os.chdir().
Example:
import os
current_directory = os.getcwd()
print("Current Directory:", current_directory)
os.chdir("my_directory")
print("New Current Directory:", os.getcwd())
v) Renaming Directories
To rename a directory, use the os.rename() or pathlib.Path.rename() method.
Example: import os
os.rename("old_directory", "new_directory")
vi) Deleting Directories with Contents
For directories that are not empty, We can delete them using shutil.rmtree(). This will
remove the directory and all its contents recursively.
Example: import shutil
shutil.rmtree("parent_dir")
vii) Getting the File/Directory Name and Parent Directory
Using os.path.basename() and os.path.dirname()
● os.path.basename(): Returns the name of the file or directory.
● os.path.dirname(): Returns the directory name of the path.
Example:
import os
path = "/home/user/documents/file.txt"
print("File Name:", os.path.basename(path)) print("Directory:",
os.path.dirname(path))
___________________________________________________________________________
4. Thread Synchronization in Concurrent Programming

Introduction to Threads in Python


Threads are lightweight units of a process that can be run concurrently to perform tasks
simultaneously. They are particularly useful for I/O-bound operations (such as reading from
or writing to files, network requests, etc.) since they allow the program to continue running
other tasks while waiting for the I/O operation to complete.
Python provides the threading module to create and manage threads. Even though Python
has a Global Interpreter Lock (GIL) that prevents multiple native threads from executing
Python bytecode in parallel for CPU-bound tasks, threads can still be used efficiently for
tasks that involve waiting for external resources.
Benefits of Using Threads in Programming
1. Concurrency: Threads allow multiple parts of a program to run concurrently. This
means one task doesn't have to wait for another to complete, improving the overall
performance of the program, especially for I/O-bound operations.
2. Efficient Resource Usage: Threads share the same memory space and resources
within a process, making them less resource-intensive than creating multiple
processes (which have separate memory spaces).
3. Responsiveness: In GUI applications or web servers, threading can keep the
interface responsive by running tasks in the background, such as handling user input
or serving multiple clients simultaneously.
4. Simplified Code for Multitasking: Instead of writing complex code to handle multiple
operations manually, We can use threads to simplify multitasking and asynchronous
operations.
5. Improved Program Performance for I/O-bound Tasks: For tasks that involve waiting
(e.g., reading files, fetching data from a network), threads allow other parts of the
program to continue executing, thus making better use of time and resources.
Thread Synchronization in Concurrent Programming
When multiple threads share resources (e.g., variables, data structures, files),
synchronization becomes crucial to avoid conflicts and ensure consistency. Thread
synchronization refers to techniques used to ensure that multiple threads can cooperate
and execute concurrently without causing data corruption or race conditions.
Common Thread Synchronization Techniques:
Locks (Mutexes): A lock (or mutex) allows only one thread to access a shared
resource at a time. When a thread acquires a lock, other threads must wait until the
lock is released. This ensures that the shared resource is not modified by multiple
threads at the same time.
Example Using threading.Lock:
import threading
# Create a lock
lock = threading.Lock()
shared_resource = 0
def increment():
global shared_resource
with lock: # Acquire the lock
local_copy = shared_resource
local_copy += 1
shared_resource = local_copy
threads = [threading.Thread(target=increment) for _ in range(5)]
# Start all threads
for thread in threads:
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
print("Final value of shared_resource:", shared_resource)
RLocks (Reentrant Locks): A reentrant lock (RLock) allows a thread to acquire the same lock
multiple times without causing a deadlock. It is useful when a thread needs to re-enter the
same lock-protected code block multiple times.
Example:

import threading
lock = threading.RLock()
def task():
with lock:
print("Acquired lock")
with lock: # Acquiring the same lock again
print("Acquired lock again")
thread = threading.Thread(target=task)
thread.start()
thread.join()
Semaphores: A semaphore is a counter that limits the number of threads that can access a
resource simultaneously. Unlike a lock, which allows only one thread to access a resource, a
semaphore can allow a fixed number of threads to access a resource concurrently.
Example Using threading.Semaphore:
import threading
semaphore = threading.Semaphore(2) # Allow up to 2 threads to access
def task(thread_id):
with semaphore:
print(f"Thread {thread_id} is working")
# Simulate a task by sleeping
import time
time.sleep(2)
print(f"Thread {thread_id} has finished")
threads = [threading.Thread(target=task, args=(i,)) for i in range(5)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
___________________________________________________________________________
5. Publish-Subscribe Model With Examples
Introduction:
The Publish-Subscribe (Pub-Sub) model is a messaging pattern used in distributed
systems where the sender (publisher) of a message is decoupled from the receiver
(subscriber). In this model, publishers and subscribers do not communicate directly with
each other. Instead, they communicate via an intermediary, often referred to as a message
broker or event bus.
This model is widely used in systems where multiple parties need to consume the
same data, or where data producers and consumers should remain independent of each
other. It is commonly employed in event-driven architectures, real-time applications, and
cloud-based solutions like messaging queues.

Key Concepts of Publish-Subscribe Model


Publisher: The component responsible for producing or publishing messages. The publisher
is unaware of who the subscribers are or how many there are.
Subscriber: The component that listens for or subscribes to specific messages or topics.
Subscribers only receive messages that they are interested in (i.e., those they subscribe to).
Message Broker: A system responsible for routing messages from publishers to subscribers.
It manages the delivery of messages and ensures that subscribers receive relevant
messages.
Topic: Messages are classified based on topics or channels. A topic is like a "category" under
which publishers publish messages, and subscribers can choose to subscribe to topics they
are interested in.
Decoupling: One of the core principles of the Pub-Sub model is that publishers and
subscribers are decoupled. They do not know about each other and don’t interact directly.
This ensures scalability and flexibility in distributed systems.

Benefits:
Scalability: The model allows for easy scaling because publishers and subscribers can be
added independently. A publisher can have multiple subscribers without any change in its
code.
Decoupling of Components: Publishers and subscribers don’t need to be aware of each
other. This allows components to evolve independently, fostering modularity and reducing
dependencies.
Efficiency: Multiple subscribers can receive messages from the same publisher, making it an
efficient way to deliver messages to different consumers. The Pub-Sub model is also ideal
for real-time data delivery.
Flexibility: New subscribers can be added dynamically without modifying the existing
system. Similarly, publishers can produce messages without knowing how many subscribers
there are, providing flexibility in system design.
Load Distribution: The message broker can manage how messages are delivered, distribute
load, and handle issues like message queuing or buffering, making the system more robust.
Real-Time Updates: This model is particularly well-suited for systems where real-time
updates are critical, such as stock market applications, chat systems, IoT applications, and
more.

How the Publish-Subscribe Model Works


The workflow of a basic Publish-Subscribe model involves the following steps:
Publishers Publish Messages to a Topic: Publishers send messages to a specific "topic" or
"channel" without any knowledge of the subscribers.

Subscribers Subscribe to Topics: Subscribers express interest in one or more topics by


subscribing to them. They receive updates whenever a new message is published to those
topics.
Message Broker Routes Messages: The message broker (or event bus) takes care of
delivering the messages published to a topic to all subscribers who have subscribed to that
topic.
Subscribers Process Messages: Once subscribers receive the messages from the broker,
they can process them independently.

Publish-Subscribe Model Example


Let’s take a real-world example of a news publishing system:
● The news agency acts as the publisher, publishing news stories categorized into
different topics like "Sports," "Politics," and "Technology."
● Users (subscribers) can subscribe to topics they are interested in. For instance, a user
may subscribe to the "Sports" topic.
● When the news agency publishes a new sports article, it is only delivered to users
who subscribed to the "Sports" topic.

Technical Example of Publish-Subscribe in Python


Here’s an example of implementing the Publish-Subscribe pattern using Python,
leveraging the paho-mqtt library, which is commonly used for MQTT (Message Queuing
Telemetry Transport) in IoT and real-time messaging.

Publisher (Using MQTT Broker)


import paho.mqtt.client as mqtt
# Set up the publisher client
broker = "test.mosquitto.org"
port = 1883
topic = "news/sports"
def publish_message():
client = mqtt.Client()
client.connect(broker, port)
# Publish a message to the "news/sports" topic
message = "Breaking News: Team A wins the championship!"
client.publish(topic, message)
print(f"Published message: '{message}' to topic: '{topic}'")
client.disconnect()
if __name__ == "__main__":
publish_message()
Subscriber
import paho.mqtt.client as mqtt
# Set up the subscriber client
broker = "test.mosquitto.org"
port = 1883
topic = "news/sports"
def on_message(client, userdata, message):
print(f"Received message: '{message.payload.decode()}' on topic:
'{message.topic}'")
def subscribe_to_topic():
client = mqtt.Client()
client.connect(broker, port)
# Set the on_message callback function
client.on_message = on_message
# Subscribe to the "news/sports" topic
client.subscribe(topic)
print(f"Subscribed to topic: '{topic}'")
# Start the MQTT client loop
client.loop_forever()
if __name__ == "__main__":
subscribe_to_topic()
Explanation of the Example:
Publisher:
The publish_message() function connects to an MQTT broker (test.mosquitto.org),
then publishes a message to the "news/sports" topic.
The message published is "Breaking News: Team A wins the championship!".
After publishing, the client disconnects from the broker.
Subscriber:
The subscribe_to_topic() function connects to the same MQTT broker and subscribes
to the "news/sports" topic.
When a message is received on that topic, the on_message() function prints the received
message.
The MQTT client keeps listening for messages indefinitely using client.loop_forever().
In this setup:
● The publisher sends messages related to a specific topic without knowing who or
how many subscribers exist.
● The subscriber receives all messages related to the topic it is interested in (i.e.,
sports news), independent of when it subscribes.

Advanced Example: Pub-Sub with Multiple Subscribers


import paho.mqtt.client as mqtt
broker = "test.mosquitto.org"
port = 1883
topic_sports = "news/sports"
topic_technology = "news/technology"
# Define the on_message callback function for each subscriber
def on_sports_message(client, userdata, message):
print(f"Sports Subscriber: '{message.payload.decode()}' on topic '{message.topic}'")
def on_tech_message(client, userdata, message):
print(f"Tech Subscriber: '{message.payload.decode()}' on topic '{message.topic}'")
# Subscribe to the sports topic
def subscribe_to_sports():
client = mqtt.Client()
client.connect(broker, port)
client.on_message = on_sports_message
client.subscribe(topic_sports)
client.loop_forever()
# Subscribe to the technology topic
def subscribe_to_tech():
client = mqtt.Client()
client.connect(broker, port)
client.on_message = on_tech_message
client.subscribe(topic_technology)
client.loop_forever()
# Publisher
def publish_message(topic, message):
client = mqtt.Client()
client.connect(broker, port)
client.publish(topic, message)
print(f"Published: '{message}' to {topic}")
client.disconnect()
if __name__ == "__main__":
p1 = Process(target=subscribe_to_sports)
p2 = Process(target=subscribe_to_tech)
p1.start()
p2.start()
publish_message(topic_sports, "Big Game Tonight!")
publish_message(topic_technology, "New AI Tech Released!")
___________________________________________________________________________
6. Mapreduce Implementation in Python
Introduction:
MapReduce is a programming model used for processing and generating large
datasets in a distributed environment. The model divides the task into two distinct phases:
Map and Reduce. This framework was introduced by Google for efficiently processing
massive data sets in parallel across a distributed cluster. The central idea is to enable the
processing of data on multiple nodes, distributing the workload, and scaling easily as data
grows.
● Map Phase: In the Map phase, the input data is divided into smaller chunks,
which are processed in parallel by the Mapper. Each Mapper function takes
an input key-value pair and produces a set of intermediate key-value pairs.
● Reduce Phase: In the Reduce phase, the output from the Mappers is
aggregated. The Reducer takes these intermediate key-value pairs, groups
them by key, and applies a function to produce a final result.
This approach is ideal for large-scale data processing tasks like counting word occurrences in
large datasets, sorting, indexing, and data transformations.

Benefits of Using MapReduce


Scalability: The MapReduce model can easily handle very large datasets by distributing the
work across many nodes. It scales horizontally by adding more nodes as data grows.
Fault Tolerance: If a node fails, MapReduce can rerun the task on a different node, ensuring
that the system is robust and fault-tolerant.
Parallelism: The model splits tasks into smaller sub-tasks (Map phase) that can be processed
in parallel, making it faster and more efficient, especially for large datasets.
Simplified Data Processing: MapReduce abstracts the complexity of parallel processing,
allowing developers to focus on writing map and reduce functions without worrying about
underlying parallelization, load balancing, or data distribution.
Support for Distributed Data: It’s designed to work efficiently with distributed storage
systems like HDFS (Hadoop Distributed File System).

Components of MapReduce
Mapper: Takes the input data and transforms it into intermediate key-value pairs.
Combiner (Optional): Acts as a mini-reducer that performs a local reduce operation on the
output of the mapper before sending the data to the reducer, reducing data transfer across
the network.
Reducer: Processes intermediate key-value pairs, groups by key, and produces the final
output.
Shuffling and Sorting: Intermediate key-value pairs are shuffled and sorted to ensure that all
values for the same key are sent to the same reducer.

How MapReduce Works


The MapReduce model works in the following way:

Input Data: The input data is split into smaller chunks and fed to the mappers.
Map Phase: Mappers process these chunks independently, producing intermediate key-
value pairs.
Shuffle and Sort: The intermediate data is shuffled and sorted by key so that all the values
associated with a specific key are grouped together.
Reduce Phase: Reducers aggregate the values for each key to generate the final output.
Output: The final output is written to a distributed storage system or returned to the user.

Example: Word Count Using MapReduce


The classic example to demonstrate MapReduce is counting the occurrences of words in a
dataset. Here’s a detailed Python implementation of MapReduce for word count using
native Python.

Step-by-Step MapReduce in Python


Step 1: Map Function
The Map function reads the input data (a chunk of text) and splits it into words. It
then outputs key-value pairs where the key is the word, and the value is 1, indicating one
occurrence of the word.
def map_function(text):
# Split the text into words
words = text.split()
# Emit key-value pairs: (word, 1)
result = []
for word in words:
result.append((word, 1))
return result
Step 2: Shuffle and Sort
In the shuffle and sort phase, key-value pairs with the same key (word) are grouped
together. This ensures that all occurrences of a word are sent to the same reducer.
from collections import defaultdict
def shuffle_sort(mapped_data):
grouped_data = defaultdict(list)
# Group key-value pairs by key (word)
for key, value in mapped_data:
grouped_data[key].append(value)
return grouped_data
Step 3: Reduce Function
The Reduce function takes the grouped key-value pairs and aggregates them. For the
word count example, it sums the values (occurrences) for each word.
def reduce_function(grouped_data):
reduced_data = {}
# Sum the counts for each word
for key, values in grouped_data.items():
reduced_data[key] = sum(values)
return reduced_data
Step 4: MapReduce Pipeline
The entire process — mapping, shuffling, and reducing — is combined into a single
function to perform the MapReduce operation. In this case, it processes a list of text chunks
(simulating distributed input).
def mapreduce(input_data):
# Step 1: Apply the Map function
mapped_data = []
for chunk in input_data:
mapped_data.extend(map_function(chunk))
# Step 2: Apply the Shuffle and Sort phase
grouped_data = shuffle_sort(mapped_data)
# Step 3: Apply the Reduce function
reduced_data = reduce_function(grouped_data)
return reduced_data
Full Example: Word Count
Let’s implement a word count using the MapReduce pipeline. Suppose we have a
large dataset split into smaller chunks. We want to count how often each word appears.
if __name__ == "__main__":
# Simulated input data, split into chunks
input_data = [
"cat dog dog",
"cat bird bird bird",
"dog cat"
]
# Perform MapReduce
result = mapreduce(input_data)
# Output the result
for word, count in result.items():
print(f"{word}: {count}")
Output:
cat: 3
dog: 3
bird: 3
Explanation:
Input Data: The input data is split into chunks, each containing some text. In a real-world
scenario, this could be split across multiple machines or storage nodes.
Map Phase: The map_function splits the text into words and generates key-value pairs,
where each word is paired with the value 1.
Shuffle and Sort Phase: The shuffle_sort function groups key-value pairs by key (word) so
that all occurrences of each word are together.
Reduce Phase: The reduce_function aggregates the values for each key by summing the
occurrences of each word.

Handling Larger Datasets: Parallel MapReduce


For larger datasets, the MapReduce process can be scaled up by distributing the map
and reduce functions across multiple machines. In Python, We can use parallel processing
tools like multiprocessing or dask to simulate distributed MapReduce on a single machine.
Here’s an example of how to parallelize the map phase using Python’s
multiprocessing module:
import multiprocessing
def parallel_map(input_data):
with multiprocessing.Pool() as pool:
mapped_data = pool.map(map_function, input_data)
return [item for sublist in mapped_data for item in sublist] # Flatten the list
if __name__ == "__main__":
input_data = [
"cat dog dog",
"cat bird bird bird",
"dog cat"]
# Step 1: Parallelized Map Phase
mapped_data = parallel_map(input_data)
# Step 2: Shuffle and Sort Phase
grouped_data = shuffle_sort(mapped_data)
# Step 3: Reduce Phase
reduced_data = reduce_function(grouped_data)
# Output the result
for word, count in reduced_data.items():
print(f"{word}: {count}")
Output (Parallelized Version):
cat: 3
dog: 3
bird: 3
Benefits of This Implementation
❖ Simplicity: This implementation provides a clear understanding of the MapReduce
process using basic Python functions without any external libraries.
❖ Flexibility: The map_function and reduce_function can easily be modified for
different tasks beyond word counting, such as filtering or summarizing data.
❖ Parallelism: By using Python’s multiprocessing module, We can simulate the parallel
execution of the Map phase, which mimics the distributed nature of real MapReduce
frameworks.
___________________________________________________________________________

You might also like