0% found this document useful (0 votes)
27 views205 pages

Ilovepdf Merged

The document provides an overview of string operations in Python, including creation, access, addition, editing, deletion, and various string functions. It covers concepts such as indexing, slicing, string immutability, and common string methods like count, find, and replace. Additionally, it includes example programs demonstrating string manipulation techniques.

Uploaded by

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

Ilovepdf Merged

The document provides an overview of string operations in Python, including creation, access, addition, editing, deletion, and various string functions. It covers concepts such as indexing, slicing, string immutability, and common string methods like count, find, and replace. Additionally, it includes example programs demonstrating string manipulation techniques.

Uploaded by

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

Strings

Seq. of Chars

In Python: Unicode Chars

Ops:

• Create
• Access
• Add Chars
• Edit
• Delete
• Ops on Strings
• String Functions

1. Create
In [1]: c = 'Hello'
print(c)

Hello

In [2]: c = "Hello"
print(c)

Hello

In [3]: 'It's raining outside'

File "C:\Users\pc\AppData\Local\Temp/ipykernel_16404/2132155197.py", line 1


'It's raining outside'
^
SyntaxError: invalid syntax

In [4]: "It's raining outside"

Out[4]: "It's raining outside"

In [5]: c = '''Hello''' # multi-line string


print(c)

Hello

In [6]: c = """Hello"""
print(c)

Hello
In [7]: c = str("Hello")
c

Out[7]: 'Hello'

2. Access

Accessing Substrings from a String


In [8]: # Concept of Indexing
c = "hello"
print(c)

hello

In [9]: print(c[0])

Indexing Types:

• Positive
• Negative

In [10]: print(c[-1])

In [11]: # Slicing
c = "Hello World"
print(c)

Hello World

In [12]: print(c[0:5])

Hello

In [13]: print(c[2:])

llo World

In [14]: print(c[:4])

Hell

In [15]: print(c[:])

Hello World

In [16]: print(c[2:6:2])

lo
In [17]: print(c[0:8:3])

HlW

In [18]: print(c[0:6:-1])

In [19]: print(c[-5:-1:2])

Wr

In [20]: print(c[::-1])

dlroW olleH

In [21]: print(c[-1:-5:-1])

dlro

3. Add Chars
In [1]: string1 = "Hello"
string2 = 'World'

In [2]: new_string = string1 + " " + string2


new_string

Out[2]: 'Hello World'

In [3]: another_string = string1 + ", how are you?"


another_string

Out[3]: 'Hello, how are you?'

4. Edit & 5. Delete


In [22]: c = "Hello"
print(c)

Hello

In [23]: c[0] = 'X'

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/2980535339.py in <module>
----> 1 c[0] = 'X'

TypeError: 'str' object does not support item assignment

Strings are a Immutable Data Types


In [25]: c = "World"
print(c)

World

In [26]: c[5] = "X"

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/2773719593.py in <module>
----> 1 c[5] = "X"

TypeError: 'str' object does not support item assignment

In [27]: # Deletion
c

Out[27]: 'World'

In [28]: del c

In [29]: print(c)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/2743823995.py in <module>
----> 1 print(c)

NameError: name 'c' is not defined

In [30]: c = "hello"

In [31]: print(c)

hello

In [32]: del c[0]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/3601072613.py in <module>
----> 1 del c[0]

TypeError: 'str' object doesn't support item deletion

In [33]: del c[:3:2]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/3090026040.py in <module>
----> 1 del c[:3:2]

TypeError: 'str' object does not support item deletion


6. Ops on Strings
• Arithmetic: Concatenation ( + ), Repetition ( * )
• Relational: Equality ( == ), Inequality ( != ), Comparison ( < , > , <= ,
>= )
• Logical: and , or , not
• Loops: for loop, while loop
• Membership: in , not in

In [34]: # Arithmetic
"Hello" + "-" + "World"

Out[34]: 'Hello-World'

In [35]: print("*"*50)

**************************************************

In [36]: print("Hello"*4)

HelloHelloHelloHello

In [37]: # Relational
"Hello" == "World"

Out[37]: False

In [38]: "Hello" != "World"

Out[38]: True

In [39]: "Mumbai" > "Pune" # Lexiographically

Out[39]: False

In [40]: "Goa" > "Kolkata"

Out[40]: False

In [41]: "kol" < "Kol"

Out[41]: False

In [42]: # Logical
"Hello" and "World"

Out[42]: 'World'

In [43]: # "" ---> False


# "saurabh" ---> True

In [44]: "" and "Hello"

Out[44]: ''

In [45]: "" or "World"

Out[45]: 'World'

In [46]: "Hello" or "World"

Out[46]: 'Hello'

In [47]: "Hello" and "World"

Out[47]: 'World'

In [48]: not "Hello"

Out[48]: False

In [49]: print(not "Hello")

False

In [50]: not ""

Out[50]: True

In [51]: # Loops on Strings


c = "Hello World"
for i in c:
print(i)

H
e
l
l
o

W
o
r
l
d

In [52]: for i in c[2:7]:


print(i)
l
l
o

In [53]: for i in c[2:7:2]:


print(i)

l
o
W

In [54]: for i in c[::-1]:


print(i)

d
l
r
o
W

o
l
l
e
H

In [55]: # Membership
c

Out[55]: 'Hello World'

In [56]: 'H' in c

Out[56]: True

In [57]: 'h' in c

Out[57]: False

In [58]: 'World' not in c

Out[58]: False

7. String Functions

1. Common Functions
• len()
• max()
• min()
• sorted()

In [59]: c = 'kolkata'
len(c)

Out[59]: 7

In [60]: max(c)

Out[60]: 't'

In [61]: min(c)

Out[61]: 'a'

In [62]: sorted(c)

Out[62]: ['a', 'a', 'k', 'k', 'l', 'o', 't']

In [63]: sorted(c, reverse=True)

Out[63]: ['t', 'o', 'l', 'k', 'k', 'a', 'a']

2. Capitalize/Title/Upper/Lower/Swapcase
In [64]: c
c.capitalize()

Out[64]: 'Kolkata'

In [65]: c

Out[65]: 'kolkata'

In [66]: 'it is raining today'.capitalize()

Out[66]: 'It is raining today'

In [67]: 'it is raining today'.title()

Out[67]: 'It Is Raining Today'

In [68]: c.upper().lower()

Out[68]: 'kolkata'

In [69]: "KoLkAtA".swapcase()
Out[69]: 'kOlKaTa'

3. Count
In [70]: "it is raining".count("i")

Out[70]: 4

In [71]: "it is raining".count("ing")

Out[71]: 1

In [72]: "it is raining".count("x")

Out[72]: 0

4. Find/Index
In [73]: "it is raining".find("i")

Out[73]: 0

In [74]: "it is raining".find("g")

Out[74]: 12

In [75]: "it is raining".find("raining")

Out[75]: 6

In [76]: "it is raining".find("x")

Out[76]: -1

In [77]: "it is raining".index("raining")

Out[77]: 6

In [78]: "it is raining".index("x")

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_16404/4196732353.py in <module>
----> 1 "it is raining".index("x")

ValueError: substring not found


5. endswith/startswith
In [79]: "it is raining".endswith("ing")

Out[79]: True

In [80]: "it is raining".endswith("ingf")

Out[80]: False

In [81]: "it is raining".startswith("it")

Out[81]: True

6. format
In [82]: "Hello my name is {} and I am {}".format("Saurabh", 22)

Out[82]: 'Hello my name is Saurabh and I am 22'

In [83]: "Hello my name is {1} and I am {0}".format("Saurabh", 22)

Out[83]: 'Hello my name is 22 and I am Saurabh'

In [84]: "Hello my name is {name} and I am {age}".format(name = "Saurabh", age = 22)

Out[84]: 'Hello my name is Saurabh and I am 22'

In [85]: "Hello my name is {age} and I am {name}".format(name = "Saurabh", age = 22)

Out[85]: 'Hello my name is 22 and I am Saurabh'

