Python
Python
Lists
Lists are mutable, ordered collections of items.
A list can contain the same data type items or even different data type items.
lst = []
lst1 = list()
# This will return the value of the list element which is present in the 0th ind
ex
# This is for returning a specific index
print(lst[0]) # a
Python 1
# Returning the last index of the list
print(lst[-1]) # True
# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex
print(lst)
Some useful functions that are used while the list is being implemented
lst.append('b')
print(lst)
Python 2
2. insert() - If we want to insert a value at a specific index then we use insert.
lst.insert(1, 'b')
print(lst)
3. remove() - This is used to remove the first occurrence of a value from a list that is specified.
lst.remove('a')
print(lst)
lst.pop()
print(lst)
# Result: a, b, a, 1, 2.14,
5. index() - This is used to fetch the first occurrence of the index of a particular value present in the list
print(lst.index(2.14))
# Result: 2
6. count() - This is used to count the occurrences of values present in the list
Python 3
lst = ['a', 'b', 'a', 1, 2.14, True]
print(lst.count('a'))
# Result: 2
lst.sort()
print(lst)
# Result: a, b, c
lst.reverse()
print(lst)
# Result: c, b, a
lst.clear()
print(lst)
Python 4
# Optimized code would be
lst = [i**2 for i in range(10)]
Tuple
Tuples are immutable, ordered collections of items
Tuples can contain the same data type items or even different data type items.
The main difference between a list and a tuple is that lists are mutable and tuples are immutable.
tpl1 = tuple()
# This will return the value of the tuple element which is present in the 0th in
dex
# This is for returning a specific index
print(tpl[0]) # a
Python 5
print(tpl[-1]) # True
# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex
Some useful functions that are used while the list is being implemented
tpl1 = (1,2,3,4,5)
tpl2 = ('a','b')
print(tpl3)
# Result: 1, 2, 3, 4, 5, a, b
tpl1 = (1,2,3,4,5,1,1,2)
print(tpl.count(1))
# Result: 3
3. index() - This is used to return the first occurrence index of a specified value
tpl1 = (1,2,3,4,5,1,1,2)
Python 6
print(tpl.index(1))
# Result: 0
4. Packed tuple - By default if we haven’t specified any method then by default it is set as a tuple
tpl1 = 1,2,3,4
print(type(tpl1))
# Result: tuple
tpl1 = 1,2,3,4
a,b,c,d = tpl1
print(a) # 1
print(b) # 2
print(c) # 3
print(d) # 4
Sets
Sets are used to store unique elements in an unordered manner.
Sets can contain the same data type items or even different data type items.
st = ()
st1 = set()
Python 7
# Set containing different data type values
st = {'a', 1, 2.14, False}
# This will return the value of the set element that is present in the 0th index
# This is for returning a specific index
print(st[0]) # a
# Result: 1, 2.14
# This returns the 1st and 2nd index and not the 3rd index as the first number
"1" indicates that the index must be included and the second number in the brack
et indicates that include the values up to that index but do not include that in
dex
1. add() - It is used to add a value at the last index of the set. Remember if that particular value that is
being added is already present in the set then the value will not be added as a set only stores unique
values.
st = {1,2,3,4}
st.add(5)
print(st) # 1,2,3,4,5
Python 8
st.add(1) # Does not add the value 1 in the set because the value already exists
in the set
print(st) # 1,2,3,4,5
st = {1,2,3,4}
st.remove(3)
print(st) # 1,2,4
3. discard() - This is the same as the remove() but the only difference is that suppose we have a value
that we want to remove in the set but that value is not present in the set, so remove() will prompt us an
error but discard will not prompt us an error.
st = {1,2,3,4}
st.discard(5)
print(st) # 1,2,3,4
4. pop() - This is used to remove the value that is present in the last index of the set.
st = {1,2,3,4}
st.pop()
print(st) # 1,2,3
5. clear() - This is used to remove all the values present in the set.
st = {1,2,3,4}
st.clear()
6. in - This is used to check whether a particular value is present in the set or not
Python 9
st = {1,2,3,4}
st1 = {1,2,3,4}
st2 = {7,8,9,10}
print(set1.union(set2))
# Result: {1,2,3,4,7,8,9,10}
8. intersection() - This is used to return those values which are present in both the sets
st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}
print(set1.intersection(set2))
# Result: {1,9}
9. intersection_update() - This is used to return those values that are present in both sets and also update
the first set which is used before the intersection_update() function.
st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}
print(set1.intersection_update(set2))
# Result: {1,9}
print(set1)
# Result: {1,9}
10. difference() - This is used to return the elements which are present in the first set and not present in
the second set.
st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}
Python 10
print(set1.differnce(set2))
# Result: {2,3,4}
11. symmetric_difference() - This is used to return the elements which are not common in both sets.
st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}
print(set1.symmetric_difference(set2))
# Result: {2,3,4,7,8,10}
12. issubset() - This returns True or False whether the set is a subset of another set or not
st1 = {1,2,3,4,9}
st2 = {1,2,3,4,7,8,9,10}
print(set1.issubset(set2))
# Result: True
st1 = {1,2,3,4,9}
st2 = {1,7,8,9,10}
print(set1.issubset(set2))
# Result: False
13. superset() - This returns True or False whether a set is a superset of another set or not. This is the
opposite of issubset().
Dictionary
Dictionaries are an unordered collection of items.
They store the data in the form of key-value pairs and each key must be unique and immutable.
my_dict = {}
my_dict = dict()
Python 11
"name": "Alice",
"age": 30,
"city": "New York"
}
# Result
# name: Alice
# age: 31
# email: alice@example.com
1. copy() - This is used to copy the exisiting dictionary key-values into another variable whose type will
become a dictionary.
# Original dictionary
original_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}
Python 12
# Modifying the copied dictionary
copied_dict["age"] = 31
# Result:
# Original dictionary: {'name': 'Alice', 'age': 30, 'city': 'New York'}
# Copied dictionary: {'name': 'Alice', 'age': 31, 'city': 'New York'}
# Example dictionary
my_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Example dictionary
my_dict = {
"name": "Alice",
"age": 30,
"city": "New York"
}
Python 13
# Displaying the values
print("Result:", values) # Result: dict_values(['Alice', 30, 'New York'])
print("Result:", values_list) # Result: ['Alice', 30, 'New York']
4. Merging 2 dictionaries
# Example dictionaries
dict1 = {"name": "Alice", "age": 30}
dict2 = {"city": "New York", "email": "alice@example.com"}
# Result
print("Result:", merged_dict)
# Result: {'name': 'Alice', 'age': 30, 'city': 'New York', 'email': 'alice@examp
le.com'}
Function
A function is a block of code which can be used for many purposes like code reusability, orgainizing
code and imporve readability.
Python 14
# The returned value is stored in the result variable.
# We print the result to see the output of the function call.
# Defining a function
def add_numbers(a, b):
"""
This function returns the sum of two numbers.
"""
return a + b
Args - In Python, *args is used in function definitions to allow the function to accept a variable
number of positional arguments. This means you can pass any number of arguments to the
Python 15
function, and they will be accessible as a tuple within the function.
Kwargs - In Python, **kwargs is used in function definitions to allow the function to accept a variable
number of keyword arguments. This means you can pass any number of arguments in the form of
key-value pairs, and they will be accessible as a dictionary within the function.
Python 16
for arg in args:
print(f"- {arg}")
# Result
# Positional arguments:
# Alice
# 30
# Keyword arguments:
# city: New York
# email: alice@example.com
Lambda Function - A lambda function in Python is a small anonymous function defined using the
lambda keyword. Lambda functions can have any number of arguments but only one expression.
The expression is evaluated and returned. They are often used for short, simple operations where
defining a full function would be overkill.
# Syntax
lambda arguments: expression
Map Function - The map() function in Python applies a given function to all the items in an input list
(or any iterable) and returns a map object (which is an iterator) of the results. You can convert the
map object to a list, tuple, or other collection types if needed.
# Syntax
map(function, iterable, ...)
Python 17
return x ** 2
# List of numbers
numbers = [1, 2, 3, 4, 5]
# Using map() to apply the square function to each element in the numbers list
squared_numbers = map(square, numbers)
# Using the above example but using the lambda function to return the square r
oot of each value
# List of numbers
numbers = [1, 2, 3, 4, 5]
So from above we see that it is better to use lambda functions as compared to creating functions as
they are simple calculations
Filter Function - The filter() function in Python is used to construct an iterator from elements of
an iterable for which a function returns true. In other words, it filters the elements of the iterable
based on a condition provided by a function.
# Syntax
filter(function, iterable)
# List of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Python 18
even_numbers = filter(lambda x: x % 2 == 0, numbers)
removes elements that do not satisfy the provided function's condition while a
filter() map()
# List of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Use filter() when you want to remove items from an iterable based on a condi
tion.
# Use map() when you want to transform items in an iterable.
# They can be combined for more complex operations, such as filtering and then
transforming the items.
File Handling
File handling in Python involves working with files to read from or write to them. Python provides built-
in functions and methods to handle files easily.
1. Opening a file
Python 19
'a' : Append (writes data to the end of the file)
seek(): This function points back to the first line and should be used when we are preforming both
write as well as read operations.
1. Opening a file
# Opens a file named example.txt in write mode. If the file doesn't exist, it is
created
2. Writing to a file
file.write("Hello, world!\n")
file.write("This is a file handling example.\n")
3. Closing a file
# Closes the file to ensure the data is saved and resources are freed
file.close()
Using the above 3 point we are going to write a code that opens a file, writes some text in it and then
closes the file.
Python 20
4. Reading from a file
5. Using with statements - Using the with statement is a preferred way to handle files because it
ensures that the file is properly closed after its suite finishes, even if an exception is raised.
# Appending to a file
with open("example.txt", "a") as file:
file.write("Appending a new line.\n")
Use open() to open a file in various modes ( 'r' , 'w' , 'a' , etc.).
Python 21
Use file.write() to write to a file.
Use the with statement to ensure that the file is properly closed after its block of code is executed.
Working with OS package which can be imported from the python library.
import os
Some of the functions that are most commonly used in the os packages are:-
print(os.getcwd())
3. listdir() - This is used to list all the files and directories present in our current path.
print(os.listdir('.'))
4. join() - This is used to join paths if the path of a file is present in the form of a list or a string.
dir_name "dir1"
file_name = "file1"
full_path = os.path.join(os.getcwd(), dir_name, file_name)
print(full_path)
dir_name "dir1"
file_name = "file1"
full_path = os.path.join(os.getcwd(), dir_name, file_name)
Python 22
6. isfile() - This is used to check whether the given path is a file or not.
7. isdir() - This is used to check whether the given path is a directory or not.
9. abspath() - This is used to get the complete path from the relative path, we should make sure that the
relative path that we are specifying must not have 2 same file names. This is also called as the absolute
path .
Exception Handling
Exception handling in Python is done using the try , except , else , and finally blocks. This mechanism
allows you to handle errors gracefully without crashing your program.
# Syntax
try:
# Code that might raise an exception
pass
except ExceptionType:
# Code that runs if an exception occurs
pass
else:
# Code that runs if no exception occurs
pass
finally:
# Code that runs no matter what (even if an exception occurs or not)
pass
6. Exception - This contains most of the common error in a class. (We should preferably be using this as
comapred to some common errors) [This should be written in the last of all the exceptions as it is the
parent of exceptions]
Python 23
except ZeroDivisionError as e:
print(f"Error: Division by zero is not allowed. Exception: {e}")
except TypeError as e:
print(f"Error: Unsupported operand type(s). Exception: {e}")
except Exception as e:
print(f"Error: Unsupported operand type(s). Exception: {e}")
else:
print(f"The result is {result}")
finally:
print("Execution completed.")
# Test cases
divide(10, 2) # Normal case
divide(10, 0) # Division by zero
divide(10, "a") # Unsupported operand type
class Dog:
# Class attribute
species = "Canis familiaris"
# Instance method
def description(self):
return f"{self.name} is {self.age} years old"
An object is an instance of a class. It is a specific realization of the class with actual values for the
attributes defined in the class.
Python 24
# You create an object by calling the class as if it were a function
my_dog = Dog("Buddy", 3)
Accessing the attributes and methods - We can access the attributes and methods of an object using
dot notation.
Here self is a instance variable, now we’ll be looking into instance methods
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.odometer_reading = 0
def get_descriptive_name(self):
return f"{self.year} {self.make} {self.model}"
def read_odometer(self):
return f"This car has {self.odometer_reading} miles on it."
Python 25
self.odometer_reading += miles
# Creating an object
my_car = Car("Toyota", "Corolla", 2020, "Blue")
__init__ Method: A special method called when an object is instantiated. It initializes the instance
attributes.
Inheritance
Inheritance is a fundamental concept in object-oriented programming (OOP) that allows one class (the
child or derived class) to inherit attributes and methods from another class (the parent or base class).
This promotes code reuse and logical hierarchy.
Single inheritance is a form of inheritance where a class (child or derived class) inherits from only one
parent (base) class. This is the simplest form of inheritance and helps in reusing the code of the parent
class in the child class.
# Parent Class
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
Python 26
def start_engine(self):
return f"The engine of the {self.brand} {self.model} is starting."
def stop_engine(self):
return f"The engine of the {self.brand} {self.model} is stopping."
# Child Class
class Car(Vehicle):
def __init__(self, brand, model, num_doors):
super().__init__(brand, model) # Initialize the parent class
self.num_doors = num_doors # Initialize the child class attribute
Methods ( start_engine and stop_engine ): Provide functionality related to the vehicle's engine.
Initializer ( __init__ method): Calls the parent class's __init__ method using super().__init__(brand,
model) to initialize brand and model . It also initializes a new attribute num_doors .
3. Creating Objects:
The start_engine , stop_engine , and play_music methods are available in the Car class.
The Car class also has the num_doors attribute initialized properly.
Maintainability: Makes the code easier to maintain by ensuring the parent class is properly initialized.
Flexibility: Allows the child class to add additional initialization code specific to its needs.
Python 27
Multiple inheritance is a feature in object-oriented programming where a class can inherit attributes
and methods from more than one parent class. This allows the child class to utilise the functionality of
multiple parent classes.
# Parent Class 1
class Engine:
def __init__(self, engine_type):
self.engine_type = engine_type
def start_engine(self):
return f"Starting {self.engine_type} engine."
# Parent Class 2
class Body:
def __init__(self, body_type):
self.body_type = body_type
def body_info(self):
return f"This is a {self.body_type} body."
# Child Class
class Car(Engine, Body):
def __init__(self, engine_type, body_type, brand):
Engine.__init__(self, engine_type)
Body.__init__(self, body_type)
self.brand = brand
def car_info(self):
return f"This is a {self.brand} car with a {self.engine_type} engine and a
# Object creation
my_car = Car("V8", "Sedan", "Toyota")
Polymorphism
Polymorphism is a key concept in object-oriented programming that refers to the ability of different
classes to be treated as instances of the same class through a common interface. This means that a
single function or method can work in different ways depending on the object it is acting upon.
Common in scenarios where you want to enforce a certain interface across multiple related classes,
such as in payment processing, file handling, or UI components.
Python 28
Method Overriding (Runtime Polymorphism): Achieved through inheritance, where a child class
provides a specific implementation of a method that is already defined in its parent class.
# Parent Class
class Animal:
def speak(self):
return "Animal sound"
# Child Classes
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
Both override the speak method of the Animal class to provide specific sounds for dogs and cats.
Takes an Animal object (or any object that has a speak method) as an argument and calls the speak
method.
Depending on the object passed ( Dog or Cat ), the method behaves differently, demonstrating
polymorphism.
Python 29
1. Flexibility: Allows a single function to work with objects of different types.
2. Code Reusability: You can write more generic code that can work with various types of objects
without modification.
3. Ease of Maintenance: Reduces redundancy by enabling a single method to work for various object
types.
Abstract Classes
An abstract class in Python is a class that cannot be instantiated on its own and is meant to be
subclassed. Abstract classes allow you to define a common interface for a group of related
classes, ensuring that all subclasses implement certain methods. Abstract classes serve as a
blueprint for other classes.
Abstract Method: A method that is declared in an abstract class but has no implementation.
Subclasses must override this method.
Abstract Class: A class containing one or more abstract methods. You cannot instantiate abstract
classes directly.
# Abstract Class
class PaymentProcessor(ABC):
@abstractmethod
def authorize_payment(self, amount):
pass
@abstractmethod
def process_payment(self, amount):
pass
# Derived Classes
class CreditCardProcessor(PaymentProcessor):
def authorize_payment(self, amount):
return f"Authorizing credit card payment of {amount}."
class PayPalProcessor(PaymentProcessor):
def authorize_payment(self, amount):
return f"Authorizing PayPal payment of {amount}."
Python 30
# Function Demonstrating Polymorphism
def execute_payment(processor, amount):
print(processor.authorize_payment(amount))
print(processor.process_payment(amount))
# Using CreditCardProcessor
cc_processor = CreditCardProcessor()
execute_payment(cc_processor, 100.00)
# Using PayPalProcessor
paypal_processor = PayPalProcessor()
execute_payment(paypal_processor, 50.00)
Defines a template for all payment processors with authorize_payment and process_payment
methods.
Both implement the authorize_payment and process_payment methods, each in a way that suits their
specific payment method.
Usage:
You can pass different payment processors to the execute_payment function, and it will work
seamlessly with any class that follows the PaymentProcessor blueprint.
Class Method
A class method is a method that is bound to the class and not the instance of the class. Class
methods can modify class state that applies across all instances of the class, and they have access
to the class itself (not the instance). They are defined using the @classmethod decorator and
take cls (referring to the class) as their first parameter instead of self .
Class methods are defined using the @classmethod decorator, and the first argument must
be cls (which stands for the class itself).
class Employee:
# Class attribute
raise_percentage = 1.05
Python 31
cls.raise_percentage = new_percentage
def apply_raise(self):
self.salary *= self.raise_percentage
4. from_string(cls, emp_string) : This is an alternative constructor that takes a string (like 'John-50000' ),
splits it, and creates a new Employee instance.
5. Calling the Class Method: We call Employee.set_raise_percentage(1.10) to set the raise percentage for
all employees.
6. Using Class Method for Alternative Constructor: The from_string method allows creating
an Employee object from a string without having to parse the string manually outside the class.
You use class methods when you need to work with class-level data, or when you want to provide
alternative constructors that operate on the class itself rather than instances. A common use case
is to provide functionality that applies to the class as a whole, rather than specific instances.
Static Method
In Python, a staticmethod is a method within a class that does not receive an implicit first argument
like self (which is for instance methods) or cls (which is for class methods). This means
Python 32
a staticmethod does not have access to the instance or class itself, but it can still be called on the
class or an instance of the class.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
2. Method Definition: The add method is defined with the @staticmethod decorator. It only takes
parameters a and b , and it performs a simple addition operation.
3. Calling the Static Method: You can call the static method using the class name MathUtils.add(5, 3) .
This is because static methods are not tied to a particular instance of the class.
4. Output: The method returns the result of the addition, which is then printed out.
Utility Functions: Use static methods for utility functions that perform operations independent of
the class or instance.
@Property
The @property decorator in Python is used to define methods in a class that act like attributes. This
allows you to create managed attributes where you can control how they are accessed and
modified, while keeping the syntax clean and intuitive.
class Rectangle:
def __init__(self, length, width):
self.__length = length
self.__width = width
@property
def length(self):
return self.__length
@length.setter
def length(self, length):
self.__length = length
Python 33
@property
def width(self):
return self.__width
@width.setter
def width(self, width):
self.__width = width
Private Attributes: __length and __width are private attributes intended to be inaccessible directly
from outside the class. This is a form of encapsulation that helps protect the internal state of the
object.
Properties: The @property decorator is used to define getter methods for length and width , allowing
you to access these attributes in a controlled manner. The corresponding setter methods allow you
to update these attributes while maintaining encapsulation.
Encapsulation: Using @property allows you to control how attributes are accessed and modified,
even though they are private. This approach ensures that the internal representation is hidden from
outside the class, while still providing a clear and intuitive interface for interacting with these
attributes.
Encapsulation
Encapsulation is one of the fundamental concepts in object-oriented programming. It refers to the
bundling of data (attributes) and methods (functions) that operate on the data into a single unit, or
class. Encapsulation also restricts direct access to some of the object’s components, preventing
accidental interference and misuse of the data.
In Python, encapsulation is implemented through access modifiers, which control the visibility of class
members (attributes and methods). The three levels of access control are:
1. Public: Accessible from anywhere. These attributes are accessible from outside the class. They can be
accessed directly using the object of the class.
class Car:
def __init__(self, make, model, year):
self.make = make # Public attribute
self.model = model # Public attribute
self.year = year # Public attribute
def display_info(self):
return f"Car: {self.year} {self.make} {self.model}"
Python 34
# Accessing public attributes directly
print(my_car.make) # Output: Toyota
print(my_car.model) # Output: Corolla
print(my_car.year) # Output: 2020
In the Car class, make , model , and year are public attributes because they are defined without any
leading underscores. This means they can be accessed and modified directly from outside the class,
as shown in the example.
2. Private: Accessible only within the class itself. Private attributes in Python are a way to prevent direct
access to class variables from outside the class. This is a form of encapsulation, which restricts
access to certain components, ensuring that data is not modified unintentionally.
In Python, private attributes are created by prefixing the attribute name with two underscores ( __ ).
class Car:
def __init__(self, make, model, year):
self.__make = make # Private attribute
self.__model = model # Private attribute
self.__year = year # Private attribute
def display_info(self):
return f"Car: {self.__year} {self.__make} {self.__model}"
def get_year(self):
return self.__year
Python 35
# Modifying private attributes via public methods
my_car.set_year(2021)
print(my_car.display_info()) # Output: Car: 2021 Toyota Corolla
Private Attributes: In the Car class, __make , __model , and __year are private attributes. The double
underscore ( __ ) at the beginning of the attribute name makes it private. This means that these
attributes cannot be accessed or modified directly from outside the class.
Encapsulation: By making attributes private, you encapsulate the internal state of the object, providing
controlled access via public methods ( get_year , set_year ). This ensures that the data is validated or
processed before being modified, preventing accidental or inappropriate changes.
Accessing Private Attributes: Attempting to access __make directly using my_car.__make will raise an
AttributeError , as these attributes are not accessible directly. Instead, public methods like get_year()
Name Mangling: Python internally changes the name of the private attribute to make it harder to
access them from outside. For example, __make is internally changed to _Car__make . However, this is not
intended for regular access but rather to prevent accidental modifications.
Encapsulation: Hides the internal representation of the object from the outside world.
Controlled Access: Allows for controlled access through getter and setter methods.
3. Protected: Accessible within the class and its subclasses. Protected attributes in Python are intended
to be accessed within the class itself and by subclasses, but not from outside these classes.
In Python, a protected attribute is indicated by prefixing the attribute name with a single underscore
( _ ).
class Car:
def __init__(self, make, model, year):
self._make = make # Protected attribute
self._model = model # Protected attribute
self._year = year # Protected attribute
def display_info(self):
return f"Car: {self._year} {self._make} {self._model}"
Python 36
super().__init__(make, model, year)
self._battery_capacity = battery_capacity # Additional protected attribute
def display_battery_info(self):
return f"{self._make} {self._model} has a {self._battery_capacity} kWh batt
Implementing all three types of Polymorphism which are Private, Protected and Public methods and
attributes
class BankAccount:
def __init__(self, account_holder, initial_balance):
# Public attribute
self.account_holder = account_holder
# Protected attribute (typically should not be accessed directly outside th
self._balance = initial_balance
# Private attribute (intended to be accessed only within the class)
self.__account_number = "1234567890" # In a real scenario, this would be g
Python 37
self._update_balance(amount)
return f"Deposited {amount}. New balance is {self._balance}."
else:
return "Deposit amount must be positive."
# Attempting to access private attributes and methods directly (will raise an Attri
# print(account.__account_number) # This will raise an AttributeError
# print(account.__get_account_number()) # This will raise an AttributeError
# Accessing private attribute using name mangling (not recommended, but possible)
print(account._BankAccount__account_number) # Output: 1234567890
Python 38
Example: account.account_holder , account.get_balance() .
__account_number is a private attribute, and __get_account_number is a private method. These are not
accessible directly from outside the class. Python uses name mangling to make them accessible
only within the class.
You can access them indirectly using a public method or through name mangling.
class UnderageError(Exception):
def __init__(self, age, minimum_age=18):
self.age = age
self.minimum_age = minimum_age
super().__init__(f"Age {age} is below the minimum allowed age of {minimum_a
def verify_age(age):
try:
if age < 18:
# Raise the custom exception if age is below 18
raise UnderageError(age)
else:
return "Access granted!"
except UnderageError as e:
return str(e) # Return the error message as a string
Inside the verify_age function, the try block checks the age.
The except block catches this specific exception and returns the error message as a string.
Python 39
Handling Inside the Function:
Now, the verify_age function handles the exception internally, so it returns a message whether the
age is sufficient or not.
Logging
Logging is a way to track events that happen when some software runs. The logging module in Python
allows you to track the flow of a program and diagnose issues by logging information to the console, a
file, or other destinations.
Logging has different severity levels (DEBUG, INFO, WARNING, ERROR, and CRITICAL).
import logging
formatter = logging.Formatter('%(asctime)s-%(levelname)s-%(name)s-%(message)s')
logging.basicConfig(filename='logFile.log', level=logging.DEBUG, filemode='a', form
3. WARNING - An indication that something unexpected happened or indicative of some problem in the near
future (eg: disk space is low). The software is still working as expected.
4. ERROR - Due to a more serious problem, the software has not been able to perform some function.
5. CRITICAL - A very serious error, indication that the program itself may be unable to continue running.
import logging
# By default when we run this file then the level will display as root, but when th
logger = logging.getLogger(__name__)
Python 40
# Creating a filehandler to store all the logs into a log file
fh = logging.FileHandler('SaveLog.log')
# Add the file handler to the logger (basically linking the custom logger and the c
logger.addHandler(fh)
Let’s create a simple password checker example with logging. The password checker will check if the
password meets certain criteria (length, presence of numbers, etc.) and log different levels of
messages depending on the result.
import logging
# Step 2: Set the log level (DEBUG means it will log everything from DEBUG and abov
logger.setLevel(logging.DEBUG)
# Step 3: Create a formatter that specifies how the log messages should look
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s
if len(password) < 8:
logger.warning("Password is too short!")
return "Password too short!"
Python 41
return "Password must contain at least one uppercase letter!"
logger.info("Password is valid.")
return "Password is valid!"
Process - A process is an instance of a program that is being executed. A process runs independently
and is not interrupted by other processes.
Thread - A thread in computing is the smallest unit of execution within a process. Threads allow a
program to perform multiple tasks concurrently (at the same time) within the same program. Each
thread runs independently but shares the same memory space and resources of the process it belongs
to.
Multithreading
We usually use multi-threading when we perform I/O operations. Here the I/O operations can be file
operations, network requests etc.
import threading
import time
def print_numbers():
for i in range(1, 6):
print(f"Number: {i}")
time.sleep(2) # Simulate a time-consuming task
def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Letter: {letter}")
time.sleep(2) # Simulate a time-consuming task
Python 42
# Start the threads
thread1.start()
thread2.start()
threading.Thread() : Creates a new thread where target specifies the function that the thread will run.
Here, print_numbers and print_letters are assigned to separate threads.
start() : Starts the thread and allows it to run concurrently with other threads.
join() : Ensures that the main program waits for the threads to finish their execution before moving on
time.sleep() : Simulates a time-consuming task by causing the thread to pause for 1 second after each
operation.
The main difference between a thread pool executor and the code above is that, in the above code we
are manually defining the number of thread and for that we are defining individual variables for that.
But in the case of thread pool executor, we are just defining the threads to be used in the program and
that is it. We need not define the variables for each and every thread.
def print_square(number):
"""Function to print the square of a number."""
print(f"Calculating square of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number
print(f"Square of {number} is {result}")
return result
def print_cube(number):
"""Function to print the cube of a number."""
print(f"Calculating cube of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number * number
print(f"Cube of {number} is {result}")
return result
def main():
numbers = [1, 2, 3, 4, 5]
Python 43
# Create a ThreadPoolExecutor with a pool of 3 threads
with ThreadPoolExecutor(max_workers=3) as executor:
# Submit tasks to the executor
futures = []
for number in numbers:
futures.append(executor.submit(print_square, number))
futures.append(executor.submit(print_cube, number))
if __name__ == "__main__":
main()
Define Functions:
Create a ThreadPoolExecutor:
Submit Tasks:
submits tasks to the thread pool. Each task runs the specified function
executor.submit(func, *args)
Retrieve Results:
as_completed(futures) yields futures as they complete, allowing you to process results or handle
exceptions.
Multiprocessing
We usually use multi-processing for CPU bound tasks, like when we need more computation power,
like a complex calculation.
While implementing multiprocessing we must use if __name__ == "__main__" , if we do not use this, we will
be prompted with an error.
import multiprocessing
import time
def print_numbers():
Python 44
for i in range(1, 6):
print(f"Number: {i}")
time.sleep(1) # Simulate a time-consuming task
def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Letter: {letter}")
time.sleep(1) # Simulate a time-consuming task
def main():
# Create processes
process1 = multiprocessing.Process(target=print_numbers)
process2 = multiprocessing.Process(target=print_letters)
# Start processes
process1.start()
process2.start()
if __name__ == "__main__":
main()
Importing the Module: import multiprocessing provides access to the multiprocessing functionality.
Defining Functions: print_numbers and print_letters are functions that will be executed by different
processes.
Creating Processes:
function.
Starting Processes:
Joining Processes:
process1.join() and process2.join() ensure that the main program waits for these processes to
The main difference between a process pool executor and the code above is that, in the above code
we are manually defining the number of thread and for that we are defining individual variables for
Python 45
that. But in the case of process pool executor, we are just defining the process to be used in the
program and that is it. We need not define the variables for each and every thread.
def print_square(number):
"""Function to print the square of a number."""
print(f"Calculating square of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number
print(f"Square of {number} is {result}")
return result
def print_cube(number):
"""Function to print the cube of a number."""
print(f"Calculating cube of {number}")
time.sleep(1) # Simulate a time-consuming task
result = number * number * number
print(f"Cube of {number} is {result}")
return result
def main():
numbers = [1, 2, 3, 4, 5]
if __name__ == "__main__":
main()
Define Functions:
Python 46
Create a ThreadPoolExecutor:
Submit Tasks:
executor.submit(func, *args) submits tasks to the thread pool. Each task runs the specified function
with the provided arguments.
Retrieve Results:
as_completed(futures) yields futures as they complete, allowing you to process results or handle
exceptions.
Python 47