In [86]: "Hello my name is {name} and I am {name}".format(name = "Saurabh", age = 22, weight

Out[86]: 'Hello my name is Saurabh and I am Saurabh'

7. isalnum/isalpha/isdecimal/isdigit/isidentifier
In [87]: "FLAT20".isalnum() # Alphanumeric

Out[87]: True

In [88]: "FLAT20&".isalnum()

Out[88]: False

In [89]: "FLAT".isalpha() # Alphabetic


Out[89]: True

In [90]: "FLAT20".isalpha()

Out[90]: False

In [91]: "20".isdigit()

Out[91]: True

In [92]: "20A".isdigit()

Out[92]: False

In [93]: "Hello World".isidentifier()

Out[93]: False

In [94]: "Hello_World".isidentifier()

Out[94]: True

8. Split
In [95]: "who is the pm of india".split()

Out[95]: ['who', 'is', 'the', 'pm', 'of', 'india']

In [96]: "who is the pm of india".split("pm")

Out[96]: ['who is the ', ' of india']

In [97]: "who is the pm of india".split("i")

Out[97]: ['who ', 's the pm of ', 'nd', 'a']

In [98]: "who is the pm of india".split("x")

Out[98]: ['who is the pm of india']

9. Join
In [99]: " ".join(['who', 'is', 'the', 'pm', 'of', 'india'])

Out[99]: 'who is the pm of india'

In [100… "-".join(['who', 'is', 'the', 'pm', 'of', 'india'])


Out[100… 'who-is-the-pm-of-india'

10. Replace
In [101… "Hi my name is Saurabh".replace("Saurabh", "Siddhant")

Out[101… 'Hi my name is Siddhant'

11. Strip
In [102… name = " Saurabh "
name

Out[102… ' Saurabh '

In [103… "Hi" + name

Out[103… 'Hi Saurabh '

In [104… name.strip()

Out[104… 'Saurabh'

Example Programs
In [2]: # 1. Length of String without len()
s = input('enter the string: ')
counter = 0
for i in s:
counter += 1
print('length of string is', counter)

enter the string: saurabh


length of string is 7

In [4]: # 2. Extract username from email


# Eg: [email protected] ---> saurabhsinghdhami
s = input('enter the email: ')
pos = s.index('@')
print(s[0:pos])

enter the email: [email protected]


saurabhsinghdhami

In [5]: # 3. Count character frequency in a string


s = input('enter the email: ')
term = input('what would like to search for: ')
counter = 0
for i in s:
if i == term:
counter += 1
print('frequency', counter)

enter the email: [email protected]


what would like to search for: a
frequency 4

In [6]: # 4. Remove Character from String


s = input('enter the string: ')
term = input('what would like to remove: ')
result = ''
for i in s:
if i != term:
result += i
print(result)

enter the string: saurabh singh dhami


what would like to remove: s
aurabh ingh dhami

In [7]: # 5. Check if a string is a palindrome


s = input('enter the string: ')
flag = True
for i in range(0,len(s)//2):
if s[i] != s[len(s) - i -1]:
flag = False
print('Not a Palindrome')
break
if flag:
print('Palindrome')

enter the string: nitin


Palindrome

In [8]: # 6. Word Count Without split()


s = input('enter the string: ')
L = []
temp = ''
for i in s:
if i != ' ':
temp += i
else:
L.append(temp)
temp = ''
L.append(temp)
print(L)

enter the string: hi how are you?


['hi', 'how', 'are', 'you?']

In [9]: # 7. Convert String to Title Case (No title())


s = input('enter the string: ')
L = []
for i in s.split():
L.append(i[0].upper() + i[1:].lower())
print(" ".join(L))

enter the string: Hi My NaMe Is SaUrAbH SiNgH DhAmI


Hi My Name Is Saurabh Singh Dhami

In [10]: # 8. Integer to String Conversion


number = int(input('enter the number: '))
digits = '0123456789'
result = ''
while number != 0:
result = digits[number % 10] + result
number //= 10
print(result)
print(type(result))

enter the number: 345


345
<class 'str'>
List
Collection in Python for sequences of diverse data types.

List vs Array
• Array: Homogeneous; Contiguous memory; Fast; Numerical/Scientific
use.
• List: Heterogeneous; Non-contiguous memory; Programmer-friendly;
General-purpose.

1. Create
2. Access
3. Edit
4. Add
5. Delete
6. Operations
7. Functions

Arrays vs. Lists in Memory:


• Arrays:

▪ int arr[50]; ---> 50 contiguous memory blocks.


▪ Elements stored in binary form at consecutive addresses.
• Lists:

▪ list_example = [1, 2, 3] ---> Elements at different


locations.
▪ Stores references/pointers to elements, not the values.

In [1]: # proof --->


L = [1, 2, 3]
print(id(L))
print(id(L[0]))
print(id(L[1]))
print(id(L[2]))
print(id(1))
print(id(2))
print(id(3))
1867853113408
1867767939312
1867767939344
1867767939376
1867767939312
1867767939344
1867767939376

List Characteristics
• Ordered
• Mutable
• Heterogeneous
• Duplicates allowed
• Dynamic size
• Nesting supported
• Indexable
• Any object type

1. Create
In [3]: # Empty
L = []
L

Out[3]: []

In [4]: # 1D ---> Homo


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

Out[4]: [1, 2, 3, 4, 5]

In [5]: # Hetrogenous
L2 = ["Hello", 4, 5, 6, True, 5+6j]
L2

Out[5]: ['Hello', 4, 5, 6, True, (5+6j)]

In [6]: # Multidimentional list:


# 2D
L3 = [1, 2, 3, [4, 5]]
L3

Out[6]: [1, 2, 3, [4, 5]]

In [2]: # 3D
L4 = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
L4

Out[2]: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

In [8]: # Using Type Conversion


L5 = list("Saurabh")
L5

Out[8]: ['S', 'a', 'u', 'r', 'a', 'b', 'h']

In [9]: L6 = list()
L6

Out[9]: []

2. Access
In [10]: L1

Out[10]: [1, 2, 3, 4, 5]

In [11]: L1[0]

Out[11]: 1

In [12]: L1[-1]

Out[12]: 5

In [13]: # Slicing
L1[1:3]

Out[13]: [2, 3]

In [14]: L1[::-1]

Out[14]: [5, 4, 3, 2, 1]

In [15]: L3

Out[15]: [1, 2, 3, [4, 5]]

In [16]: L3[0]

Out[16]: 1

In [17]: L3[-1]
Out[17]: [4, 5]

In [18]: x = L3[-1]
x

Out[18]: [4, 5]

In [19]: x[0]

Out[19]: 4

In [20]: L3[-1][0]

Out[20]: 4

In [21]: L3[-1][-1]

Out[21]: 5

In [22]: L4

Out[22]: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

In [23]: L4[-1][-1][0]

Out[23]: 7

In [24]: L4[0][1][1]

Out[24]: 4

3. Edit
In [25]: L1

Out[25]: [1, 2, 3, 4, 5]

In [26]: # Editing With Indexing


L1[0] = 100
L1
# List in Python are Mutable

Out[26]: [100, 2, 3, 4, 5]

In [27]: L1[-1] = 500


L1

Out[27]: [100, 2, 3, 4, 500]


In [28]: # Editing With Slicing
L1[1:4] = [200, 300, 400]
L1

Out[28]: [100, 200, 300, 400, 500]

4. Add
• append()
• extend()
• insert()

In [31]: L1

Out[31]: [100, 200, 300, 400, 500]

In [32]: L1.append(1000)
L1

Out[32]: [100, 200, 300, 400, 500, 1000]

In [33]: L1.append("hello")
L1

Out[33]: [100, 200, 300, 400, 500, 1000, 'hello']

In [34]: L1.extend([5000, 6000, 7000])


L1

Out[34]: [100, 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000]

In [35]: L1.append([5, 6])


L1

Out[35]: [100, 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000, [5, 6]]

In [36]: L1.extend("goa")
L1

Out[36]: [100, 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000, [5, 6], 'g', 'o',
'a']

In [37]: L1.insert(1, "world")


L1

Out[37]: [100, 'world', 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000, [5, 6],
'g', 'o', 'a']
5. Delete
• del
• .remove()
• .pop()
• .clear()

In [39]: L2

Out[39]: ['Hello', 4, 5, 6, True, (5+6j)]

In [40]: del L2
L2

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_10180/3495304003.py in <module>
1 del L2
----> 2 L2

NameError: name 'L2' is not defined

In [41]: del L1[0]


L1

Out[41]: ['world', 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000, [5, 6], 'g',
'o', 'a']

In [42]: del L1[-4]


L1

Out[42]: ['world', 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000, 'g', 'o', 'a']

In [43]: del L1[-3:]


L1

Out[43]: ['world', 200, 300, 400, 500, 1000, 'hello', 5000, 6000, 7000]

In [44]: L1.remove("hello")
L1

Out[44]: ['world', 200, 300, 400, 500, 1000, 5000, 6000, 7000]

In [45]: L1.pop()
L1

Out[45]: ['world', 200, 300, 400, 500, 1000, 5000, 6000]

In [46]: L1.pop()
L1
Out[46]: ['world', 200, 300, 400, 500, 1000, 5000]

In [47]: L1.clear()
L1

Out[47]: []

6. Operations
• Arithmetic
• Membership
• Loop

In [48]: L = [1, 2, 3, 4]
L1 = [5, 6, 7, 8]

In [49]: # Concatenation/Merge
L + L1

Out[49]: [1, 2, 3, 4, 5, 6, 7, 8]

In [50]: L

Out[50]: [1, 2, 3, 4]

In [51]: L1

Out[51]: [5, 6, 7, 8]

In [52]: L * 3

Out[52]: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]

In [53]: # Loops
for i in L:
print(i)

1
2
3
4

In [54]: L3

Out[54]: [1, 2, 3, [4, 5]]

In [55]: for i in L3:


print(i)
1
2
3
[4, 5]

In [56]: 4 in L3

Out[56]: False

In [57]: [4, 5] in L3

Out[57]: True

7. Functions
• len()
• min()
• max()
• sorted()

In [58]: L

Out[58]: [1, 2, 3, 4]

In [59]: len(L)

Out[59]: 4

In [60]: min(L)

Out[60]: 1

In [61]: max(L)

Out[61]: 4

In [62]: sorted(L)

Out[62]: [1, 2, 3, 4]

In [63]: sorted(L, reverse = True)

Out[63]: [4, 3, 2, 1]

In [64]: L

Out[64]: [1, 2, 3, 4]
In [65]: L.sort(reverse = True)

In [66]: L

Out[66]: [4, 3, 2, 1]

In [67]: L.sort()

In [4]: # count
L = [1, 2, 1, 3, 4, 1, 5]
L.count(5)

Out[4]: 1

In [68]: L

Out[68]: [1, 2, 3, 4]

In [69]: # index
L.index(3)

Out[69]: 2

In [6]: # reverse
L = [2, 1, 5, 7, 0]
# permanently reverses the list
L.reverse()
print(L)

[0, 7, 5, 1, 2]

In [7]: # sort (vs sorted)


L = [2, 1, 5, 7, 0]
print(L)
print(sorted(L)) # New sorted list
print(L) # Original list (unchanged)
L.sort()
print(L) # Original list (sorted)

[2, 1, 5, 7, 0]
[0, 1, 2, 5, 7]
[2, 1, 5, 7, 0]
[0, 1, 2, 5, 7]

In [8]: # copy ---> shallow


L = [2, 1, 5, 7, 0]
print(L)
print(id(L))
L1 = L.copy()
print(L1)
print(id(L1))
[2, 1, 5, 7, 0]
1867853103360
[2, 1, 5, 7, 0]
1867853111936

In [70]: "hello how are you".title()

Out[70]: 'Hello How Are You'

In [71]: # Title Case a String Without title()


sample = "how are you?"
sample.split()
L = []
for i in sample.split():
L.append(i.capitalize())
print(L)
print(" ".join(L))

['How', 'Are', 'You?']


How Are You?

In [72]: sample = "[email protected]"


print(sample[:sample.find("@")])

saurabh

In [73]: L1 = [1, 1, 2, 2, 3, 3, 4, 4]
# Output: L2 = [1, 2, 3, 4]
L = []
for i in L1:
if i not in L:
L.append(i)
print(L)

[1, 2, 3, 4]

List Comprehension
Compact list creation.

newlist = [expresion for item in iterable if condition == True]


Example:

squares = [x**2 for x in range(10)] ---> squares 0-9.

Advantages

• Efficient: Time & space.


• Concise: Fewer lines.
• Formulaic: Iteration ---> expression.
In [30]: L = [1, 2, 3, 4, 5, 6, 7]
L

Out[30]: [1, 2, 3, 4, 5, 6, 7]

In [31]: L1 = [item * 2 for item in L]


L1

Out[31]: [2, 4, 6, 8, 10, 12, 14]

In [32]: L2 = [i**2 for i in range(10)]


L2

Out[32]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

In [33]: L3 = [i**2 for i in range(10) if i%2!=0]


L3

Out[33]: [1, 9, 25, 49, 81]

In [34]: fruits = ['Apple', 'Orange', 'Mango', 'Guava']


fruits

Out[34]: ['Apple', 'Orange', 'Mango', 'Guava']

In [35]: L4 = [fruit for fruit in fruits if fruit[0]=="O"]


L4

Out[35]: ['Orange']

In [11]: # Add 1-10 to list


L = []
for i in range(1, 11):
L.append(i)
print(L)

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [12]: L = [i for i in range(1, 11)]


print(L)

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [13]: # Scalar Multiplication


v = [2, 3, 4] # Vector
s = -3 # Scalar
# [-6, -9, -12]
[s*i for i in v]

Out[13]: [-6, -9, -12]

In [15]: # Add squares


L = [1, 2, 3, 4, 5]
[i**2 for i in L]

Out[15]: [1, 4, 9, 16, 25]

In [16]: # Print nums divisible by 5 from 1 to 50


[i for i in range(1, 51) if i%5 == 0]

Out[16]: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

In [17]: # Languages starting with 'p'


languages = ['java', 'python', 'php', 'c', 'javascript']
[language for language in languages if language.startswith('p')]

Out[17]: ['python', 'php']

In [18]: # Nested If with List Comprehension


basket = ['apple', 'guava', 'cherry', 'banana']
my_fruits = ['apple', 'kiwi', 'grapes', 'banana']
# Add fruits from `my_fruits` that are in `basket` and start with 'a'
[fruit for fruit in my_fruits if fruit in basket if fruit.startswith('a')]

Out[18]: ['apple']

In [19]: # 3x3 Matrix via Nested List Comprehension


[[i*j for i in range(1, 4)] for j in range(1, 4)]

Out[19]: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

In [20]: # Cartesian Products ---> List Comprehension on 2 lists together


L1 = [1, 2, 3, 4]
L2 = [5, 6, 7, 8]
[i*j for i in L1 for j in L2]

Out[20]: [5, 6, 7, 8, 10, 12, 14, 16, 15, 18, 21, 24, 20, 24, 28, 32]

List Traversal
1. Itemwise
2. Indexwise

In [21]: # Itemwise
L = [1, 2, 3, 4]
for i in L:
print(i)

1
2
3
4
In [22]: # Indexwise
L = [1, 2, 3, 4]
for i in range(0, len(L)):
print(L[i])

1
2
3
4

Zip
zip() Function:

• Yields zip obj: iterator of tuples.


• Combines 1st items of each iterator, then 2nd, etc.
• Example:
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
zipped = zip(list1, list2)
print(list(zipped)) # Output: [(1, 'a'), (2, 'b'), (3,
'c')]

Length Mismatch:

• Shortest iterator determines output length.

In [24]: # Add items of 2 lists indexwise


L1 = [1, 2, 3, 4]
L2 = [-1, -2, -3, -4]
list(zip(L1, L2))
[i+j for i, j in zip(L1, L2)]

Out[24]: [0, 0, 0, 0]

In [25]: L = [1, 2, print, type, input]


print(L)

[1, 2, <built-in function print>, <class 'type'>, <bound method Kernel.raw_inpu


t of <ipykernel.ipkernel.IPythonKernel object at 0x000001B2E47A53F0>>]

Disadvantages of Python Lists


• Slow
• Risky usage
• High memory usage

In [26]: a = [1, 2, 3]
b = a.copy()
print(a)
print(b)
a.append(4)
print(a)
print(b)
# lists are mutable

[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]

List Programs
In [1]: # Split list into odd and even
L = [1, 2, 3, 4, 5, 6]

# Odd numbers
odd_numbers = [x for x in L if x % 2 != 0]

# Even numbers
even_numbers = [x for x in L if x % 2 == 0]

print("Odd numbers:", odd_numbers)


print("Even numbers:", even_numbers)

Odd numbers: [1, 3, 5]


Even numbers: [2, 4, 6]

In [3]: # List Input from User

# 1. Prompt input
input_string = input("Enter the list elements separated by spaces: ")

# 2. Split string
string_list = input_string.split()

# 3. Convert to integers
integer_list = [int(item) for item in string_list]

# 4. Output
print("The input list is:", integer_list)

Enter the list elements separated by spaces: 12345


The input list is: [12345]

In [5]: # Merge 2 Lists Without + Operator


L1 = [1, 2, 3, 4]
L2 = [5, 6, 7, 8]

# 1. Using `extend()` Method


L1 = [1, 2, 3, 4]
L2 = [5, 6, 7, 8]
# Merge L2 into L1
L1.extend(L2)
print("Merged list:", L1)

# 2. Using for Loop


L1 = [1, 2, 3, 4]
L2 = [5, 6, 7, 8]
# Merge L2 into L1 using a loop
for element in L2:
L1.append(element)
print("Merged list:", L1)

Merged list: [1, 2, 3, 4, 5, 6, 7, 8]


Merged list: [1, 2, 3, 4, 5, 6, 7, 8]

In [6]: # Replace item in list


L = [1, 2, 3, 4, 5, 3]
# replace 3 with 300

L = [1, 2, 3, 4, 5, 3]
old_item = 3
new_item = 300
# Iterate through the list and replace old_item with new_item
for i in range(len(L)):
if L[i] == old_item:
L[i] = new_item
print("Updated list:", L)

Updated list: [1, 2, 300, 4, 5, 300]

In [7]: # Convert 2D to 1D List

# Define 2D list
L2D = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Initialize 1D list
L1D = []

# Flatten 2D to 1D
for sublist in L2D:
for item in sublist:
L1D.append(item)
print("1D list:", L1D)

1D list: [1, 2, 3, 4, 5, 6, 7, 8, 9]

In [8]: # Remove Duplicates from List


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

# Convert to set ---> Removes duplicates; Convert back to list.


L_unique = list(set(L))

print("List with duplicates removed:", L_unique)

List with duplicates removed: [1, 2, 3, 4, 5]


In [9]: # Check if list is ascending
def is_ascending(L):
for i in range(len(L) - 1):
if L[i] > L[i + 1]:
return False
return True

# Test
L1 = [1, 2, 3, 4, 5]
L2 = [1, 3, 2, 4, 5]

print("L1 is in ascending order:", is_ascending(L1))


print("L2 is in ascending order:", is_ascending(L2))

L1 is in ascending order: True


L2 is in ascending order: False
Tuples
Immutable list-like collection.

Characteristics:
• Ordered elements.
• Unmodifiable post-creation.
• Allows duplicates.

1. Create
2. Access
3. Edit
4. Add
5. Delete
6. Operations
7. Functions

1. Create
In [1]: # empty
T1 = ()
T1

Out[1]: ()

In [2]: # homo
T2 = (1, 2, 3, 4, 5)
T2

Out[2]: (1, 2, 3, 4, 5)

In [3]: # hetro
T3 = ("Hello", 4, 5, 6)
T3

Out[3]: ('Hello', 4, 5, 6)

In [4]: # tuple
T4 = (1, 2, 3, (4, 5))
T4

Out[4]: (1, 2, 3, (4, 5))


In [5]: T5 = (1)
T5

Out[5]: 1

In [6]: type(T5)

Out[6]: int

In [7]: # Type Conversion


T5 = ("Hello")
type(T5)

Out[7]: str

In [8]: # Single-item tuple creation


T5 = ("Hello",)
type(T5)

Out[8]: tuple

In [9]: T6 = tuple("Goa")
T6

Out[9]: ('G', 'o', 'a')

In [10]: T6 = tuple([1, 2, 3, 4])


T6

Out[10]: (1, 2, 3, 4)

2. Access
• Indexing
• Slicing

In [11]: T2

Out[11]: (1, 2, 3, 4, 5)

In [12]: T2[0]

Out[12]: 1

In [13]: T2[-1]

Out[13]: 5

In [14]: T2[:4]
Out[14]: (1, 2, 3, 4)

In [15]: T4

Out[15]: (1, 2, 3, (4, 5))

In [16]: T4[-1][0]

Out[16]: 4

3. Edit
In [17]: L = [1, 2, 3, 4, 5]

In [18]: L[0] = 100


L

Out[18]: [100, 2, 3, 4, 5]

In [19]: T2

Out[19]: (1, 2, 3, 4, 5)

In [20]: T2[0] = 100


# Immutable, like strings

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_22232/394845330.py in <module>
----> 1 T2[0] = 100

TypeError: 'tuple' object does not support item assignment

In [21]: # Tuples = immutable (like strings)

4. Add
In [22]: # not possible
# Tuples: immutable

5. Delete
In [23]: T1

Out[23]: ()

In [24]: del T1
T1
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_22232/1707313094.py in <module>
1 del T1
----> 2 T1

NameError: name 'T1' is not defined

In [25]: T3

Out[25]: ('Hello', 4, 5, 6)

In [26]: T2

Out[26]: (1, 2, 3, 4, 5)

In [27]: del T2(-1)

File "C:\Users\pc\AppData\Local\Temp/ipykernel_22232/1610192391.py", line 1


del T2(-1)
^
SyntaxError: cannot delete function call

In [28]: # Tuples are immutable

6. Operations
In [29]: T2

Out[29]: (1, 2, 3, 4, 5)

In [30]: T3

Out[30]: ('Hello', 4, 5, 6)

In [ ]: # + and *

In [31]: T2 + T3

Out[31]: (1, 2, 3, 4, 5, 'Hello', 4, 5, 6)

In [32]: T2 * 3

Out[32]: (1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5)

In [33]: # iteration
for i in T2:
print(i)
1
2
3
4
5

In [34]: # membership
1 in T2

Out[34]: True

7. Functions
In [35]: len(T2)

Out[35]: 5

In [36]: min(T2)

Out[36]: 1

In [37]: max(T2)

Out[37]: 5

In [38]: sum(T2)

Out[38]: 15

In [39]: sorted(T2)

Out[39]: [1, 2, 3, 4, 5]

In [40]: sorted(T2, reverse = True)

Out[40]: [5, 4, 3, 2, 1]

In [1]: # count
t = (1, 2, 3, 4, 5)
t.count(50)

Out[1]: 0

In [2]: # index
t.index(50)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[2], line 2
1 # index
----> 2 t.index(50)

ValueError: tuple.index(x): x not in tuple

Lists vs Tuples
Syntax:
• Lists: [ ]
• Tuples: ( )

Mutability:
• Lists: Mutable
• Tuples: Immutable

Speed:
• Lists: Slower (mutable)
• Tuples: Faster (immutable)

Memory:
• Lists: Higher
• Tuples: Lower

Functionality:
• Both: Indexing, slicing
• Lists: More methods

Error-Prone:
• Lists: Modifiable
• Tuples: Safer

Use Case:
• Lists: Dynamic
• Tuples: Static
In [3]: import time
L = list(range(100000000))
T = tuple(range(100000000))

# List timing
start = time.time()
for i in L:
i*5
print('List time', time.time()-start)

# Tuple timing
start = time.time()
for i in T:
i*5
print('Tuple time', time.time()-start)

List time 9.570151567459106


Tuple time 8.412890911102295

In [4]: import sys


L = list(range(1000))
T = tuple(range(1000))
print('List size', sys.getsizeof(L))
print('Tuple size', sys.getsizeof(T))

List size 8056


Tuple size 8040

In [5]: a = [1, 2, 3]
b = a
a.append(4)
print(a)
print(b)

[1, 2, 3, 4]
[1, 2, 3, 4]

In [6]: a = (1, 2, 3)
b = a
a = a + (4,)
print(a)
print(b)

(1, 2, 3, 4)
(1, 2, 3)

Q. Why use Tuples?


• Immutable; prevents changes.
• Ensures data integrity.
• Use for fixed collections.
• Example:
college_database = ('CS', 'Math', 'Physics')
# college_database[0] = 'Electronics' # TypeError
• Use for static data; lists for mutable.

Special Syntax
In [7]: # tuple unpacking
a, b, c = (1, 2, 3)
print(a, b, c)

1 2 3

In [8]: a, b = (1, 2, 3)
print(a, b)

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[8], line 1
----> 1 a,b = (1,2,3)
2 print(a,b)

ValueError: too many values to unpack (expected 2)

In [9]: a = 1
b = 2
a, b = b, a
print(a, b)

2 1

In [10]: a, b, *others = (1, 2, 3, 4)


print(a, b)
print(others)

1 2
[3, 4]

In [11]: # zipping tuples


a = (1, 2, 3, 4)
b = (5, 6, 7, 8)
tuple(zip(a, b))

Out[11]: ((1, 5), (2, 6), (3, 7), (4, 8))


Sets
Unordered, unique elements.

Set is mutable; elements are immutable.

Operations: Union, intersection, symmetric difference, etc.

Characteristics:
• Unordered
• Mutable
• Unique Elements
• No Mutable Data Types

Important Rules about Sets


• No Duplicates
• No Indexing/Slicing
• No Mutable Elements
• Set is Mutable

1. Create
2. Access
3. Edit
4. Add
5. Delete
6. Operations
7. Functions

1. Create
In [2]: # empty
S1 = {}
S1

Out[2]: {}

In [3]: type(S1)

Out[3]: dict
In [4]: S1 = set()
S1

Out[4]: set()

In [5]: type(S1)

Out[5]: set

In [6]: # 1D & 2D Sets


S1 = {1, 2, 3, 4, 5} # 1D Set
S1
# S2 = {1, 2, 3, {4, 5}} # 2D Set
# S2

Out[6]: {1, 2, 3, 4, 5}

In [7]: # homo and hetro


S2 = {"Hello", 4.5, True}
S2

Out[7]: {4.5, 'Hello', True}

In [8]: # Type Conversion


s4 = set([1, 2, 3])
print(s4)

{1, 2, 3}

In [9]: # duplicates not allowed


S3 = {1, 1, 2, 2, 3, 3}
S3

Out[9]: {1, 2, 3}

In [10]: S4 = {[1, 2, 3], "Hello"}


S4

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[10], line 1
----> 1 S4 = {[1,2,3],"Hello"}
2 S4

TypeError: unhashable type: 'list'

In [ ]: S4 = {(1, 2, 3), "Hello"}


S4

In [ ]: # set can't have mutable items


s6 = {1, 2, [3, 4]}
print(s6)
In [11]: # Sets have no indexing
# Hashing

In [12]: S5 = {{1}, {2}}

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[12], line 1
----> 1 S5 = {{1},{2}}

TypeError: unhashable type: 'set'

In [13]: s1 = {1, 2, 3}
s2 = {3, 2, 1}
print(s1 == s2)

True

2. Access
In [14]: S1

Out[14]: {1, 2, 3, 4, 5}

In [15]: S1[0]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[15], line 1
----> 1 S1[0]

TypeError: 'set' object is not subscriptable

In [16]: S1[-1]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[16], line 1
----> 1 S1[-1]

TypeError: 'set' object is not subscriptable

In [17]: S1[0:3]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[17], line 1
----> 1 S1[0:3]

TypeError: 'set' object is not subscriptable


3. Edit
In [18]: S1[2] = 100

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[18], line 1
----> 1 S1[2] = 100

TypeError: 'set' object does not support item assignment

In [19]: S1

Out[19]: {1, 2, 3, 4, 5}

In [20]: id(S1)

Out[20]: 1682006879008

In [21]: L = list(S1)
L

Out[21]: [1, 2, 3, 4, 5]

In [22]: L[0] = 100


L

Out[22]: [100, 2, 3, 4, 5]

In [23]: S1 = set(L)
S1

Out[23]: {2, 3, 4, 5, 100}

In [24]: id(S1)

Out[24]: 1682036493888

4. Add
In [25]: S1

Out[25]: {2, 3, 4, 5, 100}

In [26]: S1.add(6)
S1

Out[26]: {2, 3, 4, 5, 6, 100}


In [27]: id(S1)

Out[27]: 1682036493888

In [28]: S1.add(7)
S1

Out[28]: {2, 3, 4, 5, 6, 7, 100}

In [29]: id(S1)

Out[29]: 1682036493888

In [30]: S1.update([5, 6, 7])


print(S1)

{2, 3, 100, 5, 4, 6, 7}

5. Delete
• del
• remove()
• pop()
• discard()
• clear()

In [32]: S2

Out[32]: {4.5, 'Hello', True}

In [33]: del S2
S2

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[33], line 2
1 del S2
----> 2 S2

NameError: name 'S2' is not defined

In [34]: del S1[0]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[34], line 1
----> 1 del S1[0]

TypeError: 'set' object doesn't support item deletion

In [35]: S1
Out[35]: {2, 3, 4, 5, 6, 7, 100}

In [36]: S1.remove(100)
S1

Out[36]: {2, 3, 4, 5, 6, 7}

In [37]: S1.pop()
S1

Out[37]: {3, 4, 5, 6, 7}

In [38]: S1.pop()
S1

Out[38]: {4, 5, 6, 7}

In [39]: S1.discard(7)
S1

Out[39]: {4, 5, 6}

In [40]: S1.clear()
S1

Out[40]: set()

6. Operations
In [41]: S1 = {1, 2, 3, 4, 5}
S2 = {3, 4, 5, 6, 7}

In [42]: S1 + S2

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[42], line 1
----> 1 S1 + S2

TypeError: unsupported operand type(s) for +: 'set' and 'set'

In [43]: S1 * 3

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[43], line 1
----> 1 S1 * 3

TypeError: unsupported operand type(s) for *: 'set' and 'int'


In [44]: # Iteration
for i in S1:
print(i)

1
2
3
4
5

In [45]: # Membership Test


1 in S1

Out[45]: True

In [46]: 1 not in S1

Out[46]: False

In [47]: s1 = {1, 2, 3, 4, 5}
s2 = {4, 5, 6, 7, 8}

In [48]: # Union(|)
s1 | s2

Out[48]: {1, 2, 3, 4, 5, 6, 7, 8}

In [49]: # Intersection(&)
s1 & s2

Out[49]: {4, 5}

In [50]: # Difference(-)
s1 - s2
s2 - s1

Out[50]: {6, 7, 8}

In [51]: # Symmetric Difference(^)


s1 ^ s2

Out[51]: {1, 2, 3, 6, 7, 8}

7. Functions
• len()
• min()
• max()
• sorted()
In [53]: len(S1)

Out[53]: 5

In [54]: min(S1)

Out[54]: 1

In [55]: max(S1)

Out[55]: 5

In [56]: sorted(S1)

Out[56]: [1, 2, 3, 4, 5]

In [57]: sorted(S1, reverse = True)

Out[57]: [5, 4, 3, 2, 1]

In [58]: S1

Out[58]: {1, 2, 3, 4, 5}

In [59]: S2

Out[59]: {3, 4, 5, 6, 7}

In [60]: S1.union(S2)

Out[60]: {1, 2, 3, 4, 5, 6, 7}

In [61]: S1.intersection(S2)

Out[61]: {3, 4, 5}

In [62]: S1.difference(S2)

Out[62]: {1, 2}

In [63]: S2.difference(S1)

Out[63]: {6, 7}

In [64]: S1.symmetric_difference(S2)

Out[64]: {1, 2, 6, 7}

In [65]: S1.isdisjoint(S2)
Out[65]: False

In [66]: S1.issubset(S2)

Out[66]: False

In [67]: S1.issuperset(S2)

Out[67]: False

In [68]: S1.clear()
S1

Out[68]: set()

In [69]: # copy
s1 = {1, 2, 3}
s2 = s1.copy()
print(s1)
print(s2)

{1, 2, 3}
{1, 2, 3}

Frozen set
• Immutable set.
• Created via frozenset() .
• No modifications post-creation.
• Ideal for hashable items: strings, numbers.

In [70]: # create frozenset


fs1 = frozenset([1, 2, 3])
fs2 = frozenset([3, 4, 5])
fs1 | fs2

Out[70]: frozenset({1, 2, 3, 4, 5})

In [71]: # what works and what does not


# Works ---> all read functions
# Doesn't Work ---> write operations

In [72]: # When to use


# 2D sets
fs = frozenset([1, 2, frozenset([3, 4])])
fs

Out[72]: frozenset({1, 2, frozenset({3, 4})})


Set Comprehension
In [73]: # examples
{i**2 for i in range(1, 11) if i>5}

Out[73]: {36, 49, 64, 81, 100}


Dictionary
• Key-Value Pairs: Data mapping.
• Aliases: Map, associative array.

dict = { 'name': 'nitish', 'age': 33, 'gender': 'male' }

Traits:
• Mutable
• No indexing
• Unique keys
• Immutable keys

In [1]: {"Name":"Saurabh", "Gender":"Male"}

Out[1]: {'Name': 'Saurabh', 'Gender': 'Male'}

4 Key Rules About Dictionaries


1. No Indexing: No index support.
2. Mutable: Can modify post-creation.
3. Keys: Immutable (str, num, tuple); Values: Any type.
4. Unique Keys: Keys must be unique; overwrite on reassignment.

In [1]: # Mutable Types ---> Lists, Sets, Dicts

# Immutable Types ---> Str, Tuples, Int, Float, Bool, Complex

1. Create
2. Access
3. Edit
4. Add
5. Delete
6. Operations
7. Functions

1. Create
In [4]: # empty dictionary
D = {}
D

Out[4]: {}

In [5]: # 1D dictionary
D = {"Name":"Saurabh", "Gender":"Male"}
D

Out[5]: {'Name': 'Saurabh', 'Gender': 'Male'}

In [6]: D1 = {[1, 2, 3]:"Saurabh"}

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 D1 = {[1,2,3]:"Saurabh"}

TypeError: unhashable type: 'list'

In [ ]: # with mixed keys


D1 = {(1, 2, 3):"Saurabh"}
D1

In [ ]: # duplicate keys
D2 = {"Name":"Natty", "Name":"Maru"}
D2

In [7]: D3 = {"Name":"Saurabh", "College":"SGT", "Marks":{"M1":99, "DS":97, "Eng":98}}


D3

Out[7]: {'Name': 'Saurabh', 'College': 'SGT', 'Marks': {'M1': 99, 'DS': 97, 'Eng': 9
8}}

In [8]: # 2D dictionary ---> JSON


s = {
'name':'nitish',
'college':'bit',
'sem':4,
'subjects':{
'dsa':50,
'maths':67,
'english':34
}
}
s

Out[8]: {'name': 'nitish',


'college': 'bit',
'sem': 4,
'subjects': {'dsa': 50, 'maths': 67, 'english': 34}}

In [9]: # mutable items as keys


D4 = {'name':'nitish', (1, 2, 3):2}
print(D4)

{'name': 'nitish', (1, 2, 3): 2}

In [10]: # using sequence and dict function


D5 = dict([('name', 'nitish'), ('age', 32), (3, 3)])
D5

Out[10]: {'name': 'nitish', 'age': 32, 3: 3}

2. Access
In [11]: D

Out[11]: {'Name': 'Saurabh', 'Gender': 'Male'}

In [12]: D[0]

---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[12], line 1
----> 1 D[0]

KeyError: 0

In [13]: D["Name"]

Out[13]: 'Saurabh'

In [14]: D.get("Name")

Out[14]: 'Saurabh'

In [15]: D["Gender"]

Out[15]: 'Male'

In [16]: D.get("Gender")
# .get() ---> 1-D dicts only; Not for 2-D/nested dicts.

Out[16]: 'Male'

In [17]: D3

Out[17]: {'Name': 'Saurabh', 'College': 'SGT', 'Marks': {'M1': 99, 'DS': 97, 'Eng': 9
8}}

In [18]: D3["Marks"]["DS"]

Out[18]: 97
3. Edit
In [19]: D

Out[19]: {'Name': 'Saurabh', 'Gender': 'Male'}

In [20]: D["Name"] = "Kartik"


D

Out[20]: {'Name': 'Kartik', 'Gender': 'Male'}

In [21]: D3["Marks"]["DS"] = 10
D3

Out[21]: {'Name': 'Saurabh', 'College': 'SGT', 'Marks': {'M1': 99, 'DS': 10, 'Eng': 9
8}}

4. Add
In [22]: D

Out[22]: {'Name': 'Kartik', 'Gender': 'Male'}

In [23]: D["Age"] = 22
D

Out[23]: {'Name': 'Kartik', 'Gender': 'Male', 'Age': 22}

In [24]: D3["Marks"]["M2"] = 95
D3

Out[24]: {'Name': 'Saurabh',


'College': 'SGT',
'Marks': {'M1': 99, 'DS': 10, 'Eng': 98, 'M2': 95}}

5. Delete
In [29]: D5 = {}
D5

Out[29]: {}

In [30]: del D5
D5
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[30], line 2
1 del D5
----> 2 D5

NameError: name 'D5' is not defined

In [32]: D.pop('Name')

Out[32]: 'Kartik'

In [33]: D.popitem()

Out[33]: ('Age', 22)

In [34]: D

Out[34]: {'Gender': 'Male'}

In [35]: del D["Gender"]


D

Out[35]: {}

In [36]: D.clear()
D

Out[36]: {}

6. Operations
In [39]: D3

Out[39]: {'Name': 'Saurabh',


'College': 'SGT',
'Marks': {'M1': 99, 'DS': 10, 'Eng': 98, 'M2': 95}}

In [42]: D4

Out[42]: {'name': 'nitish', (1, 2, 3): 2}

In [43]: D3 + D4

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[43], line 1
----> 1 D3 + D4

TypeError: unsupported operand type(s) for +: 'dict' and 'dict'


In [44]: D3 * 3

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[44], line 1
----> 1 D3 * 3

TypeError: unsupported operand type(s) for *: 'dict' and 'int'

In [45]: D3

Out[45]: {'Name': 'Saurabh',


'College': 'SGT',
'Marks': {'M1': 99, 'DS': 10, 'Eng': 98, 'M2': 95}}

In [46]: # Iteration
for i in D3:
print(i)

Name
College
Marks

In [47]: for i in D3:


print(i, D3[i])

Name Saurabh
College SGT
Marks {'M1': 99, 'DS': 10, 'Eng': 98, 'M2': 95}

In [48]: # Membership
"Saurabh" in D3

Out[48]: False

In [49]: "Name" in D3

Out[49]: True

7. Functions
In [50]: len(D3)

Out[50]: 3

In [51]: min(D3)

Out[51]: 'College'

In [52]: max(D3)

Out[52]: 'Name'
In [53]: sorted(D3)

Out[53]: ['College', 'Marks', 'Name']

In [43]: sorted(D3, reverse = True)

Out[43]: ['Name', 'Marks', 'College']

In [54]: D3.items()

Out[54]: dict_items([('Name', 'Saurabh'), ('College', 'SGT'), ('Marks', {'M1': 99, 'D


S': 10, 'Eng': 98, 'M2': 95})])

In [55]: D3.keys()

Out[55]: dict_keys(['Name', 'College', 'Marks'])

In [56]: D3.values()

Out[56]: dict_values(['Saurabh', 'SGT', {'M1': 99, 'DS': 10, 'Eng': 98, 'M2': 95}])

In [57]: # update
d1 = {1:2, 3:4, 4:5}
d2 = {4:7, 6:8}
d1.update(d2)
print(d1)

{1: 2, 3: 4, 4: 7, 6: 8}

Dictionary Comprehension
{key : value for var in iterable}
Creates dicts from iterables.

Example:

squares_dict = {x: x**2 for x in range(5)}

Maps numbers to squares.

Supports conditions & expressions.

In [1]: D = {"Name":"Nitish", "Gender":"Male", "Age":30}

In [2]: D.items()

Out[2]: dict_items([('Name', 'Nitish'), ('Gender', 'Male'), ('Age', 30)])

In [3]: D1 = {key:value for key, value in D.items() if len(key)>3}


D1
Out[3]: {'Name': 'Nitish', 'Gender': 'Male'}

In [4]: L = [1, 2, 3, 4, 5, 6, 7]

In [5]: D2 = {item : item**2 for item in L}


D2

Out[5]: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49}

In [6]: D2 = {item : item**2 for item in L if item%2 == 0}


D2

Out[6]: {2: 4, 4: 16, 6: 36}

In [58]: # Print 1st 10 nums & squares


{i:i**2 for i in range(1, 11)}

Out[58]: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}

In [59]: distances = {'delhi':1000, 'mumbai':2000, 'bangalore':3000}


print(distances.items())

dict_items([('delhi', 1000), ('mumbai', 2000), ('bangalore', 3000)])

In [60]: # using existing dict


distances = {'delhi':1000, 'mumbai':2000, 'bangalore':3000}
{key:value*0.62 for (key, value) in distances.items()}

Out[60]: {'delhi': 620.0, 'mumbai': 1240.0, 'bangalore': 1860.0}

In [61]: # using zip


days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
temp_C = [30.5, 32.6, 31.8, 33.4, 29.8, 30.2, 29.9]
{i:j for (i, j) in zip(days, temp_C)}

Out[61]: {'Sunday': 30.5,


'Monday': 32.6,
'Tuesday': 31.8,
'Wednesday': 33.4,
'Thursday': 29.8,
'Friday': 30.2,
'Saturday': 29.9}

In [62]: # using if condition


products = {'phone':10, 'laptop':0, 'charger':32, 'tablet':0}
{key:value for (key, value) in products.items() if value>0}

Out[62]: {'phone': 10, 'charger': 32}

In [63]: # Nested Comprehension


# Print multiplication tables for 2 to 4
{i:{j:i*j for j in range(1, 11)} for i in range(2, 5)}
Out[63]: {2: {1: 2, 2: 4, 3: 6, 4: 8, 5: 10, 6: 12, 7: 14, 8: 16, 9: 18, 10: 20},
3: {1: 3, 2: 6, 3: 9, 4: 12, 5: 15, 6: 18, 7: 21, 8: 24, 9: 27, 10: 30},
4: {1: 4, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 7: 28, 8: 32, 9: 36, 10: 40}}

In [64]: {
2:{1:2, 2:4, 3:6, 4:8},
3:{1:3, 2:6, 3:9, 4:12},
4:{1:4, 2:8, 3:12, 4:16}
}

Out[64]: {2: {1: 2, 2: 4, 3: 6, 4: 8},


3: {1: 3, 2: 6, 3: 9, 4: 12},
4: {1: 4, 2: 8, 3: 12, 4: 16}}
Variable and Memory References
In [1]: # python calls variable as name
a

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_14692/4139848714.py in <module>
1 # python calls variable as name
----> 2 a

NameError: name 'a' is not defined

Call by Object Reference

Variable Assignment:
a = 4
a = 4 : a points to memory location of 4 .

4 stored in a: 4 in memory, a ---> 4 's address.

In [3]: id(a)

Out[3]: 2420901374352

In [4]: hex(2883116886416)

Out[4]: '0x29f47286990'

In [5]: id(4)

Out[5]: 2420901374352

Aliasing
In [6]: a = 5
b = a
# aliasing

In [7]: id(a)

Out[7]: 2420901374384

In [8]: id(b)
Out[8]: 2420901374384

In [1]: # a & b reference the same memory address.

In [10]: c = b

In [11]: id(c)

Out[11]: 2420901374384

In [2]: # c also starts pointing the same memory address.

In [13]: del(a)

In [14]: b

Out[14]: 5

In [15]: del(b)

In [16]: c

Out[16]: 5

In [3]: # removing reference, not actual value/name. original value remains intact.

In [18]: a = 5
b = a
a = 6

In [19]: b

Out[19]: 5

In [4]: # Initially, a = b = 5
# Changing a to 6 leaves b at 5

Reference Counting
In [21]: # Reference Counting tracks variables referencing a memory address.
a = "erherherh" # Memory address X
b = a # b ---> X
c = b # c ---> X

In [22]: id(a)

Out[22]: 2420982108464
In [23]: id(b)

Out[23]: 2420982108464

In [24]: id(c)

Out[24]: 2420982108464

In [25]: b

Out[25]: 'erherherh'

In [26]: c

Out[26]: 'erherherh'

In [27]: import sys

In [28]: sys.getrefcount(a)

Out[28]: 16

sys.getrefcount() :

Shows +1 reference count.

Internally adds a temporary reference.

In [30]: a = "abcdef"
b = a
c = b

In [31]: sys.getrefcount(a)

Out[31]: 4

Garbage Collection
Unused memory remains after references are deleted.

Garbage Collector manages and frees this space.

Periodic deletion of unreferenced values is called Garbage Collection


Wierd Stuff
Python's Wierd Behaviour/Oddities:
1. Ref Count Anomaly/Getrefcount Anomaly: sys.getrefcount()
shows an extra count due to temp ref.
2. Integer Caching: Caches -5 to 256 integers for optimization.
3. String Interning: Same string literals share memory.

In [33]: # WB 1
a = 2
b = a
c = b

In [34]: sys.getrefcount(a)

Out[34]: 1681

In [5]: # System vars often use 2.


# a = 2 ---> a points to 2's existing mem loc, no new 2 created.

In [36]: a = 61
b = a
c = b

In [37]: sys.getrefcount(a)

Out[37]: 25

In [38]: a = 717
b = a
c = b

In [39]: sys.getrefcount(a)

Out[39]: 4

In [40]: a = 717
# this is not aliasing

In [41]: sys.getrefcount(a)

Out[41]: 2

In [42]: d = c
# this is aliasing
In [43]: sys.getrefcount(a)

Out[43]: 2

In [44]: a = 4
b = 4

In [45]: id(a)

Out[45]: 2420901374352

In [46]: id(b)

Out[46]: 2420901374352

In [47]: a = 256
b = 256

In [48]: id(a)

Out[48]: 2420901570960

In [49]: id(b)

Out[49]: 2420901570960

In [50]: a = 257
b = 257

In [51]: id(a)

Out[51]: 2420982884720

In [52]: id(b)

Out[52]: 2420982884400

In [53]: a = -5
b = -5

In [54]: id(a)

Out[54]: 2420901374064

In [55]: id(b)

Out[55]: 2420901374064

In [56]: a = -6
b = -6
In [57]: id(a)

Out[57]: 2420982884240

In [58]: id(b)

Out[58]: 2420982884752

Variable IDs in Python


• Range -5 to 256: Same ID for vars.
• Outside this range: Different IDs.

This is due to software optimization.

In [60]: a = 'haldia'
b = 'haldia'

In [61]: id(a)

Out[61]: 2420982910320

In [62]: id(b)

Out[62]: 2420982910320

In [63]: a = 'haldia inst tech'


b = 'haldia inst tech'

In [64]: id(a)

Out[64]: 2420982917600

In [65]: id(b)

Out[65]: 2420982915680

In [66]: a = 'haldia_inst_tech'
b = 'haldia_inst_tech'

In [67]: id(a)

Out[67]: 2420982917680

In [68]: id(b)

Out[68]: 2420982917680
In [6]: # Valid Identifiers yield Same IDs.
# Invalid Identifiers creates Different IDs.

Mutability
In [70]: L = [1, 2, 3]

In [71]: id(L)

Out[71]: 2420982908608

In [72]: id(1)

Out[72]: 2420901374256

In [73]: id(L[0])

Out[73]: 2420901374256

In [74]: id(2)

Out[74]: 2420901374288

In [75]: id(3)

Out[75]: 2420901374320

In [76]: L[2] = 1

In [77]: L

Out[77]: [1, 2, 1]

In [78]: id(L[2])

Out[78]: 2420901374256

In [79]: L = [1, 2, 3, [4, 5]]

Mutability ---> Ability to modify data at its


memory location.
Depends on Data type.
Immutable Data Types
• str , int , float , bool , complex , tuple

Mutable Data Types


• list , dict , set

In [83]: a = 'Hello'

In [84]: id(a)

Out[84]: 2420982941168

In [85]: a = a + 'World'

In [86]: a

Out[86]: 'HelloWorld'

In [87]: id(a)

Out[87]: 2420982924208

In [88]: T = (1, 2, 3)

In [89]: id(T)

Out[89]: 2420982094144

In [90]: T = T + (5, 6)

In [91]: T

Out[91]: (1, 2, 3, 5, 6)

In [92]: id(T)

Out[92]: 2420981669536

In [93]: L = [1, 2, 3]

In [94]: id(L)

Out[94]: 2420982910464

In [95]: L.append(4)
In [96]: L

Out[96]: [1, 2, 3, 4]

In [97]: id(L)

Out[97]: 2420982910464

In [7]: # Immutable Data Types:


# Ops = new objs, new mem locs, new refs.

# Mutable Data Types:


# Ops = in-place, same addr.

Side Effects of Mutation


In [99]: # Mutability can be dangerous sometimes
L = [1, 2, 3]

In [100… L1 = L

In [101… L1

Out[101… [1, 2, 3]

In [102… id(L)

Out[102… 2420982893056

In [103… id(L1)

Out[103… 2420982893056

In [104… L1.append(4)

In [105… id(L1)

Out[105… 2420982893056

In [106… L1

Out[106… [1, 2, 3, 4]

In [107… L

Out[107… [1, 2, 3, 4]
Cloning
In [108… L

Out[108… [1, 2, 3, 4]

In [109… L1 = L[:]

In [110… id(L)

Out[110… 2420982893056

In [111… id(L1)

Out[111… 2420982138432

In [112… L1.append(5)

In [113… L1

Out[113… [1, 2, 3, 4, 5]

In [114… L

Out[114… [1, 2, 3, 4]

In [115… # In cloning we create a copy of the list at a different memory address

In [116… a = (1, 2, 3, [4, 5])

In [117… a

Out[117… (1, 2, 3, [4, 5])

In [118… a[-1][-1] = 500

In [119… a

Out[119… (1, 2, 3, [4, 500])

In [120… a = [1, 2, 3, (4, 5)]

In [121… a

Out[121… [1, 2, 3, (4, 5)]

In [122… a(-1)(-1) = 500


File "C:\Users\pc\AppData\Local\Temp/ipykernel_14692/4218575189.py", line 1
a(-1)(-1) = 500
^
SyntaxError: cannot assign to function call

In [123… a = [1, 2]

In [124… b = [3, 4]

In [125… c = (a, b)

In [126… c

Out[126… ([1, 2], [3, 4])

In [127… id(a)

Out[127… 2420982924544

In [128… id(b)

Out[128… 2420982150336

In [129… id(c)

Out[129… 2420981226688

In [130… c[0][0] = 100

In [131… c

Out[131… ([100, 2], [3, 4])

In [132… id(a)

Out[132… 2420982924544

In [133… id(c)

Out[133… 2420981226688

In [134… L = [1, 2, 3]

In [135… id(L)

Out[135… 2420982145216

In [136… L = L + [4, 5]
In [137… id(L)

Out[137… 2420982891072

append , edit , insert , extend modifies list in place (mutable); same memory
address.

Concatenation creates new list; different memory address.

In [139… c

Out[139… ([100, 2], [3, 4])

In [140… c[0] = c[0] + [5, 6]

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_14692/3447556373.py in <module>
----> 1 c[0] = c[0] + [5,6]

TypeError: 'tuple' object does not support item assignment

In [141… c

Out[141… ([100, 2], [3, 4])

In [142… a

Out[142… [100, 2]

In [143… a = a + [5, 6]

In [144… a

Out[144… [100, 2, 5, 6]

In [145… c

Out[145… ([100, 2], [3, 4])


What are Functions?
Block of code for specific tasks.

Black Box Concept:


Ignore internal code; focus on inputs/outputs.

Advantages:
• Reusability
• Avoids repetition

Two Key Points Regarding Functions:


1. Abstraction
• Hides internal workings.
• Users know "what" it does, not "how".

2. Decomposition
• Splits systems into modules.
• Each module offers specific functionality.
• Modules can impact others.

In [ ]: # Components of a Function

def function_name(parameters):
"""docstring"""
statement(s)

`def` ---> Function start.


Name ---> Function identifier.
Params ---> Input values.
Colon (`:`) ---> Ends header.
Docstring ---> Function description.
Body ---> Statements.
`return` ---> Output value (optional).

function_name(values)
Let's create a function
In [5]: # Check if number is even/odd
def is_even(number):
"""
This function tells if a given number is odd or even
Input - any valid integer
Output - odd/even
Created By - Saurabh
Last edited - 22 Oct 2022
"""
if number % 2 == 0:
return "Even"
else:
return "Odd"

In [6]: for i in range(1, 11):


print(is_even(i))

Odd
Even
Odd
Even
Odd
Even
Odd
Even
Odd
Even

In [7]: print(is_even.__doc__)

This function tells if a given number is odd or even


Input - any valid integer
Output - odd/even
Created By - Saurabh
Last edited - 22 Oct 2022

In [8]: print.__doc__

Out[8]: "print(value, ..., sep=' ', end='\\n', file=sys.stdout, flush=False)\n\nPrint


s the values to a stream, or to sys.stdout by default.\nOptional keyword argu
ments:\nfile: a file-like object (stream); defaults to the current sys.stdou
t.\nsep: string inserted between values, default a space.\nend: string ap
pended after the last value, default a newline.\nflush: whether to forcibly f
lush the stream."

In [9]: type.__doc__

Out[9]: "type(object_or_name, bases, dict)\ntype(object) -> the object's type\ntype(n


ame, bases, dict) -> a new type"
Functions: 2 Perspectives
1. Creator's perspective
2. User's perspective

In [11]: pwd

Out[11]: 'C:\\Users\\pc\\100 Days of Python Programming'

In [1]: # Creating `is_even.py` file w `is_even()` function ---> to import into Jupyter Notebo

In [13]: import func_demo

In [14]: func_demo.is_even(34)

Out[14]: 'Even'

In [15]: func_demo.is_even("Hello")

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21972/3239686104.py in <module>
----> 1 func_demo.is_even("Hello")

~\100 Days of Python Programming\func_demo.py in is_even(number)


8 """
9
---> 10 if number % 2 == 0:
11 return "Even"
12 else:

TypeError: not all arguments converted during string formatting

In [16]: def is_even(number):


if type(number) == int:
if number % 2 == 0:
return "Even"
else:
return "Odd"
else:
return "Not allowed"

In [17]: import func_demo2 as fd

In [18]: fd.is_even("Hello")

Out[18]: 'Not allowed'


Parameters Vs Arguments
Parameters:
• Vars in () during func definition..
• Defined in func declaration.

def func(param1, param2):


# Body

Arguments:
• Values passed at func call.
• Inputs during function invocation.

func(arg1, arg2)

1. Default Argument

2. Positional Argument

3. Keyword Argument

*4. Arbitrary Argument ( *args )*

In [20]: def power(a, b):


return a**b

In [21]: power(2, 3)

Out[21]: 8

In [22]: power(3, 2)

Out[22]: 9

In [23]: power(3)

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21972/512475094.py in <module>
----> 1 power(3)

TypeError: power() missing 1 required positional argument: 'b'

In [24]: power()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21972/964676364.py in <module>
----> 1 power()

TypeError: power() missing 2 required positional arguments: 'a' and 'b'

In [25]: # Default Argument: Function arguments with default values.


def power(a=1, b=1):
return a**b

In [26]: power(2,3)

Out[26]: 8

In [27]: power(2)

Out[27]: 2

In [28]: power()

Out[28]: 1

In [ ]: # Positional Arguments: Values assigned by call order.


power(2, 3)

In [30]: # Keyword Argument: Values assigned to args by name at call time.

# NOTE: *Keyword args* will Overrides *Positional args*.

# Priority ---> Keyword args > Positional args.

power(b=2, a=3)

Out[30]: 9

In [32]: # Arbitrary Argument: Accepts any number of args.


# Useful when the number of arguments is unknown.
def flexi(*number):
product = 1
for i in number:
product *= i
print(product)

In [33]: flexi(1)

In [34]: flexi(1, 2)

2
In [35]: flexi(1, 2, 3)

In [36]: flexi(1, 2, 3, 4, 5, 6, 7, 8, 9)

362880

In [37]: def flexi(*number): # Flexible inputs ---> tuple


product = 1
print(number)
print(type)
for i in number:
product *= i
print(product)

In [38]: flexi(1, 2, 3, 4, 5)

(1, 2, 3, 4, 5)
<class 'type'>
120

*args and **kwargs


*args : Variable-length positional arguments.

def func(*args)
**kwargs : Variable-length keyword arguments.

def func(**kwargs)

In [1]: # *args
# Pass variable non-keyword args to func
def multiply(*kwargs):
product = 1
for i in kwargs:
product *= i
print(kwargs)
return product

In [2]: multiply(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12)

(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12)


Out[2]: 43545600

In [3]: # **kwargs
# Pass any no. of keyword args (key-value pairs).
# Acts like a dict.
def display(**salman):
for (key, value) in salman.items():
print(key, '->', value)
In [4]: display(india='delhi', srilanka='colombo', nepal='kathmandu', pakistan='islamabad'

india -> delhi


srilanka -> colombo
nepal -> kathmandu
pakistan -> islamabad

Notes: while using *args and **kwargs


• Argument order: normal ---> *args ---> **kwargs
• The words “ args ” and “ kwargs ” are only a convention, you can use
any name of your choice

How Functions Are Executed in Memory?


Functions in Python are defined when def is encountered. Execution continues
until a function call (e.g., print ) is made. Each call allocates a separate memory
block for that function. Variables within a function are confined to its own block.

analogy ---> RAM == city, program == house, function == room.

Functions operate independently, like distinct programs; their memory is released


post-completion.

In [ ]: Withour return statement


L = [1, 2, 3]
print(L.append(4))
print(L)

Global Var and Local Var

Examples:
In [39]: # Functions as Arguments
def func_a():
print("inside func_a: ")
# No return value ---> `None`
def func_b(y):
print("inside func_b: ")
return y
def func_c(z):
print("inside func_c: ")
return z()
print(func_a())
print(5 + func_b(2))
print(func_c(func_a))
inside func_a:
None
inside func_b:
7
inside func_c:
inside func_a:
None

In [40]: # Variable scope & function behavior

def f(y):
x = 1 # Local x
x += 1
print(x)

x = 5 # Global x
f(x) # Calls f()
print(x)

# Functions have local scope. Global vars coexist but are not affected.

2
5

Local Variables: Inside function.

Global Variables: Outside any function, in main program.


In [42]: def g(y):
print(x) # x (global) used in g()
print(x + 1) # x (global) remains 5; new int (6) created, x unchanged

x = 5
g(x)
print(x) # x = 5 remains unchanged

5
6
5

In [43]: def h(y):


x += 1 # Error: needs "global x" to modify x
x = 5
h(x)
print(x)

# Rule: Global vars: accessed but not modified in functions.


# Concept 1: Globals exist outside funcs, accessed by any func.
# Concept 2: Funcs without local vars can use globals.
# Concept 3: Locals access globals but can't modify.
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21972/2056165220.py in <module>
3
4 x = 5
----> 5 h(x)
6 print(x)
7

~\AppData\Local\Temp/ipykernel_21972/2056165220.py in h(y)
1 def h(y):
----> 2 x += 1 # leads to an error without line "global x" inside h
3
4 x = 5
5 h(x)

UnboundLocalError: local variable 'x' referenced before assignment

In [44]: # EXPLICITLY Modifying Global Variables Locally


def h(y):
global x # Note: Modifying global vars is discouraged
x += 1
x = 5
h(x)
print(x)

In [45]: # Complicated Scope


def f(x):
x += 1
print("in f(x): x =", x)
return x

x = 3
z = f(x)
print("in main proram scope: z =", z)
print("in main program scope: x =", x)

in f(x): x = 4
in main proram scope: z = 4
in main program scope: x = 3

Nested Functions
In [46]: def f():
print("Inside f")
def g():
print("Inside g")
g()

In [47]: f()
Inside f
Inside g

In [48]: g()
# Nested Function stays Abstracted/Hidden from main program

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21972/1648027428.py in <module>
----> 1 g()
2
3 # Nested Function stays abstracted/hidden from the main program.

TypeError: g() missing 1 required positional argument: 'y'

In [49]: def f():


print("Inside f")
def g():
print("Inside g")
f()
g()

In [ ]: f()
# Infinite Loop ---> Code will Crash ---> Kernel Dead

In [1]: # Harder Scope


def g(x):
def h():
x = "abc"
x += 1
print("in g(x): x =", x)
h()
return x
x = 3
z = g(x)

in g(x): x = 4

In [2]: # Complicated Scope


def g(x):
def h(x):
x += 1
print("in h(x): x =", x)
x += 1
print("in g(x): x =", x)
h(x)
return x
x = 3
z = g(x)
print("in main proram scope: x =", x)
print("in main program scope: z =", z)
in g(x): x = 4
in h(x): x = 5
in main proram scope: x = 3
in main program scope: z = 4

Everything in Python an Object

Functions too
In [3]: # Functions as Objects

In [4]: def f(num):


return num**2

In [5]: f(2)

Out[5]: 4

In [6]: f(4)

Out[6]: 16

In [7]: x = f # aliasing

In [8]: # since functions are objects just like int, str,

In [9]: x(2)

Out[9]: 4

In [10]: x(4)

Out[10]: 16

In [11]: del f # Del functions in Python

In [12]: f(2)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_22548/3272250079.py in <module>
----> 1 f(2)

NameError: name 'f' is not defined

In [13]: x(2) # Call by Object Reference

Out[13]: 4
In [14]: type(x)

Out[14]: function

In [15]: L = [1, 2, 3, 4]
L

Out[15]: [1, 2, 3, 4]

In [16]: L = [1, 2, 3, 4, x]
L

Out[16]: [1, 2, 3, 4, <function __main__.f(num)>]

In [17]: L[-1](-3) # sqr

Out[17]: 9

In [18]: L = [1, 2, 3, 4, x(5)]


L

Out[18]: [1, 2, 3, 4, 25]

In [19]: # In Python, Functions behave like any other Data type.


# Can be assigned, passed, and returned.

So What?
1. Renaming Function: def new_name(old_name):

2. Deleting Function: del func_name

3. Storing Function: func_var = def_func()

4. Returning Function: return func_name

5. Function as Argument: def outer(func): func()

In [20]: # Function as argument/input


def func_a():
print("inside func_a")
def func_c(z):
print("inside func_c")
return z()
print(func_c(func_a))

inside func_c
inside func_a
None
In [21]: # Returning a Function + Nested Calling
def f():
def x(a, b):
return a + b
return x
val = f()(3, 4)
print(val)

Functions are First-Class Citizens in Python.

In [12]: # type & id


def square(num):
return num**2
type(square)
id(square)

Out[12]: 2159425052320

In [7]: # reassign
x = square
id(x)
x(3)

Out[7]: 9

In [8]: a = 2
b = a
b

Out[8]: 2

In [9]: # Deleting Function


del square

In [10]: square(3)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[10], line 1
----> 1 square(3)

NameError: name 'square' is not defined

In [13]: # Storing
L = [1, 2, 3, 4, square]
L[-1](3)

Out[13]: 9

In [14]: s = {square}
s

Out[14]: {<function __main__.square(num)>}

Benefits of Functions
• Modularity: Self-contained code, modularizes login.
• Reusability: Write once, use forever.
• Readability: Organized and coherent.
Recursion
Function calls itself

Advantages:
• No loops needed.
• Solves problems without iteration.

"To understand recursion you must understand recursion"

Iterative Vs Recursive

a*b
In [2]: # iterative method
def multiply(a, b):
result = 0
for i in range(b):
result += a
print(result)
multiply(3, 4)

12

In [3]: # Recursive Method

# 1. Base Case: Define stopping condition.

# 2. Decompose: Break main problem into smaller subproblems until base case is reached

def mul(a, b):


if b == 1:
return a
else:
return a + mul(a, b-1)

In [4]: print(mul(3, 4))

12

In [5]: # Factorial via Recursion


def fact(number):
if number == 1:
return 1
else:
return number * fact(number-1)
print(fact(5))
120

In [6]: # Palindrome
def palin(text):
if len(text) <= 1:
print("palindrome")
else:
if text[0] == text[-1]:
palin(text[1:-1])
else:
print("not a palindrome")

In [7]: palin("madam")

palindrome

In [8]: palin("malayalam")

palindrome

In [9]: palin("python")

not a palindrome

In [10]: palin("abba")

palindrome

In [11]: # The Rabbit Problem: Fibonacci Number

# Scenario ---> 2 newborn rabbits: 1 male + 1 female monthly.


# Reproduce after 1 month, immortality.

def fib(m):
if m == 0 or m == 1:
return 1
else:
return fib(m-1) + fib(m-2)
print(fib(12)) # T = O(2^n)

# Key Concepts:
# Fibonacci Sequence
# Reproduction Rate
# Population Growth

233

In [12]: import time


start = time.time()
print(fib(12))
print(time.time() - start)

233
0.0

In [13]: print(fib(24))
print(time.time() - start)

75025
0.22108960151672363

In [14]: print(fib(36))
print(time.time() - start)

24157817
6.103978157043457

Memoization
Memoization refers to remembering method call results based on inputs.

• Returns cached results, avoiding recomputation.


• Speeds up computations; stores previous results.
• Used in dynamic programming for recursive solutions.
• Reduces time complexity; avoids redundant calculations.
• Optimizes recursive algorithms by reusing results.

In [16]: def memo(m, d):


if m in d:
return d[m]
else:
d[m] = memo(m-1, d) + memo(m-2, d)
return d[m]
d = {0:1, 1:1}
print(memo(48, d))

7778742049

In [17]: print(memo(48, d))


print(time.time() - start)

7778742049
6.157997369766235

In [18]: print(memo(500, d))


print(time.time() - start)

2255915161619363308725126950360720720460113249137581905886388664184746277386868
83405015987052796968498626
6.17300009727478

In [19]: print(memo(1000, d))


print(time.time() - start)

7033036771142281582183525487718354977018126983635873274260490508715453711819693
3579742249494562611733487750449241765991088186363265450223647106012053374121273
867339111198139373125598767690091902245245323403501
6.189241170883179
In [20]: print(d) # Dict in memory, execution time reduced

In [21]: # Recursive PowerSet Function in Python

# PowerSet: Given set S, return power set P(S) (all subsets of S).

# Input: String
# Output: Array of Strings (power set)

# Example: S = "123", P(S) = ['', '1', '2', '3', '12', '13', '23', '123']

def powerset1(xs):
res = [[]]
if len(xs) <= 0:
return "Please Enter a parameter"
if len(xs) == 1:
res.append([xs[0]])
return res
else:
z = []
for i in powerset1(xs[1:]):
z.append(i)
z.append([xs[0]] + i)
return z

final = powerset1('123')
print(final)
print(len(final))

[[], ['1'], ['2'], ['1', '2'], ['3'], ['1', '3'], ['2', '3'], ['1', '2', '3']]
8
Lambda Functions
Anonymous functions.

Single Expression Limit.

Syntax: lambda args: expr

Example: lambda a, b: a + b

In [3]: x = lambda x : x**2


x(9)

Out[3]: 81

In [4]: a = lambda x, y : x + y
a(4, 5)

Out[4]: 9

In [5]: type(a)

Out[5]: function

Lambda Function vs. Normal Function


Lambda function:
• No return value.
• Written in Single-line.
• Not used for code reusability.
• Anonymous/No name.

Normal function:
• Has a return value.
• Multi-line.
• Encourages code reusability via named functions.

In [7]: # Why?

# Lambda functions are Anonymous functions ---> `lambda args: expr`,


# ideal for Higher-order functions, offers Concise function writing without naming.

In [ ]: # Higher Order Functions:


# Functions that take/return other functions.
# Useful for Abstraction, Code reuse.

# Enhances code modularity, readability.


# Encourages functional programming.

# Can lead to Complexity and Performance overhead (if overused).

In [9]: b = lambda x : x[0] == 'a'


b('apple')

Out[9]: True

In [10]: b('banana')

Out[10]: False

In [11]: b = lambda x :'Even' if x%2 == 0 else 'Odd'


b(3)

Out[11]: 'Odd'

In [12]: b(2)

Out[12]: 'Even'

In [13]: # HOF

In [14]: L = [11, 14, 27, 21, 23, 56, 78, 39, 45, 29, 28, 30]

# Even Sum
# Odd Sum
# Div3 Sum

def return_sum(L):
even_sum = 0
odd_sum = 0
div3_sum = 0
for i in L:
if i%2 == 0:
even_sum = even_sum + i
for i in L:
if i%2 != 0:
odd_sum = odd_sum + i
for i in L:
if i%3 == 0:
div3_sum = div3_sum + i
return(even_sum, odd_sum, div3_sum)
print(return_sum(L))

(206, 195, 240)


In [15]: L = [11, 14, 27, 21, 23, 56, 78, 39, 45, 29, 28, 30]
def return_sum(func, L):
result = 0
for i in L:
if func(i):
result = result + i
return result
x = lambda x : x%2 == 0 # Even Sum
y = lambda x : x%2 != 0 # Odd Sum
z = lambda x : x%3 == 0 # Div3 Sum
print(return_sum(x, L))
print(return_sum(y, L))
print(return_sum(z, L))

206
195
240

Higher-order functions (HOF) accepts input + define operation of function.

Higher-Order Functions
1. Map
2. Filter
3. Reduce

1. Map
Applies a function to each item in iterable .

Syntax:

map(function, iterable)
Returns an Iterator of results.

In [18]: L = [1, 2, 3, 4, 5, 6, 7]
L

Out[18]: [1, 2, 3, 4, 5, 6, 7]

In [19]: map(lambda x : x * 2, L)

Out[19]: <map at 0x1f17e229cd0>

In [20]: list(map(lambda x : x * 2, L))

Out[20]: [2, 4, 6, 8, 10, 12, 14]


In [21]: list(map(lambda x : x % 2 == 0, L))

Out[21]: [False, True, False, True, False, True, False]

In [22]: students = [
{
"name" : "Jacob Martin",
"Father name" : "Ros Martin",
"Address" : "123 Hills Street",
},{
"name" : "Angela Stevens",
"Father name" : "Robert Stevens",
"Address" : "3 Upper Street London",
},{
"name" : "Ricky Smart",
"Father name" : "William Smart",
"Address" : "Unknown",
}

]
list(map(lambda student : student["name"], students))

Out[22]: ['Jacob Martin', 'Angela Stevens', 'Ricky Smart']

2. Filter
Applies a function to sequence .

Syntax:

filter(function, sequence)
Returns elements where function is True .

In [24]: L

Out[24]: [1, 2, 3, 4, 5, 6, 7]

In [25]: list(filter(lambda x : x > 4, L))

Out[25]: [5, 6, 7]

In [26]: fruits = ["Apple", "Orange", "Mango", "Guava"]

In [27]: list(filter(lambda fruit : "e" in fruit, fruits))

Out[27]: ['Apple', 'Orange']


3. Reduce
Reduces an iterable to a single value.

Syntax:

from functools import reduce


reduce(function, iterable)
Operates on pairs of elements until a single value remains.

Useful for Cumulative Operations (e.g., sum, product).

In [29]: import functools

In [30]: L

Out[30]: [1, 2, 3, 4, 5, 6, 7]

In [31]: functools.reduce(lambda x, y : x + y, L)

Out[31]: 28

In [32]: L1 = [12, 34, 56, 11, 21, 58]


L1

Out[32]: [12, 34, 56, 11, 21, 58]

In [33]: functools.reduce(lambda x, y : x if x>y else y, L1)

Out[33]: 58

In [34]: functools.reduce(lambda x, y : x if x<y else y, L1)

Out[34]: 11
OBJECT-ORIENTED PROGRAMMING
OOP is a programming paradigm using objects & classes.

The Observation
In [5]: L = [1, 2, 3, 4]

In [6]: L

Out[6]: [1, 2, 3, 4]

In [7]: L.upper()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[7], line 1
----> 1 L.upper()

AttributeError: 'list' object has no attribute 'upper'

In [10]: city = 'Kolkata'

In [11]: city.append('a')

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[11], line 1
----> 1 city.append('a')

AttributeError: 'str' object has no attribute 'append'

In [14]: a = 3

In [15]: a.upper()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[15], line 1
----> 1 a.upper()

AttributeError: 'int' object has no attribute 'upper'


"Everything in Python is an Object"

But, What is an Object?

What is OOP?

The PROBLEM!

Generality to Specificity
The Core Fundamental Feature of OOP:

"One of the core fundamental features of OOP is the ability to create Custom Data
Types tailored to specific needs (e.g., LinkedIn platform).

Without OOP:

• Reliance on primitive data types: int , list , tuple .


• Less optimal Not tailored, harder to manage.

With OOP:

• Custom Types: Efficient, intuitive, scalable.


• Encapsulation: Better feature management.
• Manageable Codebase.

So, What is Object Oriented Programming?


Key Concepts of OOP:

• Object
• Class
• Polymorphism
• Encapsulation
• Inheritance
• Abstraction

CLASS
• Blueprint for objects
• Defines object behavior

(from now onwards variable == object)


In [22]: a = 2

In [23]: type(a)

Out[23]: int

In [24]: L

Out[24]: [1, 2, 3, 4]

In Python,

Datatype = Class

Variable = Object of Class

Class:

1. Attributes: Data/Properties
2. Methods: Functions/Behavior

Class Basic Structure


In [30]: class Car:
color = "blue" # data
model = "sports" # data
def calculate_avg_speed(km, time): # method
# some code

In [31]: # Naming Conventions:

Class names ---> PascalCase

Data/functions ---> snake_case

# Examples:

PascalCase ---> ThisIsPascalCase

CamelCase ---> thisIsCamelCase

snake_case ---> snake_case

In [32]: Class Representation:

+-------------------------+
| - Car |
|-------------------------|
| - Color |
| - mileage | # attributes/data (private)
| - engine |
|-------------------------|
| + cal_avg_speed |
| + open_airbags | # methods/functions (public)
| + show_gps |
+-------------------------+

OBJECT
Object is an instance of a Class

Class is the data type; Object is a variable of a Class

In [ ]: # Object Examples:

1. Car ---> WagonR | wagonr = Car()


2. Sports ---> Gilli Danda | gillidanda = Sports()
3. Animals ---> Langoor | langoor = Animals()

In [36]: L = [1, 2, 3] # Obj Literal ---> Built-in classes

In [37]: L

Out[37]: [1, 2, 3]

In [38]: L = list()

In [39]: L

Out[39]: []

In [40]: city = str()

In [41]: city

Out[41]: ''

Practical Implementation of Class and Object

Lets Create a Class


In [45]: Functions Vs Methods:

Methods ---> Defined inside a class.


---> Accessed via object of the class.

Functions ---> Not inside a class.


---> General, accessible everywhere.

In [46]: len(L) ---> Function


General-purpose, applicable to types like `str`, `int`.

Out[46]: 0

In [47]: L.append(1) ---> Method


Defined in `list` class; usable only with `list` objects.

In [48]: L

Out[48]: [1]

Functions Inside Class will be called Methods.

In [68]: class Atm:


__counter = 1 # static/class var

def __init__(self): # `__init__` ---> Constructor


# Special method inside a class.
# Initializes instance members.
# Executes automatically on object creation.

self.__pin = ""
self.__balance = 0

self.sno = Atm.counter # instance var


Atm.__counter = Atm.__counter + 1
print(id(self))
# self.__menu()

@staticmethod
def get_counter():
return Atm.__counter

@staticmethod
def set_counter(new):
if type(new) == int:
Atm.__counter = new
else:
print('Not Allowed')

def get_pin(self):
return self.__pin

def set_pin(self, new_pin):


if type(new_pin) == str:
self.__pin = new_pin
print("Pin changed")
else:
print("Not allowed")
def __menu(self):
user_input = input("""
Hello, how would you like to proceed?
1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
""")
if user_input == "1":
self.create_pin()
elif user_input == "2":
self.deposit()
elif user_input == "3":
self.withdraw()
elif user_input == "4":
self.check_balance()
else:
print("bye")

def create_pin(self):
self.__pin = input("Enter your pin: ")
print("Pin set successfully")

def deposit(self):
temp = input("Enter your pin: ")
if temp == self.__pin:
amount = int(input("Enter the amount: "))
self.__balance = self.__balance + amount
print("Deposit successful")
else:
print("Invalid pin")

def withdraw(self):
temp = input("Enter your pin: ")
if temp == self.__pin:
amount = int(input("Enter the amount: "))
if amount <= self.__balance:
self.__balance = self.__balance - amount
print("Operation successful")
else:
print("insufficient funds")
else:
print("invalid pin")

def check_balance(self):
temp = input("Enter your pin: ")
if temp == self.__pin:
print(self.__balance)
else:
print("invalid pin")
In [75]: sbi = Atm()

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

In [55]: sbi.deposit()

Enter your pin: 1234


Enter the amount: 50000
Deposit successful

In [56]: sbi.check_balance()

Enter your pin: 1234


50000

In [57]: sbi.withdraw()

Enter your pin: 1234


Enter the amount: 25000
Operation successful

In [58]: sbi.check_balance()

Enter your pin: 1234


25000

In [59]: hdfc = Atm()

2104822250560

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 2345
Pin set successfully

In [60]: hdfc.deposit()

Enter your pin: 2345


Enter the amount: 100000
Deposit successful

In [61]: sbi.check_balance()
Enter your pin: 1234
25000

In [62]: hdfc.check_balance()

Enter your pin: 2345


100000

In [65]: # Constructor ---> Special/Magic/Dunder Methods

In [66]: dir(int)
Out[66]: ['__abs__',
'__add__',
'__and__',
'__bool__',
'__ceil__',
'__class__',
'__delattr__',
'__dir__',
'__divmod__',
'__doc__',
'__eq__',
'__float__',
'__floor__',
'__floordiv__',
'__format__',
'__ge__',
'__getattribute__',
'__getnewargs__',
'__gt__',
'__hash__',
'__index__',
'__init__',
'__init_subclass__',
'__int__',
'__invert__',
'__le__',
'__lshift__',
'__lt__',
'__mod__',
'__mul__',
'__ne__',
'__neg__',
'__new__',
'__or__',
'__pos__',
'__pow__',
'__radd__',
'__rand__',
'__rdivmod__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__rfloordiv__',
'__rlshift__',
'__rmod__',
'__rmul__',
'__ror__',
'__round__',
'__rpow__',
'__rrshift__',
'__rshift__',
'__rsub__',
'__rtruediv__',
'__rxor__',
'__setattr__',
'__sizeof__',
'__str__',
'__sub__',
'__subclasshook__',
'__truediv__',
'__trunc__',
'__xor__',
'as_integer_ratio',
'bit_count',
'bit_length',
'conjugate',
'denominator',
'from_bytes',
'imag',
'numerator',
'real',
'to_bytes']

Magic Methods
Predefined (e.g., __and__ , __bool__ , __float__ )

Triggered automatically, not called directly by objects.

Constructor is a magic method invoked during object creation.

In [72]: # What is Utility of Constructor?

Special method auto-executed at app start.

Handles ---> DB/Internet/Hardware connectivity


Initial configurations

In [73]: # Highly Philosophical Perspective:

If World == Class
God == Programmer
Humans == Object

Code Not Controlled by Humans == ?

Breathing, Eating, Drinking is under human control

But, ---> D E A T H

Death is controlled by constructor (God)

constructor's code includes death at birth


self

In [77]: sbi = Atm()

2104822251232

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
5
bye

In [60]: id(sbi)

Out[60]: 2658947923632

In [78]: # sbi = self

In [79]: hdfc = Atm()

2104822248976

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
5
bye

In [80]: id(hdfc)

Out[80]: 2104822248976

In [81]: # hdfc = self

In [82]: id(sbi)

Out[82]: 2104822251232

self refers to the current object instance.

The need of self


class holds data & methods; access via object.
self refers to current object; needed for inter-method/data access within a class.

Methods can't access other methods/data directly without self .

Creating Custom Data Type (Fraction) & Corresponding


Methods to Add, Sub, Mul and Divide Fractions
In [13]: class Fraction:

def __init__(self, n, d):


self.num = n
self.den = d

def __str__(self):
return"{}/{}".format(self.num, self.den)

def __add__(self, other):


temp_num = self.num * other.den + other.num * self.den
temp_den = self.den * other.den
return"{}/{}".format(temp_num, temp_den)

def __sub__(self, other):


temp_num = self.num * other.den - other.num * self.den
temp_den = self.den * other.den
return"{}/{}".format(temp_num, temp_den)

def __mul__(self, other):


temp_num = self.num * other.num
temp_den = self.den * other.den
return"{}/{}".format(temp_num, temp_den)

def __truediv__(self, other):


temp_num = self.num * other.den
temp_den = self.den * other.num
return"{}/{}".format(temp_num, temp_den)

In [91]: x = Fraction(4, 5)

In [92]: print(x)

4/5

In [93]: type(x)

Out[93]: __main__.Fraction

In [94]: y = Fraction(5, 6)

In [95]: print(y)

5/6
In [96]: L = [1, 2, 3, x]

In [97]: L

Out[97]: [1, 2, 3, <__main__.Fraction at 0x1ea112eb4f0>]

In [98]: print(x + y)

49/30

In [99]: print(x - y)

-1/30

In [100… print(x * y)

20/30

In [101… print(x / y)

24/25

ENCAPSULATION

Instance Variable
Unique value per object.

Defined inside the constructor (e.g., self.pin , self.balance ).

Different values for each object.

In [106… sbi = Atm()

2104822248160

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

In [107… sbi.balance

Out[107… 0

In [108… sbi.balance = 'ahjhaja'


In [109… sbi.deposit()

Enter your pin: 1234


Enter the amount: 50000
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[109], line 1
----> 1 sbi.deposit()

Cell In[76], line 54, in Atm.deposit(self)


52 if temp == self.pin:
53 amount = int(input("Enter the amount: "))
---> 54 self.balance = self.balance + amount
55 print("Deposit successful")
56 else:

TypeError: can only concatenate str (not "int") to str

In [95]: # Class Data Encapsulation:

Data should not be left exposed in a class.

Java ---> Use access modifiers (e.g., `private`) to hide data.

Python ---> Use `__` prefix (e.g., `self.__pin`, `self.__balance`) for data hiding

In [111… sbi = Atm()

2104822398544

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

Class Design ---> Hide (Encapsulate ) data members & Hide non-public methods.

In [113… # Python Name Mangling:

How does it hide? What happens behind the scenes?

Atm
__pin

python interpreter internally converts __pin ---> _Atm__pin

Why?
Name mangling to prevent direct access.
In [115… sbi.__balance = "abcdefg"

In [116… sbi.deposit()

Enter your pin: 1234


Enter the amount: 50000
Deposit successful

In [117… sbi.check_balance() # no error

Enter your pin: 1234


50000

In [118… the above code worked because,

`__balance` ---> `_Atm__balance` (name mangling)

`sbi.__balance` = `""`, creates `__balance` in `Atm` but unused.

`__balance` not used; `_Atm__balance` is used.

`__balance` creation doesn't affect code logic due to `__balance` not being utilized.

Code logic remains unchanged; class functionality remains unaffected.

In [119… # Suprising thing:

sbi._Atm__balance = "wgwwg"

In [120… sbi.deposit() # code crashed

Enter your pin: 1234


Enter the amount: 50000
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[120], line 1
----> 1 sbi.deposit()

Cell In[110], line 54, in Atm.deposit(self)


52 if temp == self.__pin:
53 amount = int(input("Enter the amount: "))
---> 54 self.__balance = self.__balance + amount
55 print("Deposit successful")
56 else:

TypeError: can only concatenate str (not "int") to str

In [125… # Python Variable Privacy:

`__balance` ---> Name-mangled to `_Atm__balance`.

_Truly private_ is not a concept in Python.

`__balance` is _pseudo-private_, but accessible via `_Atm__balance` if known.


Reason for Why "Nothing in Python is Truly Private"?

"Python targets adults; privacy is a convention, not enforcement."

__balance indicates intent for privacy; convention

__ prefix suggests private elements; use with caution, understanding


implications.

In [130… Also, for every Data Members we can create 2 Functions:

Getter ---> Retrieve value


&
Setter ---> Update value

Although we have hidden Pin and Balance,


But if needed, `get_pin()` & `set_pin()` to get and set pin.

In [145… sbi = Atm()

2104822263728

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

In [134… sbi.get_pin()

Out[134… '1234'

In [135… sbi.set_pin("235235")

Pin changed

In [136… sbi.get_pin()

Out[136… '235235'

In [137… # let's set a Rule ---> PIN must be a String

In [146… sbi.set_pin(5.6)

Pin Changed

In [147… sbi.get_pin()
Out[147… 5.6

In [150… Whats the purpose of `get` and `set` functions if,


`sbi.set_pin` & `sbi.pin` ---> Same functionality

Purpose ---> Hide Data & Control Access (Encapsulation)

controlled access (sbi.get_pin/sbi.set_pin) vs. direct access (sbi.pin)

Input values are processed through functions this allows logical control over data

In [151… sbi = Atm()

2104815840384

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

In [152… sbi.set_pin(5.6)

Not allowed

In [153… sbi.set_pin("2345") # Encapsulation

Pin changed

In [155… Summary:

The whole idea is to prevent direct public access to data to avoid breaches.

Solution:
Hide data using `__`.

if anyone wants access, must follow 2 methods:


1. `get` ---> Fetch value.
2. `set` ---> Modify value under set conditions.

Protecting our Data, Access Data via Functions, Set Data via Logic.

Concept ---> Encapsulation i.e. Protect and Access data through controlled methods

Use getter and setter functions.

Data is protected and accessible through controlled means.

In [156… Class diagram:


+-------------------------------------+
| - Atm |
|-------------------------------------|
| - pin |
| - balance |
|-------------------------------------|
| - __init__() |
| - menu |
| + change_pin() |
| + deposit |
| + withdraw |
| + check_balance() |
+-------------------------------------+

Reference Variable
In [157… Atm()

2104822426656

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully
Out[157… <__main__.Atm at 0x1ea11316c20>

In [162… Object created but unusable.

Reason ---> Not stored in any variable.

Result ---> Object lost, memory inaccessible.

In [163… # whenever creating object, write this code:

sbi = Atm()

2104851242224

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully
In [165… # Reference vs. Object:

reference ---> `sbi`


object ---> `Atm()`

`sbi` ≠ object
`sbi` is a reference variable
`sbi` points to the memory address of the actual object `Atm()`

Pass by Reference
In [194… class Customer:
def __init__(self, name):
self.name = name

cust = Customer("Nitish")
print(cust.name)

Nitish

In [196… class Customer:


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

def greet(customer):
print("Hello", customer.name)

cust = Customer("Nitish")
greet(cust)

# Passed class object 'cust' to 'greet()'


# Function receives object, accesses its attributes

Hello Nitish

In [197… class Customer:


def __init__(self, name, gender):
self.name = name
self.gender = gender

def greet(customer):
if customer.gender == "Male":
print("Hello", customer.name, "sir")
else:
print("Hello", customer.name, "ma'am")

cust = Customer("Nitish", "Male")


greet(cust)

Hello Nitish sir

In Python, all data types(e.g., int, str, list, dict) are object.
Custom class instances = objects too.

Everything in Python is an object and behaves uniformly.

In [199… class Customer:


def __init__(self, name, gender):
self.name = name
self.gender = gender

def greet(customer):
if customer.gender == "Male":
print("Hello", customer.name, "sir")
else:
print("Hello", customer.name, "ma'am")

cust2 = Customer("Nitish", "Male")


return cust2

cust = Customer("Ankita", "Female")


new_cust = greet(cust)
print(new_cust.name)

Hello Ankita ma'am


Nitish

learned 2 concepts
1. Class Object as Argument: Pass own class objects as function
arguments.

2. Class Object as Return: Functions can return class objects.

Custom class objects = int, str, dict, list in behavior.

In [213… # Pass by Reference:

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

def greet(customer):
pass

cust = Customer("Ankita")
print(id(cust))

2104851621072

In [220… class Customer:


def __init__(self, name):
self.name = name
def greet(customer):
print(id(customer))

cust = Customer("Ankita")
print(id(cust))
greet(cust)

2104856055520
2104856055520

In [221… # Aliasing:

a = 3
b = a

In [222… id(a)

Out[222… 2104739692848

In [223… id(b)

Out[223… 2104739692848

In [224… (customer)----->(Object)<--------(cust)
| |
| |
| |
+---------------(greet)---------------+

In [225… class Customer:


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

def greet(customer):
customer.name = "Nitish"
print(customer.name)

cust = Customer("Ankita")
greet(cust)

Nitish

In [226… class Customer:


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

def greet(customer):
customer.name = "Nitish"
print(customer.name)

cust = Customer("Saurabh")
greet(cust)
print(cust.name)
Nitish
Nitish

In [ ]: # Object Mutation in Functions:

Passing obj to func ---> func modifies obj ---> Original obj reflects changes.

In [228… class Customer:


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

def greet(customer):
print(id(customer))
customer.name = "Nitish"
print(customer.name)
print(id(customer))

cust = Customer("Saurabh")
print(id(cust))
greet(cust)
print(cust.name)

2104856063344
2104856063344
Nitish
2104856063344
Nitish

class objects are mutable like lists, dict, sets.

In [230… def change(L):


print(id(L))
L.append(5)
print(id(L))

L1 = [1, 2, 3, 4]
print(id(L1))
print(L1)

change(L1)

print(L1)

2104871192768
[1, 2, 3, 4]
2104871192768
2104871192768
[1, 2, 3, 4, 5]

In [231… def change(L):


print(id(L))
L.append(5)
print(id(L))
L1 = [1, 2, 3, 4]
print(id(L1))
print(L1)

change(L1[:])

print(L1)

2104855834176
[1, 2, 3, 4]
2104820330304
2104820330304
[1, 2, 3, 4]

Function Call - List Passing


Avoid passing original list; inter operations may alter the original list.

Use "cloning" to prevent external changes.

In [233… def change(L):


print(id(L))
L = L + (5, 6)
print(id(L))

L1 = (1, 2, 3, 4)
print(id(L1))
print(L1)

change(L1)

print(L1)

2104855459008
(1, 2, 3, 4)
2104855459008
2104820526208
(1, 2, 3, 4)

In [235… Conclusion ---> In Pass by Reference, changes to mutable data types (objects) affect
But in Immutable Types, No effect on the original (int, str) when

Collection of Objects
In [236… class Customer:
def __init__(self, name, age):
self.name = name
self.age = age

c1 = Customer("Nitish", 34)
c2 = Customer("Ankit", 45)
c3 = Customer("Neha", 32)
L = [c1, c2, c3]

for i in L:
print(i)

<__main__.Customer object at 0x000001EA138E42E0>


<__main__.Customer object at 0x000001EA138E4B50>
<__main__.Customer object at 0x000001EA138E42B0>

In [239… for i in L:
print(i.name, i.age)

Nitish 34
Ankit 45
Neha 32

In [240… class Customer:


def __init__(self, name, age):
self.name = name
self.age = age

def intro(self):
print("I am", self.name, "and I am", self.age)
c1 = Customer("Nitish", 34)
c2 = Customer("Ankit", 45)
c3 = Customer("Neha", 32)

L = [c1, c2, c3]

for i in L:
i.intro()

I am Nitish and I am 34
I am Ankit and I am 45
I am Neha and I am 32

In [1]: # Looping & Objects:

Loop + List ---> treat list as object; compatible with looping.


Dict/Tuple ---> compatible with loops, same approach as lists in loops.
Sets ---> immutable data types only; incompatible with mutable objects in

***Object Collection:** Lists, tuples, dicts can store custom class objects*

Static Variables and Methods


In [2]: # Back to ATM Code Enhancement
# Like adding unique serial number for each user.

In [4]: c1 = Atm()
1560691660048

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 1234
Pin set successfully

In [5]: c2 = Atm()

1560691670224

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 2345
Pin set successfully

In [6]: c3 = Atm()

1560691668832

Hello, how would you like to proceed?


1. Enter 1 to create pin
2. Enter 2 to deposit
3. Enter 3 to withdraw
4. Enter 4 to check balance
5. Enter 5 to exit
1
Enter your pin: 3456
Pin set successfully

In [7]: c1.sno

Out[7]: 1

In [8]: c2.sno

Out[8]: 1

In [9]: c3.sno

Out[9]: 1

In [10]: Code Issue: Constructor Reinitialization


Problem: `self.sno` resets on each object creation.

`self.sno = 0` ---> Re-initializes on each object creation.

`self.sno += 1` ---> No persistent increment, always starts at 0.

Variable Types
1. Instance Variable: Unique per object (e.g., pin, balance, GPA).

2. Static/Class Variable: Same across objects (e.g., IFSC code, Degree no.).

In [14]: # Now for this ATM Code we will create a Static Variable

Note ---> Static Variable is Defined outside constructor.


Instance Variable is Defined inside constructor.

In [27]: To access instance var ---> `self.var`

To access class/static var ---> `Class.var`

`self.sno` = `Atm.counter`

`Atm.counter` += `1`

In [39]: c1 = Atm()

1560691776512

In [40]: c2 = Atm()

1560691780544

In [41]: c3 = Atm()

1560691770128

In [42]: c1.sno

Out[42]: 1

In [43]: c2.sno

Out[43]: 2

In [44]: c3.sno

Out[44]: 3

In [45]: c3.counter # counter value in memory = 4

Out[45]: 4
In [46]: c2.counter

Out[46]: 4

In [47]: c1.counter

Out[47]: 4

In [48]: Atm.counter

Out[48]: 4

Static Variables Need


• Shared data (e.g., bank/college name)
• Object-wide counters
• Use static variables for shared/static data

In [52]: Atm.counter

Out[52]: 1

In [53]: Atm.counter = 'wrgwrg'

In [54]: c1 = Atm()

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[54], line 1
----> 1 c1 = Atm()

Cell In[51], line 15, in Atm.__init__(self)


12 self.__balance = 0
14 self.sno = Atm.counter # instance variable
---> 15 Atm.counter = Atm.counter + 1
17 print(id(self))

TypeError: can only concatenate str (not "int") to str

In [73]: # Solution ---> Add `__counter` = 1 in Atm class.

def __init__(self):
self.__pin = ""
self.__balance = 0
self.sno = Atm.__counter
Atm.__counter += 1

# Changes now immutable.


# But, need to update counter functionality.
def get_counter(self):
return Atm.__counter

def set_counter(self, new):


if isinstance(new, int):
Atm.__counter = new
else:
print('Not Allowed')

In [56]: Atm.get_counter()

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[56], line 1
----> 1 Atm.get_counter()

TypeError: Atm.get_counter() missing 1 required positional argument: 'self'

In [74]: # Static Methods:

In `get_counter()`; no `self` needed; uses class var.

Static methods don’t take self; use @staticmethod decorator.

`@staticmethod` ---> Methods that doesn’t require object instance for access.

@staticmethod
def get_counter():
return Atm.__counter

@staticmethod
def set_counter(new):
if type(new) == int:
Atm.counter = new
else:
print('Not Allowed')

In [64]: Atm.get_counter()

Out[64]: 1

In [69]: Atm.set_counter(5)

In [70]: Atm.get_counter()

Out[70]: 5

@staticmethods ---> Accessed w/o objects; Used for static vars.


Class Relationship - Aggregation
In [76]: So far, used only Single Class.
But in Real-World Scenario i.e.
Large Apps ---> involves multiple classes ---> involves complex inter-class relationsh

Relationships
1. Aggregation (Has-A)
2. Inheritance (Is-A)

In [79]: # Aggregation Ex:

Customer ---> Has-A ---> Address

# Inheritance Ex:

Smartphone ---> Is-A ---> Product

Car ---> Is-A ---> Vehicle

In [ ]: Class Diagram - Aggregation:

+--------------------+ 1 +-------------------+
| Department |<------------------->| Employee |
+--------------------+ +-------------------+
| - name: str | | - name: str |
| - location: str | | - employeeID: int |
+--------------------+ +-------------------+
| + addEmployee() | | + getDetails() |
| + removeEmployee() | | + updateDetails() |
+--------------------+ +-------------------+
1..* 1..*

In [83]: class Customer:


def __init__(self, name, gender, address):
self.name = name
self.gender = gender
self.address = address

class Address:
def __init__(self, city, pincode, state):
self.city = city
self.pincode = pincode
self.state = state

add = Address('Kolkata', 700156, 'WB')


cust = Customer('Nitish', 'Male', add)

print(cust.address)
<__main__.Address object at 0x0000016B60C8A950>

In [84]: print(cust.address.city)

Kolkata

In [85]: print(cust.address.pincode)

700156

When creating an object, if another object is passed, the new object behaves like
the passed one.

In [90]: class Customer:


def __init__(self, name, gender, address):
self.name = name
self.gender = gender
self.address = address

def edit_profile(self, new_name, new_city, new_pin, new_state):


self.name = new_name
self.address.change_address(new_city, new_pin, new_state)

class Address:
def __init__(self, city, pincode, state):
self.city = city
self.pincode = pincode
self.state = state

def change_address(self, new_city, new_pin, new_state):


self.cityy = new_city
self.pincode = new_pin
self.state = new_state

add = Address('Kolkata', 700156, 'WB')


cust = Customer('Nitish', 'Male', add)
cust.edit_profile('Ankit', 'Gurgaon', 122011, 'haryana')
print(cust.address.pincode)

# `Customer` class non-functional


# Relies on `Address` class for operations

122011

INHERITANCE
Real-world concept; means to inherit.

In [93]: OOP ---> Real-world problem-solving.


Aligns with real-world concepts
`Inheritance Benefits
DRY Principle: "Don't Repeat Yourself"

Avoid redundant code; Reuse existing code via inheritance.

Inheritance Code Reusability


Saves Time; Concise, Optimized Code; Effective.

In [97]: # Note: Inheritence Direction ---> Upward

Ex ---> `Student` inherits from `User` (e.g., login, registration).


`User` cannot inherit from `Student`.

i.e Child inherits properties/data from Parent.

In [98]: In Inheritence,

Child Inherits ---> Data Members, Methods, Constructor


Doesn't Inherit ---> Private Members

In [100… class User: # Parent

def login(self):
print('login')

def register(self):
print('Register')

class Student(User): # Child

def enroll(self):
print('Enroll')

def review(self):
print('Review')

stu1 = Student()

stu1.enroll()
stu1.review()
stu1.login()
stu1.register()

Enroll
Review
login
Register

In [103… # The reverse is not true


# i.e., parent can't access child

class User: # Parent

def login(self):
print('login')

def register(self):
print('Register')

class Student(User): # Child

def enroll(self):
print('Enroll')

def review(self):
print('Review')

u = User()

u.enroll()
u.review()
u.login()
u.register()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[103], line 22
18 print('Review')
20 u = User()
---> 22 u.enroll()
23 u.review()
24 u.login()

AttributeError: 'User' object has no attribute 'enroll'

In [ ]: Class Diagram - Inheritence:

+-------------+
| Animal |
+-------------+
| - name: str |
| - age: int |
+-------------+
| + eat() |
| + sleep() |
+-------------+
/ | \
/ | \
/ | \
+----------+ +----------+ +---------+
| Dog | | Cat | | Hamster |
+----------+ +----------+ +---------+
| - breed | | - color | | - size |
+----------+ +----------+ +---------+
| + bark() | | + meow() | | + run() |
+----------+ +----------+ +---------+

Ex 1 - Inheriting Constructer
In [108… # Constructor Ex:

class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.price = price
self.brand = brand
self.camera = camera

class SmartPhone(Phone):
pass

s = SmartPhone(20000, "Apple", 13)

Inside phone constructor

In [109… print(s.brand)

Apple

In [110… `class B` inherits `class A`

No constructor in `B` ---> `A`'s constructor invoked

Parent class constructor used if child lacks one

Ex 2 - Inheiting Private Members


In [114… class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.price = price
self.__brand = brand
self.camera = camera

class SmartPhone(Phone):
pass

s = SmartPhone(20000, "Apple", 13)


print(s.__brand)

Inside phone constructor


---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[114], line 12
9 pass
11 s=SmartPhone(20000, "Apple", 13)
---> 12 print(s.__brand)

AttributeError: 'SmartPhone' object has no attribute '__brand'

Hidden parent members not accessible by child class.

POLYMORPHISM

Ex 3 - Polymorphism
In [117… # Method Overriding:

class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera

def buy(self):
print("Buying a phone")

class SmartPhone(Phone):
def buy(self):
print("Buying a smartphone")

s = SmartPhone(20000, "Apple", 13)

s.buy()

Inside phone constructor


Buying a smartphone

Method Overriding
Child Class method overrides Parent Class method if they have the same name.

Child Class method is called in case of name conflict.

Parent Class method is overridden by Child Class method.

Polymorphism
• Method Overriding
• Method Overloading:
• Operator Overloading

Ex - Class Parent
In [124… class Parent:
def __init__(self, num):
self.__num = num
def get_num(self):
return self.__num

class Child(Parent):
def show(self):
print("This is in child class")

son = Child(100,)
print(son.get_num())
son.show()

100
This is in child class

Ex - 2
In [126… class Parent:
def __init__(self, num):
self.__num = num
def get_num(self):
return self.__num

class Child(Parent):
def __init__(self, val, num):
self.__val = val
def get_val(self):
return self.__val

son = Child(100, 10)


print("Parent: Num:", son.get_num())
print("Child: Val:", son.get_val())
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[126], line 18
15 return self.__val
17 son = Child(100, 10)
---> 18 print("Parent: Num:", son.get_num())
19 print("Child: Val:", son.get_val())

Cell In[126], line 7, in Parent.get_num(self)


6 def get_num(self):
----> 7 return self.__num

AttributeError: 'Child' object has no attribute '_Parent__num'

In [127… No Child Constructor ---> Parent Constructor invoked automatically.

Child Constructor present ---> Parent Constructor not called.

Ex - 3
In [129… class A:
def __init__(self):
self.var1 = 100
def display1(self, var1):
print("class A :", self.var1)

class B(A):
def display2(self, var1):
print("class B :", self.var1)

obj = B()
obj.display1(200)

class A : 100

User of super()

Ex - Super
In [131… class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")

class SmartPhone(Phone):
def buy(self):
print ("Buying a smartphone")
super().buy() # Call parent buy()

s = SmartPhone(20000, "Apple", 13)


s.buy()

Inside phone constructor


Buying a smartphone
Buying a phone

In [132… # `super` keyword is invalid outside class.

s.super().buy()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[132], line 3
1 # super keyword doesn't work outside the class
----> 3 s.super().buy()

AttributeError: 'SmartPhone' object has no attribute 'super'

super keyword
1. Accesses parent class methods
2. Accesses parent class constructor

Does NOT access parent class attributes

Ex - Super with Constructor


In [139… class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera

class SmartPhone(Phone):
def __init__(self, price, brand, camera, os, ram):
print('Inside smartphone constructor')
super().__init__(price, brand, camera)
self.os = os
self.ram = ram
print ("Inside smartphone constructor")

s = SmartPhone(20000, "Samsung", 12, "Android", 2)


print(s.os)
print(s.brand)
Inside smartphone constructor
Inside phone constructor
Inside smartphone constructor
Android
Samsung

In [140… # Task Delegation in Inheritance:

Child class handles its tasks.

Reuses parent class tasks via `super` in constructor.

Initialization ---> Half by child, rest via parent with `super`.

Ex - Super
In [143… class Parent:
def __init__(self, num):
self.__num = num
def get_num(self):
return self.__num

class Child(Parent):
def __init__(self, num, val):
super().__init__(num)
self.__val = val
def get_val(self):
return self.__val

son = Child(100, 200)


print(son.get_num())
print(son.get_val())

100
200

In [144… class Parent:


def __init__(self):
self.num = 100

class Child(Parent):
def __init__(self):
super().__init__()
self.var = 200
def show(self):
print(self.num)
print(self.var)

son = Child()
son.show()

100
200
Access parent attribute in child class via self .

self = current object ( son ).

If accessible externally, accessible internally too.

In [146… class Parent:


def __init__(self):
self.__num = 100
def show(self):
print("Parent:", self.__num)

class Child(Parent):
def __init__(self):
super().__init__()
self.__var = 10
def show(self):
print("Child:", self.__var)

dad = Parent()
dad.show()
son = Child()
son.show()

Parent: 100
Child: 10

In [ ]: # Inheritance Summary:

Class Inheritance allows one class to inherit from another; enhances code reuse

Inherited Members ---> Constructors, Attributes, Methods.

Parent can’t access Child.

Private members are not directly accessible in Child.

Child can override parent’s methods/attributes.

`super()` ---> Calls parent’s methods/constructor.

Types of inheritance
1. Single Inheritance
2. Multilevel Inheritance
3. Hierarchical Inheritance
4. Multiple Inheritance (Diamond Problem)
5. Hybrid Inheritance

In [ ]: Single Inheritance Multilevel Inheritance Hierarchical Inherita


+-------------+ +-------------+ +-------------+
| Animal | | Animal | | Animal
+-------------+ +-------------+ +-------------+
| - name: str | | - name: str | | - name: str
| - age: int | | - age: int | | - age: int
+-------------+ +-------------+ +-------------+
| + eat() | | + eat() | | + eat()
| + sleep() | | + sleep() | | + sleep()
+-------------+ +-------------+ +-------------+
| | / |
| | / |
| | / |
+--------------+ +--------------+ +----------+ +----------+
| Dog | | Pet | | Dog | | Cat |
+--------------+ +--------------+ +----------+ +----------+
| - breed: str | | - owner: str | | - breed | | - color |
+--------------+ +--------------+ +----------+ +----------+
| + bark() | | + play() | | + bark() | | + meow() |
+--------------+ +--------------+ +----------+ +----------+
|
|
|
+--------------+
| Dog |
+--------------+
| - breed: str |
+--------------+
| + bark() |
+--------------+

In [ ]: Multiple Inheritance (Diamond Problem)

+------------------+
| Animal |
+------------------+
| - name: str |
| - age: int |
+------------------+
| + eat() |
| + sleep() |
+------------------+
/ \
/ \
/ \
+----------+ +----------+
| Dog | | Cat |
+----------+ +----------+
| - breed | | - color |
+----------+ +----------+
| + bark() | | + meow() |
+----------+ +----------+
\ /
\ /
\ /
+------------------+
| PetOwner |
+------------------+
| - ownerName: str |
+------------------+
| + feed() |
| + play() |
+------------------+

Hybrid Inheritance

+------------------+
| Animal |
+------------------+
| - name: str |
| - age: int |
+------------------+
| + eat() |
| + sleep() |
+------------------+
/ \
/ \
/ \
+-------------+ +----------------+
| Dog | | Cat |
+-------------+ +----------------+
| - breed | | - color |
+-------------+ +----------------+
| + bark() | | + meow() |
+-------------+ +----------------+
/ \ / \
/ \ / \
/ \ / \
+-------------+ +-------------+ +----------------+ +----------------+
| Labrador | | Bulldog | | Siamese | | Persian
+-------------+ +-------------+ +----------------+ +----------------+
| - size: str | | - size: str | | - pattern: str | | - furType: str
+-------------+ +-------------+ +----------------+ +----------------+
| + fetch() | | + guard() | | + purr() | | + groom()
+-------------+ +-------------+ +----------------+ +----------------+
\ /
\ /
\ /
+--------------------+
| PetOwner |
+--------------------+
| - ownerName: str |
+--------------------+
| + feed() |
| + play() |
+--------------------+
In [ ]: # Multiple Inheritance:

Java ---> Not Supported

Python ---> Supported

Ex - Single level Inheritence


In [4]: class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")
def return_phone(self):
print("Returning a phone")

class SmartPhone(Phone):
pass

SmartPhone(1000, "Apple", "13px").buy()

Inside phone constructor


Buying a phone

Ex - Multilevel level Inheritence


In [6]: class Product:
def review(self):
print ("Product customer review")

class Phone(Product):
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")

class SmartPhone(Phone):
pass

s = SmartPhone(20000, "Apple", 12)


p = Phone(1000, "Samsung", 1)

s.buy()
s.review()
p.review()
Inside phone constructor
Inside phone constructor
Buying a phone
Product customer review
Product customer review

Ex - Hierarchical Inheritence
In [8]: class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")
def return_phone(self):
print("Returning a phone")

class SmartPhone(Phone):
pass

class FeaturePhone(Phone):
pass

SmartPhone(1000, "Apple", "13px").buy()

Inside phone constructor


Buying a phone

Ex - Multiple Inheritence
In [10]: class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")

class Product:
def review(self):
print ("Customer review")

class SmartPhone(Phone, Product):


pass

s = SmartPhone(20000, "Apple", 12)

s.buy()
s.review()
Inside phone constructor
Buying a phone
Customer review

MRO - Method Resolution Order


In [13]: class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")

class Product:
def buy(self):
print ("Product buy method")

# MRO: Product ---> Phone


class SmartPhone(Product, Phone):
pass

s = SmartPhone(20000, "Apple", 12)


s.buy()

Inside phone constructor


Product buy method

In [14]: MRO ---> Order of inheritance determines priority.


In conflict, methods from first-inherited classes execute.

Ex - Multilevel Inheritence
In [16]: class A:
def m1(self):
return 20

class B(A):
def m1(self):
return 30
def m2(self):
return 40

class C(B):
def m2(self):
return 20

obj1 = A()
obj2 = B()
obj3 = C()
print(obj1.m1() + obj3.m1() + obj3.m2())
70

In [2]: class A:
def m1(self):
return 20

class B(A):
def m1(self):
val = super().m1() + 30
return val

class C(B):
def m1(self):
val = self.m1() + 20
return val

obj = C()
print(obj.m1())

# This code will causes infinite recursion

Method Overloading and Operator Overloading

Polymorphism
1. Method Overriding
2. Method Overloading
3. Operator Overloading

In [11]: "Method Overloading" ---> Single method, Multiple implementations.


different inputs = different behaviors.

In [6]: # Ex - Code for Area Calculation:

Different shapes have unique area formulas.

Thus, uniform function name ---> `area()` for clarity.

Different Inputs ---> Different Outputs.

In [7]: class Geometry:


def area(self, radius):
return 3.24 * radius * radius
def area(self, l, b):
return l * b

obj = Geometry()
print(obj.area(4))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[7], line 10
7 return l * b
9 obj = Geometry()
---> 10 print(obj.area(4))

TypeError: Geometry.area() missing 1 required positional argument: 'b'

In [8]: Method Overloading in Java ---> Multiple methods, same name, different inputs
in Python ---> No true traditional method overloading.
Same method name = last definition overrides

Multiple methods with same name not allowed.

Workaround:

In [9]: # Use default params or var args for simulated overloading.

class Geometry:
def area(self, a, b = 0):
if b == 0:
print('Circle', 3.14 * a * a)
else:
print('Rectangle', a * b)

obj = Geometry()
obj.area(4)
obj.area(4, 5)

Circle 50.24
Rectangle 20

Note: Python's workaround with default arguments simulates overloading.

In [12]: "Operator Overloading" ---> Customize default operator behavior.


(e.g., `+` for addition)

Define behavior for operators beyond built-in.


(e.g., custom class logic)

In [14]: 'hello' + 'world'

# Not mathematical addition; string concatenation instead.


# String class designer redefined `+` operator for concatenation.

Out[14]: 'helloworld'

In [15]: x = Fraction(3, 4)
y = Fraction(5, 6)

print(x + y)
# Operator Overloading:

# Custom logic for operators (e.g., `+`).


# Fraction addition, where `+` is redefined.
# Allow custom behavior for standard operators in classes.

38/24

In [21]: 4 + 5

Out[21]: 9

In [22]: [1, 2, 3] + [4, 5]

Out[22]: [1, 2, 3, 4, 5]

ABSTRACTION
Hides implementation details; focuses on functionality.

In [1]: Ex: Laptop ---> details (how it's made) hidden.


user interface is exposed.

Ex: Electromagnetic waves ---> invisible but enable internet, calls.

Present in both nature and software.

Bank Example Hierarchy


In [2]: # Ex - Abstraction & Inheritance in Bank Application Design:

+----------+
| Bank App | (Top-level)
+----------+
/ \
/ \
+---------+ +------------+
| Web App | | Mobile App | (Children)
+---------+ +------------+

Bank App ---> Database


Web App <--- Bank App ---> Mobile App

Children (Web & Mobile) must inherit Bank App and include security functions.
Ensures secure access to database-related functions.

Senior dev imposes constraints (abstraction) on subclasses (Web & Mobile) for consiste
Abstract Class
Abstract Class Contains at least 1 Abstract Method.

Abstract Method No implementation/code.

2 Method Types:

1. Abstract: No code.
2. Concrete: Contains code.

Coding Abstract Class Ex - BankApp Class


In [6]: from abc import ABC, abstractmethod

class BankApp(ABC):
def database(self):
print('connected to database')

@abstractmethod # decorator
def security(self):
pass

2 Essentials for making Abstract Class


1. Inherit from ABC class.
2. Must have at least one abstract method, marked with
@abstractmethod .

***Note:** Abstract methods contain no code.*

In [7]: class MobileApp(BankApp):


def mobile_login(self):
print('login into mobile')

In [8]: mob = MobileApp()

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 mob = MobileApp()

TypeError: Can't instantiate abstract class MobileApp with abstract method secu
rity

Bank app inheritance requires implementing abstract methods from the bank app.

In [11]: class MobileApp(BankApp):


def mobile_login(self):
print('login into mobile')
def security (self):
print('mobile security')

In [12]: mob = MobileApp()

In [13]: mob.database()

connected to database

In [14]: mob.mobile_login()

login into mobile

In [15]: mob.security()

mobile security

In [16]: Abstraction ---> Top-level classes control lower-level class actions.

Ensures consistency in behavior across a large app.

Hide implementation details; enforce behavior via abstract methods.

Senior devs/top-level designers enforce abstract methods (clauses) for child classes

In [22]: from abc import ABC, abstractmethod

class BankApp(ABC):
def database(self):
print('connected to database')

@abstractmethod
def security(self):
pass

@abstractmethod
def display(self):
pass

In [23]: class MobileApp(BankApp):


def mobile_login(self):
print('login into mobile')
def security(self):
print('mobile security')

In [24]: mob = MobileApp()


---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[24], line 1
----> 1 mob = MobileApp()

TypeError: Can't instantiate abstract class MobileApp with abstract method disp
lay

In [25]: class MobileApp(BankApp):


def mobile_login(self):
print('login into mobile')
def security(self):
print('mobile security')
def display(self):
print('display')

In [26]: mob = MobileApp()

so basically a mechanism for applying constraints.

In [28]: obj = BankApp()

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[28], line 1
----> 1 obj = BankApp()

TypeError: Can't instantiate abstract class BankApp with abstract methods displ
ay, security

Abstract Class Instantiation is Not Allowed


What is Threading?
In [ ]: +-------------------------------------+ +----------------------------------+
| Single-Threaded Process | | Multi-Threaded Process
|-------------------------------------| |----------------------------------|
| +------------+ | | +------------+ +------------+
| | Thread 1 | | | | Thread 1 | | Thread 2
| +------------+ | | +------------+ +------------+
| | Task 1 | | | | Task A | | Task C
| | Task 2 | | | | Task B | | Task D
| | Task 3 | | | +------------+ +------------+
| +------------+ | | +------------+ +------------+
+-------------------------------------+ | | Thread 3 | | Thread 4
| +------------+ +------------+
| | Task E | | Task G
| | Task F | | Task H
| +------------+ +------------+
+----------------------------------+

Single Thread: Default; main thread executes code sequentially.

Multithreading: Needed for concurrent/simultaneous code execution.

What is a thread?

• Threads are basic CPU units.


• Single process contains multiple threads.
• Threads share code, data, files.
• Each thread has its own registers, separate stack.

Why or When Threading?


App Screen Persistence:

Static Image Display:

# Main Thread
while(True):
displayScreen()
Shows fleeting images; appears like the app is running but isn't.

Heavy Operation + Display:

# Main Thread
while(True):
# Heavy Operation
displayScreen()
Results in flicker; heavy op delays screen display.

Network Call + Display:

# Main Thread
while(True):
Image = request(ImageUrl)
displayScreen()
Causes screen to pop up based on network delay.

Solution: Multithreading

# Main Thread
Image = None

def startAnotherThread():
while(True):
Image = request(ImageUrl)

while(True):
displayScreen(Image)
startAnotherThread() fetches images concurrently; displayScreen() shows
available images.

In [3]: # another example ---> multithreading in servers is used to handle multiple requests.
# mechanism involves diff threads for each request

How threads are handled by OS?


Threads Running Concurrently a "False Statement". Threads are not truly
running concurrently.

Reality: Limited by single-core CPUs; Concurrent ≠ Parallel

In [ ]: Web Server Thread


+-----------------------------------------+ ^ +--------------------
| +-------------------------+ | | t3 ----> | Running | Sleeping
| | Request Handler | | | +--------------------
| +-------------------------+ | | +--------------------
| +---------+ +---------+ +---------+ | | t2 ----> | Sleeping | Running
| | Client1 | | Client2 | | Client3 | | | +--------------------
| +---------+ +---------+ +---------+ | | +--------------------
+-----------------------------------------+ | t1 ----> | Running | Running
| +--------------------
+--------------------------------

3 threads.
CPU switches between them.

Only 1 thread executes at a time.

Effect: Concurrency.

Implementation
In [6]: from time import sleep, time
import threading

start = time()

def task(id):
print(f"Sleeping...{id}")
sleep(1)
print(f"Woke up...{id}")

threads = [threading.Thread(target=task, args=[i]) for i in range(10)]


for t in threads:
t.start()

for t in threads:
t.join()

end = time()
print(f"Main Thread Duration: {end - start} sec")

Sleeping...0
Sleeping...1
Sleeping...2
Sleeping...3
Sleeping...4
Sleeping...5
Sleeping...6
Sleeping...7
Sleeping...8
Sleeping...9
Woke up...8Woke up...7
Woke up...5
Woke up...4
Woke up...2
Woke up...1
Woke up...9
Woke up...3
Woke up...6
Woke up...0

Main Thread Duration: 1.0146267414093018 sec


Thread Synchronization
In [10]: import threading

balance = 200
lock = threading.Lock()

def deposit(amount, times, lock):


global balance
for _ in range(times):
lock.acquire()
balance += amount
lock.release()

def withdraw(amount, times, lock):


global balance
for _ in range(times):
lock.acquire()
balance -= amount
lock.release()

deposit_thread = threading.Thread(target=deposit, args=[1, 100000, lock])


withdraw_thread = threading.Thread(target=withdraw, args=[1, 100000, lock])

deposit_thread.start()
withdraw_thread.start()
deposit_thread.join()
withdraw_thread.join()

print(balance)

200
What is an Iteration?
Sequentially processing each item in a collection.

Mechanism utilizes loops (explicit/implicit) to traverse data sequences.

In [1]: num = [1, 2, 3]

for i in num: # `for` loops iterate over data sequences, executing operations.
print(i)

1
2
3

What is Iterator?
Object for sequential data traversal without storing entire dataset in memory.

Function internally fetches items one-by-one during loop execution.

Key Feature: Retrieves data incrementally, minimizing memory usage by loading


one item at a time.

In [13]: L = [x for x in range(1, 100)]

for i in L:
print(i*2, end = ',')

2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,5
6,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,10
6,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,1
46,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,18
4,186,188,190,192,194,196,198,

In [14]: # Memory Comparison

import sys

print(sys.getsizeof(L)/1024) # List size in KB

0.8984375

In [15]: x = range(1, 100)

for i in x:
print(i*2, end = ',')
2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,5
6,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,10
6,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,1
46,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,18
4,186,188,190,192,194,196,198,

In [16]: print(sys.getsizeof(x)/1024) # Iterator size in KB

0.046875

Iterator traverses data sequence without full memory load. Processes one item at a
time, discards, and loads next. Efficient looping/traversing.

What is Iterable?
Object that can be iterated over (looped through).

Examples:

• L = [x for x in range(1,100)] (list is iterable)


• x = range(1,100) (range is iterable)

Generates an iterator via iter() method.

In [4]: L = [1, 2, 3]
type(L)

# L is an iterable

Out[4]: list

In [5]: iter(L)

Out[5]: <list_iterator at 0x20956534520>

In [6]: type(iter(L))

# iter(L) ---> iterator

Out[6]: list_iterator

In [ ]: Iteration ---> Traversing items in an object.

Iterable ---> Object that can be iterated.

Iterator ---> Object facilitating iteration.


Key Points
Iterator = Iterable (loopable).

Iterable ≠ Iterator (e.g., L = [x for x in range(1,100)] is iterable but not


an iterator; it stores data in memory).

In [ ]: # Trick

Iterable ---> `iter()`

Iterator ---> `iter()` + `next()`

In [1]: a = 2
a

Out[1]: 2

In [2]: for i in a:
print(i)

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[2], line 1
----> 1 for i in a:
2 print(i)

TypeError: 'int' object is not iterable

In [3]: dir(a)
Out[3]: ['__abs__',
'__add__',
'__and__',
'__bool__',
'__ceil__',
'__class__',
'__delattr__',
'__dir__',
'__divmod__',
'__doc__',
'__eq__',
'__float__',
'__floor__',
'__floordiv__',
'__format__',
'__ge__',
'__getattribute__',
'__getnewargs__',
'__gt__',
'__hash__',
'__index__',
'__init__',
'__init_subclass__',
'__int__',
'__invert__',
'__le__',
'__lshift__',
'__lt__',
'__mod__',
'__mul__',
'__ne__',
'__neg__',
'__new__',
'__or__',
'__pos__',
'__pow__',
'__radd__',
'__rand__',
'__rdivmod__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__rfloordiv__',
'__rlshift__',
'__rmod__',
'__rmul__',
'__ror__',
'__round__',
'__rpow__',
'__rrshift__',
'__rshift__',
'__rsub__',
'__rtruediv__',
'__rxor__',
'__setattr__',
'__sizeof__',
'__str__',
'__sub__',
'__subclasshook__',
'__truediv__',
'__trunc__',
'__xor__',
'as_integer_ratio',
'bit_count',
'bit_length',
'conjugate',
'denominator',
'from_bytes',
'imag',
'numerator',
'real',
'to_bytes']

In [4]: T = (1, 2, 3)

dir(T)
Out[4]: ['__add__',
'__class__',
'__class_getitem__',
'__contains__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__getnewargs__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__mul__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__rmul__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'count',
'index']

In [5]: T = {1, 2, 3}

dir(T)
Out[5]: ['__and__',
'__class__',
'__class_getitem__',
'__contains__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__iand__',
'__init__',
'__init_subclass__',
'__ior__',
'__isub__',
'__iter__',
'__ixor__',
'__le__',
'__len__',
'__lt__',
'__ne__',
'__new__',
'__or__',
'__rand__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__ror__',
'__rsub__',
'__rxor__',
'__setattr__',
'__sizeof__',
'__str__',
'__sub__',
'__subclasshook__',
'__xor__',
'add',
'clear',
'copy',
'difference',
'difference_update',
'discard',
'intersection',
'intersection_update',
'isdisjoint',
'issubset',
'issuperset',
'pop',
'remove',
'symmetric_difference',
'symmetric_difference_update',
'union',
'update']

In [6]: T = {1:2, 3:4}

dir(T)

Out[6]: ['__class__',
'__class_getitem__',
'__contains__',
'__delattr__',
'__delitem__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__ior__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__ne__',
'__new__',
'__or__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__reversed__',
'__ror__',
'__setattr__',
'__setitem__',
'__sizeof__',
'__str__',
'__subclasshook__',
'clear',
'copy',
'fromkeys',
'get',
'items',
'keys',
'pop',
'popitem',
'setdefault',
'update',
'values']
Check Iterability/Iterator in Python:
1. Iterable:

If for loop works, it’s iterable.

If dir(obj) shows __iter__ , it's iterable.

2. Iterator:

If dir(obj) shows both __iter__ and __next__ , it's an iterator.

In [17]: L = [1, 2, 3]

# L is not an iterator
dir(L)
Out[17]: ['__add__',
'__class__',
'__class_getitem__',
'__contains__',
'__delattr__',
'__delitem__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__gt__',
'__hash__',
'__iadd__',
'__imul__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__mul__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__reversed__',
'__rmul__',
'__setattr__',
'__setitem__',
'__sizeof__',
'__str__',
'__subclasshook__',
'append',
'clear',
'copy',
'count',
'extend',
'index',
'insert',
'pop',
'remove',
'reverse',
'sort']

In [18]: iter(L)

Out[18]: <list_iterator at 0x2594da089d0>

In [20]: dir(iter(L))
Out[20]: ['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__length_hint__',
'__lt__',
'__ne__',
'__new__',
'__next__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__setstate__',
'__sizeof__',
'__str__',
'__subclasshook__']

In [21]: iter_L = iter(L)

# iter_L is an iterator

Understanding for Loop


In [22]: num = [1, 2, 3]

for i in num:
print(i)

1
2
3

In [23]: num = [1, 2, 3]

# Get iterator
iter_num = iter(num)

# Access elements
next(iter_num)
next(iter_num)
next(iter_num)
next(iter_num)

---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[23], line 10
8 next(iter_num)
9 next(iter_num)
---> 10 next(iter_num)

StopIteration:

Custom for Loop


In [24]: def mera_khudka_for_loop(iterable):
iterator = iter(iterable)
while True:
try:
print(next(iterator))
except StopIteration:
break

In [25]: a = [1, 2, 3]
b = range(1, 11)
c = (1, 2, 3)
d = {1, 2, 3}
e = {0:1, 1:1}

mera_khudka_for_loop(e)

0
1

A Confusing Point
In [26]: num = [1, 2, 3]
iter_obj = iter(num)
print(id(iter_obj), 'Address of iterator 1')

iter_obj2 = iter(iter_obj)
print(id(iter_obj2), 'Address of iterator 2')

2582554600704 Address of iterator 1


2582554600704 Address of iterator 2

iter on an iterator ---> returns the iterator itself (not new).

Essential for custom objects: to support iteration in loops, must implement


iteration logic.
Let's Create Custom range() Function
In [27]: class mera_range:

def __init__(self, start, end):


self.start = start
self.end = end

def __iter__(self):
return mera_range_iterator(self)

In [28]: class mera_range_iterator:

def __init__(self, iterable_obj):


self.iterable = iterable_obj

def __iter__(self):
return self

def __next__(self):

if self.iterable.start >= self.iterable.end:


raise StopIteration

current = self.iterable.start
self.iterable.start += 1
return current

In [29]: for i in mera_range(1, 11):


print(i)

1
2
3
4
5
6
7
8
9
10

In [30]: x = mera_range(1, 11)

In [31]: type(x)

Out[31]: __main__.mera_range

In [32]: iter(x)

Out[32]: <__main__.mera_range_iterator at 0x2594c3fd600>


Application of Iterators
Iterators allow sequential operation over data without full memory load. Key
feature: memory efficiency.

Example: For large image datasets in deep learning:

• Problem: Limited RAM; full data load impractical.


• Solution: Use iterators to handle data one item at a time.
• Process: Loads data into memory, processes, returns next item.

Benefit: Only one item in memory at a time, managing large datasets efficiently.

Keras uses Image data generators function like iterators, embodying the same
principle.
What is a Generator?
Simple way to create iterators.

In [1]: # Iterable
class mera_range:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return mera_iterator(self)

# Iterator
class mera_iterator:
def __init__(self, iterable_obj):
self.iterable = iterable_obj
def __iter__(self):
return self
def __next__(self):
if self.iterable.start >= self.iterable.end:
raise StopIteration
current = self.iterable.start
self.iterable.start += 1
return current

now this method is cluttered, outdated for iterators.

Solution: Generators.

in simple terms Generatos are a simplified way to create iterators.

The Why

Need for Iterators


In [5]: L = [x for x in range(100000)] # 100k elements

# for i in L:
# print(i**2)

import sys
sys.getsizeof(L) # Memory size of list `L`

Out[5]: 800984

In [6]: x = range(10000000) # 10M elements


# for i in x:
# print(i**2)

sys.getsizeof(x) # Size of `range` obj (constant space)

Out[6]: 48

this is why iterators are vital; generators are key for easily creating them.

A Simple Example
In [10]: def gen_demo():
yield "first statement"
yield "second statement"
yield "third statement"

In [11]: gen = gen_demo()


print(gen)

<generator object gen_demo at 0x00000239E3EF3140>

In [12]: print(next(gen))

first statement

In [13]: print(next(gen))

second statement

In [14]: print(next(gen))

third statement

In [15]: print(next(gen))

---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[15], line 1
----> 1 print(next(gen))

StopIteration:

In [16]: gen = gen_demo()


for i in gen:
print(i)

first statement
second statement
third statement

Generator is function with yield (not return ).


Returns a generator object.

Usage:

• next(gen) to get items.


• for loop for iteration.

Advantage: Simplifies iteration vs. old iterator chaos.

yield vs return
Normal Function executes, completes, and is removed from memory; Generator
pauses, retains state (variables), and resumes from pause point.

Key Difference: Generators maintain state and resume execution, while normal
functions are discarded post-execution.

Example 2
In [25]: def square(num):
for i in range(1, num+1):
yield i**2

In [26]: gen = square(10)

In [27]: print(next(gen))

In [28]: print(next(gen))

In [29]: print(next(gen))

In [30]: print(next(gen))

16

In [31]: for i in gen:


print(i)

25
36
49
64
81
100
Range Function (Generator)
In [35]: def mera_range(start, end):
for i in range(start, end):
yield i

In [36]: gen = mera_range(15, 26)


for i in gen:
print(i)

15
16
17
18
19
20
21
22
23
24
25

In [37]: for i in mera_range(15, 26):


print(i)

15
16
17
18
19
20
21
22
23
24
25

***Generators in Python** simplify iterator creation, reducing it to just two lines.*

Generator Expression
Generator expressions simplifies iterator creation with (expr for item in
iterable) .

In [40]: # list comprehension

L = [i**2 for i in range(1, 11)]

In [41]: L
Out[41]: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

In [42]: gen = (i**2 for i in range(1, 11))


for i in gen:
print(i)

1
4
9
16
25
36
49
64
81
100

Practical Example
In [49]: import os
import cv2

def image_data_reader(folder_path):
for file in os.listdir(folder_path):
f_array = cv2.imread(os.path.join(folder_path,file))
yield f_array

In [50]: gen = image_data_reader('C:/Users/91842/emotion-detector/train/Sad')

next(gen)
next(gen)

next(gen)
next(gen)
Out[50]: array([[[ 38, 38, 38],
[ 26, 26, 26],
[ 23, 23, 23],
...,
[198, 198, 198],
[196, 196, 196],
[167, 167, 167]],

[[ 32, 32, 32],


[ 25, 25, 25],
[ 26, 26, 26],
...,
[194, 194, 194],
[204, 204, 204],
[181, 181, 181]],

[[ 44, 44, 44],


[ 42, 42, 42],
[ 38, 38, 38],
...,
[156, 156, 156],
[214, 214, 214],
[199, 199, 199]],

...,

[[150, 150, 150],


[165, 165, 165],
[186, 186, 186],
...,
[229, 229, 229],
[226, 226, 226],
[239, 239, 239]],

[[145, 145, 145],


[156, 156, 156],
[180, 180, 180],
...,
[227, 227, 227],
[228, 228, 228],
[221, 221, 221]],

[[144, 144, 144],


[150, 150, 150],
[172, 172, 172],
...,
[211, 211, 211],
[189, 189, 189],
[217, 217, 217]]], dtype=uint8)

Handles large image datasets (4,000+ images to even 40 million images), by


loading 1 image at a time into memory.
Keras uses ImageDataGenerators for one-by-one data loading.

Benefits of Using a Generator

1. Ease of Implementation
In [ ]: class mera_range:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return mera_iterator(self)

In [ ]: # iterator

class mera_iterator:
def __init__(self, iterable_obj):
self.iterable = iterable_obj
def __iter__(self):
return self
def __next__(self):
if self.iterable.start >= self.iterable.end:
raise StopIteration
current = self.iterable.start
self.iterable.start += 1
return current

In [ ]: # generator

def mera_range(start, end):


for i in range(start, end):
yield i

Iterators: More code; Generators: Less code.

2. Memory Efficiency
In [55]: L = [x for x in range(100000)]
gen = (x for x in range(100000))

import sys
print('Size of L in memory', sys.getsizeof(L))
print('Size of gen in memory', sys.getsizeof(gen))

Size of L in memory 800984


Size of gen in memory 104

Generators save significant memory vs. lists, even when lists are expanded 10x;
use generators for sequential tasks.
3. Infinite Streams
In [56]: def all_even():
n = 0
while True:
yield n
n += 2

In [57]: even_num_gen = all_even()


next(even_num_gen)
next(even_num_gen)

Out[57]: 2

Infinite Data: Use Generators

4. Chaining Generators
In [58]: def fibonacci_numbers(nums):
x, y = 0, 1
for _ in range(nums):
x, y = y, x + y
yield x

def square(nums):
for num in nums:
yield num**2

print(sum(square(fibonacci_numbers(10))))

4895

Generators enable logical connections to accomplish complex tasks.


File I/O Basics
Data Types:

• Text: Unicode chars (e.g., '12345' in UTF-8/ASCII)


• Binary: Raw bytes (e.g., number 12345)

File Types:

• Text Files: Human-readable (e.g., source code, config files)


• Binary Files: Non-readable data (e.g., images, multimedia)

Process:

1. Open: Connects program to file


2. Read/Write: Handles data based on type
3. Close: Completes operations, frees resources

In [ ]: Writing to a File ---> `.txt` extension (Notepad).

In [ ]: # Case 1 - File Not Present

f = open('sample.txt', 'w')
f.write('Hello world')
f.close()

# Create file in current dir

In [ ]: # Error: File Closed

f.write('hello')

In [ ]: # Write multiline strings to a file

f = open('sample1.txt', 'w')
f.write('hello world')
f.write('\n how are you?')
f.close()

In [ ]: # Case 2 - File Overwrite in Write Mode ('w')

f = open('sample.txt', 'w')
f.write('salman khan')
f.close()

# Note: Opening in 'w' mode replaces all existing content in 'sample.txt'.


How open() Works in Python
Handles file I/O; interacts with disk files.

Example: f = open('sample.txt', 'w') - opens 'sample.txt' in write mode.

File Access & RAM Interaction: File loaded from disk (ROM) to RAM buffer.

File Operations & Modes: Modes (e.g., 'w' for write) determine file interactions
( f.write('salman khan') writes to RAM).

Data Integrity: f.close() saves buffer changes back to disk.

In [ ]: `open()` ---> File in RAM.

`write()` ---> Modify RAM buffer.

`close()` ---> Save to disk.

Source: Python Documentation.

In [ ]: # Problem with 'w' mode ---> Overwrites file content.


# To preserves existing content, use 'a' mode (append).

f = open('/content/sample1.txt', 'a')
f.write('\nI am fine')
f.close()

In [ ]: # Write Multiple Lines to a File

L = ['hello\n','hi\n','how are you\n', 'I am fine']

f = open('/content/temp/sample.txt', 'w')
f.writelines(L) # Efficiently writes multiple lines
f.close()

When you use f.close() to close a file, it serves two main purposes:

1. Memory Management:

• Releases RAM resources.


• Crucial for large/multiple files.

2. Security:

• Closes file buffers.


• Prevents unauthorized access.
Always use f.close() after file operations; Manages memory & security.

Reading from Files


1. read() : Reads all content into a single string. Efficient for small files.

Pros: Simple. Cons: Memory-heavy for large files.

2. readline() : Reads one line at a time. Good for large files and
sequential processing.

Pros: Memory-efficient. Cons: Slower for full content access.

In [ ]: # `read()` Usage

f = open('/content/sample.txt', 'r')
s = f.read()
print(s)
f.close()

# NOTE : File I/O handles data as strings.


# `txt` files process data as text only, no other formats.

hello
hi
how are you
I am fine

In [ ]: # Read up to n chars

f = open('/content/sample.txt', 'r')
s = f.read(10)
print(s)
f.close()

hello
hi
h

In [ ]: # Using `readline()`

f = open('/content/sample.txt', 'r')
print(f.readline(), end='') # Avoid auto newline
print(f.readline(), end='')
f.close()

hello
hi

In [ ]: `read()` Method:

Smaller files ---> loads entire content.


Immediate access ---> full data available.

Memory use ---> risky for large files.

`readline()` Method:

Large files ---> processes line-by-line.

Memory-efficient ---> avoids full file load.

Handles datasets ---> prevents overflow.

In [ ]: # Count Lines in File Efficiently ---> Avoid readline() per line; use custom code for

f = open('/content/sample.txt', 'r')
while True:
data = f.readline()
if data == '':
break
else:
print(data, end='')
f.close()

hello
hi
how are you
I am fine

Context Manager ( with )


Efficient resource management (e.g., files).

with ensures auto cleanup, no manual file close needed.

Purpose of with Statement

• File Management: Handles file operations (read/write).


• Resource Release: Auto-closes files, freeing system resources.

Avoids

• Memory Leaks: Manual closure prevents leaks


• File Locking: Prevents locking issues

Benefits:

• Automated Cleanup: Ensures auto-closure of files


• Exception Handling: Closes files if exceptions occur
• Readability: Clarifies file access scopes
• Reliability: Reduces bugs, ensures robust resource management

In [ ]: # `with` Statement

with open('/content/sample1.txt', 'w') as f:


f.write('selmon bhai')

In [ ]: f.write('hello')

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-4-00cba062fa3d> in <module>
----> 1 f.write('hello')

ValueError: I/O operation on closed file.

In [ ]: # `f.readline()`

with open('/content/sample.txt', 'r') as f:


print(f.readline())

hello

In [ ]: # Reading 10 Characters at a Time

with open('sample.txt', 'r') as f:


print(f.read(10)) # First 10 chars
print(f.read(10)) # Next 10 chars
print(f.read(10)) # Next 10 chars
print(f.read(10)) # Next 10 chars
# Each `print(f.read(10))` reads next 10 chars sequentially.

# Buffering tracks processed chars; `read()` resumes from buffer.

hello
hi
h
ow are you

I am fine

File Processing Strategy for Large Files


Crucial for files > RAM.

Chunk-Based Processing
• Process in chunks, not all at once. e.g., 10 GB file, 8 GB RAM ---> 2000
chars/chunk.

Advantages

• Memory Efficiency: RAM used for one chunk only.


• Scalability: Handles files > RAM.
• Performance: Avoids system slowdowns.

In [ ]: # Purpose: Save dataset to file (avoid memory load).

big_L = ['hello world ' for i in range(1000)]

with open('big.txt', 'w') as f:


f.writelines(big_L)

In [ ]: with open('big.txt', 'r') as f:


chunk_size = 10
while len(f.read(chunk_size)) > 0:
print(f.read(chunk_size), end='***')
f.read(chunk_size) # Skip to next chunk

# Handles large files, processes in chunks, avoiding memory overload.


# Libraries like Pandas, Keras use chunk-based data processing.
d hello wo***o world he***d hello wo***o world he***d hello wo***o world he***d
hello wo***o world he***d hello wo***o world he***d hello wo***o world he***d h
ello wo***o world he***d hello wo***o world he***d hello wo***o world he***d he
llo wo***o world he***d hello wo***o world he***d hello wo***o world he***d hel
lo wo***o world he***d hello wo***o world he***d hello wo***o world he***d hell
o wo***o world he***d hello wo***o world he***d hello wo***o world he***d hello
wo***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***d hello wo***o world he***d hello w
o***o world he***d hello wo***o world he***

In [ ]: # Seek and Tell Function

with open('sample.txt', 'r') as f:


f.seek(15) # Move to 15th char
print(f.read(10)) # Read 10 chars
print(f.tell()) # Position after read
print(f.read(10)) # Read next 10 chars
print(f.tell()) # New position

e you
I am
25
fine
30

In [ ]: `seek` ---> Set desired location within context.


---> Like YouTube red line for precise navigation.
---> Moves to specified points in system.

`tell` ---> Reveals current position/status.


---> Acts as a marker indicating present state.
---> Provides feedback without changing position.

# `seek` navigates to points (YouTube red line analogy).


# `tell` shows current position/status.

In [ ]: # Seek during write

with open('sample.txt', 'w') as f:


f.write('Hello')
f.seek(0) # Cursor to start
f.write('Xa') # Overwrite 'He' ---> 'Xa'

Limitations of Text Mode


• Binary Files: Incompatible with non-text data (e.g., images, binaries).
• Data Type Efficiency: Inefficient for non-text types (integers, floats,
lists, tuples).

Binary Files:

• Contain non-textual binary data.


• Text Mode cannot process these effectively.

Non-Textual Data:

• Incompatible with Text Mode.


• Requires specific methods for management.

Structured Data:

• Struggles with types like integers, floats, lists, tuples.


• Needs specialized handling.

In [ ]: # Read Binary File

with open('screenshot1.png', 'r') as f:


f.read()

---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-23-b662b4ad1a91> in <module>
1 # working with binary file
2 with open('screenshot1.png','r') as f:
----> 3 f.read()

/usr/lib/python3.7/codecs.py in decode(self, input, final)


320 # decode input (taking the buffer into account)
321 data = self.buffer + input
--> 322 (result, consumed) = self._buffer_decode(data, self.errors, fin
al)
323 # keep undecoded input until the next call
324 self.buffer = data[consumed:]

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid


start byte

In [ ]: # Binary File I/O

with open('screenshot1.png', 'rb') as f: # Read binary


with open('screenshot_copy.png', 'wb') as wf: # Write binary
wf.write(f.read())

In [ ]: # Working with a Large Binary File

In [ ]: # Working with Different Data Types


with open('sample.txt', 'w') as f:
f.write(str(5))

# Error: Text must be Unicode; ensure data is a string.

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-26-a8e7a73b1431> in <module>
1 # working with other data types
2 with open('sample.txt','w') as f:
----> 3 f.write(5)

TypeError: write() argument must be str, not int

In [ ]: with open('sample.txt', 'w') as f:


f.write('5')

In [ ]: with open('sample.txt', 'r') as f:


print(int(f.read()) + 5) # convert read() output to int

10

In [ ]: # More Complex Data

d = {
'name':'nitish',
'age':33,
'gender':'male'
}

with open('sample.txt', 'w') as f:


f.write(str(d))

In [ ]: with open('sample.txt', 'r') as f:


print(dict(f.read()))

# Error: str ---> dict

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-34-949b64f1fbe0> in <module>
1 with open('sample.txt','r') as f:
----> 2 print(dict(f.read()))

ValueError: dictionary update sequence element #0 has length 1; 2 is required

In [ ]: # Text-based Limitations for Complex Data Storage:

1. Storage ---> Plain Text Files ideal for simple textual data.
Complex Data (e.g., Python Dicts) contains structured data with

2. Conversion ---> Saving Dicts with `write()` converts dicts to strings.


`{'name': 'John', 'age': 30}` ---> `"{'name': 'John', 'age': 30}"
This flattening loses structure and format.
3. Retrieval ---> Retrieval returns as a string; requires parsing to reconstruct
Error-Prone parsing can introduce errors.

# NOTE: for Simple Data use text files; for Complex Data use serialization libraries o

JSON Serialization & Deserialization


Serialization:

Convert Python data ---> JSON.

json.dumps()

Human-readable & machine-parsable.

Deserialization:

Convert JSON ---> Python.

json.loads()

Manipulate JSON data in Python.

What is JSON?
JavaScript Object Notation

Widely adopted in Web apps, APIs, data interchange.

Simple syntax, supports key-value pairs, arrays, nested objects.

{
"d": {
"results": [
{
"_metadata": {
"type": "Employee Details. Employee"
},
"UserID": "E12012",
"RoleCode": "35"
}
]
}
}

JSON is a widely-used text format across languages.


In [ ]: # JSON Serialization

# List to JSON
import json

L = [1, 2, 3, 4]

with open('demo.json', 'w') as f:


json.dump(L, f) # Serialize L to 'demo.json'

In [ ]: # Dict to JSON
d = {
'name':'nitish',
'age':33,
'gender':'male'
}

with open('demo.json', 'w') as f:


json.dump(d, f, indent=4) # Serialize dict d with indentation

In [ ]: # Deserialization

import json

with open('demo.json', 'r') as f:


d = json.load(f)
print(d)
print(type(d))

{'name': 'nitish', 'age': 33, 'gender': 'male'}


<class 'dict'>

Serialization and Deserialization Convert complex data (lists, dicts, 2D


dicts, tuples, sets) to/from JSON.

Serialization : Complex ---> JSON (for storage).

Deserialization : JSON ---> Original (for retrieval).

Handles complex data efficiently, overcoming string-based limitations.

In [ ]: # Serialize/Deserialize Tuple

import json

t = (1, 2, 3, 4, 5)

with open('demo.json', 'w') as f:


json.dump(t, f)

In [ ]: # Note: Serialization/Deserialization
Serialize tuple ---> List (using `dump`)

Deserialize ---> List (not tuple)

Need tuple later? ---> Explicit conversion required

In [ ]: # Serialize/Deserialize Nested Dict

d = {
'student':'nitish',
'marks':[23, 14, 34, 45, 56]
}

with open('demo.json', 'w') as f:


json.dump(d, f)

Serializing & Deserializing Custom Objects


In [ ]: class Person:

def __init__(self, fname, lname, age, gender):


self.fname = fname
self.lname = lname
self.age = age
self.gender = gender

# Print format:
# Name: {fname} {lname}
# Age: {age}
# Gender: {gender}

In [ ]: person = Person('Nitish', 'Singh', 33, 'male')

Python serializes built-in types natively (e.g., dicts).

Custom Classes Needs Custom Serialization (Explicit).

In [ ]: # String Representation

import json

def show_object(person):
if isinstance(person, Person):
return "{} {} age -> {} gender -> {}".format(person.fname, person.lname, person

with open('demo.json', 'w') as f:


json.dump(person, f, default=show_object)

In [ ]: # Dictionary Representation
import json

def show_object(person):
if isinstance(person, Person):
return {'name':person.fname + ' ' + person.lname, 'age':person.age, 'gender'

with open('demo.json', 'w') as f:


json.dump(person, f, default=show_object, indent=4)

In [ ]: # indent attribute

# As a dict

In [ ]: # Deserializing JSON

import json

with open('demo.json', 'r') as f:


d = json.load(f)
print(d)
print(type(d))

{'name': 'Nitish Singh', 'age': 33, 'gender': 'male'}


<class 'dict'>

Until now, we've printed Python Custom Objects (dicts, strings) in specific
formats.

Cross-file Object Usage i.e. Direct use of class/obj from another file not possible.

Solution: Convert object to binary format for cross-file compatibility.

Pickling and Unpickling


In [ ]: +-------------------------------------------+-----------------------------------------
| Pickling | Unpickling
+-------------------------------------------+-----------------------------------------
| |
| Serialize Python objects to byte stream. | Deserialize byte stream to original
| |
| Byte stream compactly represents objects. | Reconstructs objects/data structures
| |
| Enables storage/transmission of objects. | Restores objects for use by Python
| |
+-------------------------------------------------------------------------------------
| Purpose
+-------------------------------------------------------------------------------------
| |
| Convert objects to portable byte format. | Restore objects from byte format.
| |
| Save/load data, caching, IPC. | Save/load data, caching, IPC.
| |
+-------------------------------------------------------------------------------------
| Applications
+-------------------------------------------------------------------------------------
| |
| Save/load complex data. | Restore complex data.
| |
| Cache objects. | Rebuild cached objects.
| |
| Transmit objects over networks. | Handle transmitted objects.
| |
+-------------------------------------------------------------------------------------

In [ ]: class Person:

def __init__(self, name, age):


self.name = name
self.age = age

def display_info(self):
print('Hi my name is', self.name, 'and I am ', self.age, 'years old')

In [ ]: p = Person('nitish', 33)

In [ ]: # Pickle Dump

import pickle
with open('person.pkl', 'wb') as f:
pickle.dump(p, f)

In [ ]: # Pickle Load

import pickle
with open('person.pkl', 'rb') as f:
p = pickle.load(f)

p.display_info()

Hi my name is nitish and I am 33 years old

In [ ]: Obj ---> Bin File

Send ---> Extract ---> Use

Works like original

Pickle vs JSON
In [ ]: +-----------------------------------------------+-------------------------------------
| Pickle | JSON
+-----------------------------------------------+-------------------------------------
| |
| Binary format; Python-specific. | Text-based; cross-platform.
| |
| Non-human-readable, Python-only. | Human-readable, interoperable
| |
| Potential security risks with untrusted data. | Safer for untrusted data.
| |
| Efficient for complex Python structures. | Ideal for web APIs, configs,
| |
+-----------------------------------------------+-------------------------------------
Programming Errors
1. Compilation (High-level ---> Machine code)

Syntax Errors: Violates syntax rules.

Syntax Error

Error from code not following language rules.

• Caused by missing parenthesis, Incorrect indentation,


Misspelled keyword

• Detected when Interpreter/Compiler raises syntax error;


code can't run until fixed.

• Resolution is to identify and correct grammatical issues to


match syntax rules.

2. Execution (Runtime Errors)

Exceptions: Issues during runtime.

In [ ]: # Syntax Error Examples

print 'hello world'

File "<ipython-input-3-4655b84ba7b7>", line 2


print 'hello world'
^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print('hello
world')?

Syntax Errors: Key Points


• Missing Symbols: E.g., colons, brackets ---> disrupts code structure.
• Misspelled Keywords: Reserved words ---> unrecognized commands.
• Incorrect Indentation: Critical in Python ---> wrong block
interpretation.
• Empty Control Structures: if/else , loops, functions ---> must
include executable code or placeholders.

In [ ]: a = 5

if a == 3
print('hello')
File "<ipython-input-68-efc58c10458d>", line 2
if a==3
^
SyntaxError: invalid syntax

In [ ]: a = 5

iff a == 3:
print('hello')

File "<ipython-input-69-d1e6fae154d5>", line 2


iff a==3:
^
SyntaxError: invalid syntax

In [ ]: a = 5

if a == 3:
print('hello')

File "<ipython-input-70-ccc702dc036c>", line 3


print('hello')
^
IndentationError: expected an indented block

In [ ]: # 1. IndexError: Accessing invalid index.

L = [1, 2, 3]
L[100]

---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-71-c90668d2b194> in <module>
2 # The IndexError is thrown when trying to access an item at an invalid
index.
3 L = [1,2,3]
----> 4 L[100]

IndexError: list index out of range

In [ ]: # 2. ModuleNotFoundError: Module not found.

import mathi
math.floor(5.3)
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
<ipython-input-73-cbdaf00191df> in <module>
1 # ModuleNotFoundError
2 # The ModuleNotFoundError is thrown when a module could not be found.
----> 3 import mathi
4 math.floor(5.3)

ModuleNotFoundError: No module named 'mathi'

---------------------------------------------------------------------------
NOTE: If your import is failing due to a missing package, you can
manually install dependencies using either !pip or !apt.

To view examples of installing some common dependencies, click the


"Open Examples" button below.
---------------------------------------------------------------------------

In [ ]: # 3. KeyError: Dictionary key not found.

d = {'name':'nitish'}
d['age']

---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-74-453afa1c9765> in <module>
3
4 d = {'name':'nitish'}
----> 5 d['age']

KeyError: 'age'

In [ ]: # 4. TypeError: Inappropriate type for operation.

1 + 'a'

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-78-2a3eb3f5bb0a> in <module>
1 # TypeError
2 # The TypeError is thrown when an operation or function is applied to a
n object of an inappropriate type.
----> 3 1 + 'a'

TypeError: unsupported operand type(s) for +: 'int' and 'str'

In [ ]: # 5. ValueError: Correct type, wrong value.

int('a')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-76-e419d2a084b4> in <module>
1 # ValueError
2 # The ValueError is thrown when a function's argument is of an inapprop
riate type.
----> 3 int('a')

ValueError: invalid literal for int() with base 10: 'a'

In [ ]: # 6. NameError: Undefined name.

print(k)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-79-e3e8aaa4ec45> in <module>
1 # NameError
2 # The NameError is thrown when an object could not be found.
----> 3 print(k)

NameError: name 'k' is not defined

In [ ]: # 7. AttributeError: Invalid attribute reference.

L = [1, 2, 3]
L.upper()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-80-dd5a29625ddc> in <module>
1 # AttributeError
2 L = [1,2,3]
----> 3 L.upper()

AttributeError: 'list' object has no attribute 'upper'

Stacktrace Overview:

• Detailed error info during execution. Includes error type, message, code
location (line/file).
• Purpose is to help identify and fix issues. Crucial for debugging in
development/testing.

Production Considerations:

• UX: Avoid displaying to users. Technical jargon can confuse and


frustrate.
• Security: Exposing can leak sensitive info. Risk of exploitation.

Best Practices:
• Gracefully handle errors. Show user-friendly messages.
• Use stacktraces internally for debugging only.

Exceptions in Programming:
Runtime issues disrupting execution. Require immediate handling for stability.

Common Issues:

• Memory overflow: Exceeds memory limits.


• Division by zero: Undefined operations.
• Database errors: Connection/query failures.

Importance: Prevents crashes, ensures stability, and improves reliability.

Python Handling
In [ ]: # Create file and write text

with open('sample.txt', 'w') as f:


f.write('hello world')

In [ ]: # Try-Catch Demo

try:
with open('sample1.txt', 'r') as f:
print(f.read())
except:
print('sorry file not found')

sorry file not found

try-except Blocks
Purpose:

• Mitigate Risks: File perms, network issues.


• Enhance Robustness: Avoid crashes, manage errors.
• Graceful Handling: Recover from issues.

Structure:

• try Block: Risky ops (file I/O, DB).


• except Block: Catches exceptions from try .
Benefits:

• Reliability: Avoid crashes.


• Clean Code: Error-handling separate.
• Resilience: Recover from errors.

Best Practices:

• Specific exceptions ( FileNotFoundError , ConnectionError ).


• Appropriate messages or fallbacks in except .

In [ ]: # Catching Specific Exceptions ---> informing users about errors, improving user exper

try:
m = 5
f = open('sample1.txt', 'r')
print(f.read())
print(m)
print(5 / 2)
L = [1, 2, 3]
L[100]
except FileNotFoundError:
print('file not found')
except NameError:
print('variable not defined')
except ZeroDivisionError:
print("can't divide by 0")
except Exception as e:
print(e)

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

In [ ]: # `else` in Try-Except

try:
f = open('sample1.txt', 'r')
except FileNotFoundError:
print('file nai mili')
except Exception:
print('kuch to lafda hai')
else:
print(f.read())

file nai mili

try , else , and finally Blocks


try Block: Executes risky code; avoids abrupt halts on errors.

else Block: Runs if try succeeds; executes only when no exceptions.


except Block: Manages errors from try .

finally Block: Executes regardless of exceptions; ensures cleanup (e.g., close


files, release resources).

try , else , finally = Structured exception handling.

In [ ]: # `finally`

try:
f = open('sample1.txt', 'r')
except FileNotFoundError:
print('file nai mili')
except Exception:
print('kuch to lafda hai')
else:
print(f.read())
finally:
print('ye to print hoga hi')

file nai mili


ye to print hoga hi

raise Keyword
Trigger exceptions manually.

Custom Exceptions: Pass values for context.

Error Control: Enhance robust design and manage unexpected issues.

In [ ]: raise ZeroDivisionError('aise hi try kar raha hu')

# Java Equivalents:

# `try` ---> `try`


# `except` ---> `catch`
# `raise` ---> `throw`

---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-106-5a07d7d89433> in <module>
----> 1 raise ZeroDivisionError('aise hi try kar raha hu')

ZeroDivisionError: aise hi try kar raha hu

In [ ]: class Bank:

def __init__(self, balance):


self.balance = balance
def withdraw(self, amount):
if amount < 0:
raise Exception('amount cannot be -ve')
if self.balance < amount:
raise Exception('paise nai hai tere paas')
self.balance = self.balance - amount

obj = Bank(10000)
try:
obj.withdraw(15000)
except Exception as e:
print(e)
else:
print(obj.balance)

paise nai hai tere paas

In [ ]: `raise` ---> Trigger exceptions.


`except` ---> Handle exceptions.

Enhances app robustness by managing errors, preventing abrupt terminations.

exception hierarchy in python


In [ ]: BaseException
|
|
+--------+---------+
| |
| |
Exception KeyboardInterrupt
| +--------- FloatingPointError
| |
+-------- ArithmeticError --------+--------- OverflowError
| |
| +--------- ZeroDivisionError
|
| +--------- IndexError
+---------- LookupError ----------|
| +--------- KeyError
|
| +--------- FileExists Error
+------------ OSError ------------|
+--------- Permission Error

Python allows creating custom exceptions, which means you can define your own
types of errors.

In [ ]: class MyException(Exception):
def __init__(self, message):
print(message)
class Bank:
def __init__(self, balance):
self.balance = balance

def withdraw(self, amount):


if amount < 0:
raise MyException('amount cannot be -ve')
if self.balance < amount:
raise MyException('paise nai hai tere paas')
self.balance = self.balance - amount

obj = Bank(10000)
try:
obj.withdraw(5000)
except MyException as e:
pass
else:
print(obj.balance)

5000

Custom Classes: Why & Benefits


Purpose:

• Full control over app structure & behavior


• Ideal for custom login/registration systems

Benefits:

1. Security: Custom security measures, e.g., device signature


management, auto log-out on unrecognized devices
2. Functionality: Tailored features, e.g., user input management (name,
email, password), device signature handling

Implementation:

• Control over security protocols


• Enables advanced security features

simple example
In [ ]: class SecurityError(Exception):

def __init__(self, message):


print(message)

def logout(self):
print('logout')

class Google:

def __init__(self, name, email, password, device):


self.name = name
self.email = email
self.password = password
self.device = device

def login(self, email, password, device):


if device != self.device:
raise SecurityError('bhai teri to lag gayi')
if email == self.email and password == self.password:
print('welcome')
else:
print('login error')

obj = Google('nitish', '[email protected]', '1234', 'android')

try:
obj.login('[email protected]', '1234', 'windows')
except SecurityError as e:
e.logout()
else:
print(obj.name)
finally:
print('database connection closed')

bhai teri to lag gayi


logout
database connection closed
Namespaces & Scope
Containers for identifiers (names) and objects, akin to dictionaries.

Scope is a context for variable access.

In [ ]: Types of Namespaces:

+---------------------------------------------------------------+---------------------
| Namespace |
+---------------------------------------------------------------+---------------------
| |
| 1. Global Namespace | x = 10 # Global
| | def func():
| Scope ---> Top-level. | print(x)
| |
| Accessible everywhere unless shadowed. |
| |
+---------------------------------------------------------------+---------------------
| |
| 2. Local Namespace | def func():
| | y = 5 # Local
| Scope ---> Function-specific. | print(y)
| |
| Created during function call & Destroyed after function call. |
| |
+---------------------------------------------------------------+---------------------
| |
| 3. Enclosing Namespace | def outer():
| | z = 20
| Scope ---> Outer function for nested functions. | def inner
| | print
| Outer function's scope accessible to inner functions. |
| |
+---------------------------------------------------------------+---------------------
| |
| 4. Builtin Namespace | print(len([1,
| |
| Scope ---> Predefined names (e.g., functions, modules). |
| |
| Always accessible. |
| |
+---------------------------------------------------------------+---------------------

Scope and LEGB Rule


LEGB Rule:

1. Local: Variables in function.


2. Enclosing: Variables in nested functions.
3. Global: Top-level module/script variables.
4. Built-in: Predefined names in Python.

In [ ]: Details:

+---------------------------------+---------------------------------------------+
| Scope | Example
+---------------------------------+---------------------------------------------+
| |
| 1. Local | def outer():
| | x = "enclosing"
| Scope ---> Innermost function. | def inner():
| | x = "local"
| Checked first. | print(x) # local |
| | inner()
| |
+---------------------------------+---------------------------------------------+
| |
| 2. Enclosing | def outer():
| | x = "enclosing"
| Scope ---> Outer function. | def inner():
| | print(x) # enclosing |
| Checked after local. | inner()
| |
+---------------------------------+---------------------------------------------+
| |
| 3. Global | x = "global"
| | def func():
| Scope ---> Module/script-level. | print(x) # global |
| |
| Checked after enclosing. |
| |
+---------------------------------+---------------------------------------------+
| |
| 4. Built-in | def func():
| | print(len([1, 2, 3])) # built-in len() |
| Scope ---> Python's built-ins. | |
| |
| Checked last. |
| |
+---------------------------------+---------------------------------------------+
| |
| Unresolved Names | def func():
| | print(x) # NameError |
| `NameError` if name not found. |
| |
+---------------------------------+---------------------------------------------+

In [1]: # Global & Local Vars


a = 2 # Global

def temp():
b = 3 # Local
print(b)

temp()
print(a)

3
2

In [2]: # Variables with the Same Name

a = 2 # Global

def temp():
a = 3 # Local
print(b)

temp()
print(a)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[2], line 9
6 a = 3
7 print(b)
----> 9 temp()
10 print(a)

Cell In[2], line 7, in temp()


4 def temp():
5 # local var
6 a = 3
----> 7 print(b)

NameError: name 'b' is not defined

In [3]: # Global vs. Local

a = 2 # Global

def temp():
# Accesses global `a`
print(a)

temp()
print(a)

2
2

In [4]: # Modifying Global Variable


a = 2

def temp():
a += 1 # Modifying 'a'
print(a)

temp()
print(a)

---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[4], line 9
6 a += 1
7 print(a)
----> 9 temp()
10 print(a)

Cell In[4], line 6, in temp()


4 def temp():
5 # local var
----> 6 a += 1
7 print(a)

UnboundLocalError: local variable 'a' referenced before assignment

In [5]: a = 2

def temp():
global a
a += 1
print(a)

temp()
print(a)

3
3

In [6]: # Global variable inside a function

def temp():
global a # Declare 'a' as global
a = 1 # Modify global 'a'
print(a)

temp()
print(a)

1
1

In [7]: # Function local variable


def temp(z): # z is local to temp()
print(z)

a = 5 # a is global
temp(5)

print(a)
print(z)

5
5
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 9
7 temp(5)
8 print(a)
----> 9 print(z)

NameError: name 'z' is not defined

Built-in Scope
• Python offers built-in functions (e.g., print ) without imports.
• Part of the built-in scope, automatically included in Python.

In [ ]: # List built-in functions/vars

import builtins
print(dir(builtins))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'Block
ingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessErr
or', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'Co
nnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'Environment
Error', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'Floating
PointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'Impor
tWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryE
rror', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNo
tFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'Not
ImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'Pe
rmissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'Reso
urceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIter
ation', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabErro
r', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeErr
or', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWar
ning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__IPYTHO
N__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__',
'__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bo
ol', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'com
pile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display',
'divmod', 'enumerate', 'eval', 'exec', 'execfile', 'filter', 'float', 'format',
'frozenset', 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'h
ex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'licens
e', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oc
t', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'ro
und', 'runfile', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 's
um', 'super', 'tuple', 'type', 'vars', 'zip']

In [ ]: # Renaming Built-ins

L = [1, 2, 3]
print(max(L)) # Uses built-in max()

def max(): # Redefine max()


print('hello')

print(max(L))

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-68-c19f3451a38f> in <module>
1 # renaming built-ins
2 L = [1,2,3]
----> 3 print(max(L))
4 def max():
5 print('hello')

TypeError: max() takes 0 positional arguments but 1 was given

In [ ]: # Enclosing Scope

def outer():
def inner():
print(a) # a in outer's scope
inner()
print('outer function') # Outer

outer() # Calls outer ---> inner ---> prints a ---> 'outer function'
print('main program') # Main

1
outer function
main program

In [ ]: # `nonlocal` Keyword ---> Modify variables in outer (but non-global) scope.

def outer():
a = 1
def inner():
nonlocal a # Access outer 'a'
a += 1
print('inner', a)
inner()
print('outer', a)

outer()
print('main program')

inner 2
outer 2
main program

Decorators
Function that modifies/extends another function’s behavior.

Usage: Adds functionality (e.g., logging, access control) without altering original
code.

Types:

1. Built-in:

• @staticmethod : Class-level method, no instance.


• @classmethod : Method with class as 1st arg, modifies class state.
• @abstractmethod : Enforces method implementation in subclasses.
• @property : Method accessed as attribute (getter/setter).

2. User-Defined:

Creation:

def my_decorator(func):
def wrapper():
# Add functionality
return func()
return wrapper

In [ ]: # Python supports 1st Class Functions

def modify(func, num):


return func(num)

def square(num):
return num ** 2

modify(square, 2)

Out[ ]: 4

In [ ]: def my_decorator(func):
def wrapper():
print('***********************')
func()
print('***********************')
return wrapper

def hello():
print('hello')

def display():
print('hello nitish')

# Manual Decoration
a = my_decorator(hello)
a()

b = my_decorator(display)
b()

***********************
hello
***********************
***********************
hello nitish
***********************

more functions
In [10]: # Closure Example

def outer():
a = 5 # Outer scope var
def inner():
print(a) # Access outer scope var
return inner
b = outer() # b now holds the inner function
b()

Closures
Inner function retains access to outer function's variables post-execution.

Captures environment, preserving outer function's variables.

How Closures Work

• Local vars usually lost after function execution.


• Closures keep access to outer vars.

Benefit: Preserves state/configuration; useful for decorators.

Example Code

def outer(outer_var):
def inner():
print(outer_var)
return inner

# Usage
closure = outer('Hello, world!')
closure() # Output: Hello, world!

In [ ]: def my_decorator(func):
def wrapper():
print('***********************')
func()
print('***********************')
return wrapper

@my_decorator
def hello():
print('hello')

hello()

***********************
hello
***********************

In [1]: # anything meaningful?

import time
def timer(func):
def wrapper(*args):
start = time.time()
func(*args)
print('time taken by', func.__name__, time.time()-start, 'secs')
return wrapper

@timer
def hello():
print('hello world')
time.sleep(2)

@timer
def square(num):
time.sleep(1)
print(num**2)

@timer
def power(a, b):
print(a**b)

hello()
square(2)
power(2, 3)

hello world
time taken by hello 2.0078108310699463 secs
4
time taken by square 1.0098490715026855 secs
8
time taken by power 0.0 secs

In [11]: # Decorators with Arguments - Example

# A Big Problem

def square(num):
print(num ** 2)

# Erroneous call
square('hehe')

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[11], line 9
6 print(num ** 2)
8 # Function call with incorrect argument type
----> 9 square('hehe')

Cell In[11], line 6, in square(num)


5 def square(num):
----> 6 print(num ** 2)

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'


Create a decorator to verify number datatype for sanity checks on user input.

In [ ]: @checkdt(int)
def square(num):
print(num**2)

In [ ]: def sanity_check(data_type):
def outer_wrapper(func):
def inner_wrapper(*args):
if type(*args) == data_type:
func(*args)
else:
raise TypeError('Ye datatype nahi chalega')
return inner_wrapper
return outer_wrapper

@sanity_check(int)
def square(num):
print(num**2)

@sanity_check(str)
def greet(name):
print('hello', name)

square(2)

You might also like