The Python Language Notes :
Unit – 𝐈𝐈𝐈
Oops Concept –
OOP (Object-Oriented Programming) is a
programming paradigm that revolves
around the concept of "objects." These
objects are instances of classes, which
allow for organizing code and structuring
programs more efficiently.
➢Class: A class is a blueprint for creating
objects. It defines properties (attributes) and
behaviors (methods) that the objects created
from the class will have.
Syntax –
class ClassName:
# class definition
1|Page
Example:
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def make_sound(self):
print(f"{self.name} says {self.sound}")
# Example usage
dog = Animal("Dog", "Woof") # Object
dog.make_sound() # Output: Dog says Woof
➢Object: An object is an instance of a class. It
has a specific set of attributes and methods
defined by the class.
Syntax –
objectName = ClassName()
Example:
cat = Animal("Cat", "Meow")
cat.make_sound() # Output: Cat says Meow
2|Page
➢Types of Functions: In Python, attributes are
values or properties associated with objects.
They store information about the object and
can be accessed and modified through dot
notation (object.attribute). Attributes are often
defined in classes and can be of various types
(like integers, strings, lists, etc.).
Types of Attributes in Python:
1. Instance Attributes–
• These are attributes unique to each instance of
a class.
• They are typically defined within the __init__
method, which is called when an object is
created.
• Instance attributes are specific to the instance
and can have different values for each instance.
Example:
class Person:
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute
3|Page
person1 = Person("Uttam", 30)
person2 = Person("Vaishali", 25)
print(person1.name) # Output: Uttam
print(person2.age) # Output: 25
2. Class Attributes –
• These attributes are shared across all instances
of a class.
• They are defined directly in the class, outside of
any method, and are the same for all instances
unless specifically overridden by an instance.
Example:
class Dog:
species = "Canine" # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
dog1 = Dog("Rex")
dog2 = Dog("Buddy")
print(dog1.species) # Output: Canine
print(dog2.species) # Output: Canine
4|Page
3. Dynamic Attributes –
Attributes can be dynamically added to an object at
runtime, even after the object has been created.
Example:
class Book:
def __init__(self, title):
self.title = title
my_book = Book("Python Programming")
print(my_book.title) # Output: Python Programming
# Adding a dynamic attribute
my_book.author = "John Doe"
print(my_book.author) # Output: John Doe
➢Overloading: Method overloading allows a
class to define multiple methods with the same
name but different parameters. Python does
not support method overloading in the
traditional sense (like in languages such as Java
or C++), but it can be mimicked in various ways.
5|Page
Python achieves this behavior through default
arguments or using *args and **kwargs to
handle varying numbers of arguments.
Example of Simulating Method Overloading Using
Default Arguments:
class MathOperations:
def add(self, a, b=0, c=0):
return a + b + c
math_op = MathOperations()
# Calling with two arguments
print(math_op.add(5, 10)) # Output: 15
# Calling with three arguments
print(math_op.add(5, 10, 20)) # Output: 35
Example of Simulating Method Overloading Using
*args:
class MathOperations:
def add(self, *args):
return sum(args)
math_op = MathOperations()
# Calling with two arguments
6|Page
print(math_op.add(5, 10)) # Output: 15
# Calling with three arguments
print(math_op.add(5, 10, 20)) # Output: 35
# Calling with any number of arguments
print(math_op.add(5, 10, 20, 30)) # Output: 65
➢Overriding: Method overriding happens
when a child class provides its own
implementation of a method that is already
defined in the parent class. The child class's
version of the method is executed instead of
the parent's method when called on an
instance of the child class.
Example of Method Overriding:
class Animal:
def sound(self):
print("Animal makes a sound")
class Dog(Animal):
#overriding
def sound(self):
print("Dog barks")
7|Page
dog = Dog()
dog.sound() # Output: Dog barks (Overrides the parent method)
• Using super() with Overriding: Sometimes, you
may want to use the parent class's method as
well as extend its functionality. This is achieved
using super().
Example:
class Animal:
def sound(self):
print("Animal makes a sound")
class Dog(Animal):
def sound(self):
super().sound() # Call the parent class method
print("Dog barks")
dog = Dog()
dog.sound()
# Output:
# Animal makes a sound
# Dog barks
8|Page
➢Data Hiding: Data Hiding in Python refers to
the concept of restricting access to the internal
attributes or methods of a class to protect data
integrity and provide controlled access. While
Python doesn't enforce strict access control
like some other programming languages (such
as Java or C++), it provides mechanisms to
indicate that certain attributes or methods
should not be accessed directly from outside
the class.
Mechanism for Data Hiding:
1. Public Attributes–
• By default, all attributes and methods in Python
are public, meaning they can be accessed and
modified from outside the class.
• Public attributes do not implement data hiding.
Example:
class Employee:
def __init__(self, name, salary):
self.name = name # Public attribute
self.salary = salary # Public attribute
9|Page
emp = Employee("Uttam", 5000)
print(emp.name) # Output: Uttam (Accessible directly)
print(emp.salary) # Output: 5000 (Accessible directly)
2. Protected Attributes –
• Attributes prefixed with a single underscore (_)
are considered protected. By convention, these
are meant to be accessed only within the class
and its subclasses.
• Python does not prevent access to protected
attributes but signals that they are intended for
internal use.
Example:
class Employee:
def __init__(self, name, salary):
self._name = name # Protected attribute
self._salary = salary # Protected attribute
emp = Employee("Uttam", 5000)
print(emp._name) # Output: Uttam (Can be accessed, but
discouraged)
print(emp._salary) # Output: 5000 (Can be accessed, but
discouraged)
10 | P a g e
3. Private Attributes –
• Attributes prefixed with double underscores
(__) are private and are not directly accessible
from outside the class.
• This is known as name mangling, where Python
changes the name of the attribute to include
the class name, making it harder to access
accidentally or intentionally.
Example:
class Employee:
def __init__(self, name, salary):
self.__name = name # Private attribute
self.__salary = salary # Private attribute
def get_salary(self):
return self.__salary # Method to access private attribute
emp = Employee("Uttam", 5000)
# print(emp.__name) # This will raise an AttributeError
print(emp.get_salary()) # Output: 5000 (Accessed via public
method)
11 | P a g e
❖ Key Points –
Encapsulation: Data hiding is part of
encapsulation, which ensures that an object
controls its own data and only exposes what is
necessary.
Data Integrity: By hiding internal data, you
prevent external code from making unintended
modifications to sensitive data.
Abstraction: Hiding data helps in abstracting the
internal implementation details from the outside
world and only providing what is needed.
➢Regular Expressions: Regular expressions
(regex) in Python are powerful tools for pattern
matching and searching within strings. The re
module provides all the functionalities to work
with regular expressions. Using regex, you can
find, extract, replace, and manipulate text
patterns easily.
12 | P a g e
To work with regular expressions, you first
need to import the re module:
import re
Basic Functions in re Module:
1. re.match(): Determines if the regex matches
at the beginning of the string.
2. re.search(): Scans through a string to find the
first location where the regex patter produces
a match.
3. re.findall(): Returns a list of all matches in the
string.
4. re.sub(): Replaces occurrences of the pattern
with a replacement string.
5. re.split(): Splits the string by occurrences of
the pattern.
6. re.compile(): Compiles a regular expression
pattern into a regex object, which can be
reused.
13 | P a g e
➢Match Function: The match() function in
Python, provided by the re (regular expression)
module, attempts to match a pattern at the
beginning of a string. If the pattern matches
the start of the string, it returns a match object
otherwise, it returns None.
The key thing to remember is that re.match()
only checks for a match at the start of the
string. If you need to search throughout the
string for a match (i.e., not just at the
beginning), you should use re.search().
Syntax:
re.match(pattern, string, flags=0)
• pattern: The regex pattern to match.
• string: The target string where the search will be
performed.
• flags (optional): Special flags that change how
the pattern is interpreted (e.g., re.IGNORECASE
for case-insensitive matching).
14 | P a g e
Match Object:
If a match is found, a match object is returned.
The match object has the following methods:
• group(): Returns the part of the string where
the pattern matches.
• start(): Returns the starting position of the
match.
• end(): Returns the ending position of the
match.
• span(): Returns a tuple of the start and end
positions of the match.
Example 1: Basic Matching at the Start of the String
import re
pattern = r"\d+" # Matches one or more digits
text = "123abc456"
match = re.match(pattern, text)
if match:
print(f"Match found: {match.group()}") # Output: 123
else:
print("No match")
15 | P a g e
In this example, the pattern \d+ matches one or
more digits, and since the string starts with 123, a
match is found.
Example 2: No Match (Pattern Does Not Start at
Beginning)
import re
pattern = r"\d+" # Matches one or more digits
text = "abc123"
match = re.match(pattern, text)
if match:
print(f"Match found: {match.group()}")
else:
print("No match") # Output: No match
In this example, the string abc123 does not start
with a digit, so re.match() returns None.
Example 3: Using Flags (Case-Insensitive Matching)
import re
16 | P a g e
pattern = r"hello"
text = "Hello World"
match = re.match(pattern, text, re.IGNORECASE) # Case-
insensitive match
if match:
print(f"Match found: {match.group()}") # Output: Hello
else:
print("No match")
Here, the re.IGNORECASE flag allows the pattern to
match regardless of case, so the pattern "hello"
matches "Hello" in the string.
➢Search Function: The search() function in
Python, provided by the re module, is used to
search for a pattern anywhere in the target
string, not just at the beginning. Unlike
re.match(), which only checks if the pattern
matches from the start of the string,
re.search() scans the entire string for a match
and returns the first occurrence it finds..
17 | P a g e
Syntax:
re.search(pattern, string, flags=0)
• pattern: The regular expression pattern to search
for.
• string: The string where the search is performed.
• flags (optional): Modifiers like re.IGNORECASE,
re.MULTILINE, etc., that affect how the pattern is
interpreted.
Example 1: Basic Usage of re.search()
import re
pattern = r"\d+" # One or more digits
text = "There are 123 apples and 456 bananas."
match = re.search(pattern, text)
if match:
print(f"Match found: {match.group()}") # Output: 123
else:
print("No match found")
Here, re.search() finds the first sequence of digits
("123") in the string, regardless of its position in the
string.
18 | P a g e
Example 2: No Match Found
import re
pattern = r"\d{4}" # Matches a sequence of exactly four digits
text = "There are 123 apples."
match = re.search(pattern, text)
if match:
print(f"Match found: {match.group()}")
else:
print("No match found") # Output: No match found
In this case, since there is no sequence of exactly
four digits in the string, re.search() returns None.
Example 3: Using Flags (Case-Insensitive Search)
import re
pattern = r"apple"
text = "There are many APPLES in the basket."
match = re.search(pattern, text, re.IGNORECASE) # Case-
insensitive match
if match:
print(f"Match found: {match.group()}") # Output: APPLES
else:
print("No match found")
19 | P a g e
Here, the re.IGNORECASE flag allows the pattern to
search regardless of case.
Matching vs Searching –
Aspect Matching Searching
(re.match()) (re.search())
Checks if the Checks for the
Definition
pattern appears at pattern anywhere
the start of the in the string
string
Function re.match() re.search()
Behavior Returns a match
Returns a match for the first
only if the pattern occurrence
starts the string anywhere in the
string
When you need to When you need to
Use Case
validate the find a pattern in
beginning of a any part of the
string string
➢Modifiers: In Python, modifiers (or flags) are
used in regular expressions (regex) to control
how patterns are matched. They can change
the default behavior of pattern matching, such
20 | P a g e
as making it case-insensitive, multiline-aware,
etc. These modifiers are usually passed as a
third argument to functions like re.search(),
re.match(), or can be embedded directly in the
regex pattern using specific syntax.
Common Python Regex Modifiers:
1. re.IGNORECASE (re.I) –
• Makes the pattern matching case-insensitive,
so letters match regardless of their case.
Example:
import re
pattern = r"hello"
text = "Hello world"
match = re.search(pattern, text, re.IGNORECASE)
if match:
print("Match found:", match.group()) # Output: Match found:
Hello
2. re.MULTILINE (re.M) –
21 | P a g e
• Changes the behavior of ^ and $ to match the
start and end of each line, not just the start and
end of the whole string.
Example:
import re
pattern = r"^abc"
text = """abc
def
abc"""
match = re.search(pattern, text, re.MULTILINE)
if match:
print("Match found:", match.group()) # Output: abc (on the
first line)
3. re.DOTALL (re.S) –
• Allows the dot (.) to match all characters,
including newlines (\n). Normally, (.) does not
match newline characters.
Example:
import re
pattern = r"abc.*def"
22 | P a g e
text = "abc\ndef"
match = re.search(pattern, text, re.DOTALL)
if match:
print("Match found:", match.group()) # Output: abc\ndef
4. re.ASCII (re.A) –
• Makes \w, \W, \b, \B, \d, \D, \s, and \S match
only ASCII characters, rather than the full
Unicode range.
Example:
import re
pattern = r"\w+" # Word characters (ASCII only)
text = "Café 123"
match = re.search(pattern, text, re.ASCII)
if match:
print("Match found:", match.group()) # Output: Caf
Summary of Modifiers:
Modifier Short Form Description
re.IGNORECASE re.I Case-insensitive matching.
re.MULTILINE re.M ^ and $ match at the start/end of each
line.
re.DOTALL re.S Dot (.) matches any character, including
newlines.
23 | P a g e
re.VERBOSE re.X Allows comments and whitespace in the
pattern.
re.ASCII re.A Restricts matching to ASCII characters.
re.LOCALE re.L Locale-aware matching for \w, \d, \s.
re.UNICODE re.U Enables Unicode-aware matching. (Default
in Python 3)
Exceptions –
In Python, exceptions are errors that occur
during the execution of a program. When
an exception is raised, normal program
flow is interrupted, and the interpreter
looks for a way to handle the error. If no
handling mechanism (like a try-except
block) is found, the program crashes with a
traceback error.
➢Try Statement: The try statement in Python
is used for exception handling, allowing you to
test a block of code for potential errors and
handle those errors gracefully without
crashing the program.
24 | P a g e
Syntax –
The try block works together with one or more except blocks,
an optional else block, and an optional finally block.
try:
# Code that may raise an exception
except SomeException as e:
# Code to handle the exception
else:
# Code that runs if no exception occurs (optional)
finally:
# Code that always runs (optional, cleanup code)
Components of the try Statement:
1. try Block–
• The try block contains code that might throw
an exception. If an exception occurs within the
try block, Python immediately looks for an
except block to handle it.
Example:
try:
result = 10 / 0 # This will raise a ZeroDivisionError
except ZeroDivisionError:
print("Division by zero is not allowed!")
25 | P a g e
2. except Block–
• The except block is used to handle the
exception raised in the try block. You can
specify the type of exception to catch (e.g.,
ZeroDivisionError, ValueError), or you can use a
general except to catch any exception.
Example 1 (Catching a specific exception):
try:
value = int("abc")
except ValueError:
print("A ValueError occurred!") #Output: "A ValueError
occurred!"
Example 2 (Catching multiple exception):
try:
value = int("abc")
except (ValueError, TypeError):
print("An error occurred!") # Output: "An error occurred!"
3. else Block–
• The else block executes only if no exception
occurs in the try block. It is useful for separating
the error-prone code from the code that should
run if everything goes well.
26 | P a g e
Example:
try:
result = 10 / 2 # No exception will occur here
except ZeroDivisionError:
print("Division by zero!")
else:
print("Division successful!") # Runs if no exception
#Output: "Division successful!"
4. finally Block–
• The finally block always runs, whether an
exception occurs or not. It is generally used for
cleanup actions, such as closing files or
releasing resources.
Example:
try:
file = open("example.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found!")
finally:
file.close() # Ensures file closure, whether an exception occurs
or not
27 | P a g e
➢Exception Propagation: Exception
propagation refers to how exceptions are
passed through the call stack in Python until
they are either handled or cause the program
to terminate. When an exception occurs, it
"propagates" up the call stack, moving from
the current function to the function that
called it, and so on, until it is either handled
by a matching except block or reaches the top
of the call stack (i.e., the main program),
where it will terminate the program if still
unhandled.
Example:
def divide(a, b):
return a / b # No exception handling here
def calculate():
return divide(10, 0) # This will cause an exception
def main():
calculate() # Calling the calculate function
main()
28 | P a g e
In this example:
• The divide() function attempts to divide 10 by 0, which raises
a ZeroDivisionError.
• Since divide() does not handle the exception, it propagates to
the calculate() function.
• calculate() also doesn't handle the exception, so it further
propagates to the main() function.
• Since main() also doesn't handle the exception, it reaches the
Python interpreter, and the program terminates with a
traceback.
➢User-Defined Exceptions: In Python, you can
create your own custom exceptions (user-
defined exceptions) by creating a new class
that inherits from the built-in Exception class.
These exceptions are useful when you want to
handle specific types of errors in your code that
are not covered by the built-in exceptions.
Creating a User-Defined Exception –
To define a user-defined exception, you need to subclass
the built-in Exception class or one of its subclasses. You
can add additional attributes and methods to your
exception class if needed.
Example:
29 | P a g e
# Define a custom exception
class CustomError(Exception):
pass
# Raise the custom exception
try:
raise CustomError("This is a custom error message!") #Output:
This is a custom error message!
except CustomError as e:
print(e)
➢The Raise Statement: The raise statement in
Python is used to explicitly trigger an
exception. You can either raise built-in
exceptions like ValueError, TypeError, or create
and raise your own custom exceptions.
Syntax –
raise [ExceptionType([message])]
• ExceptionType: The type of exception to raise, like
ValueError, TypeError, etc.
• message: An optional argument that provides
additional details about the error.
Basic Usage of raise:
30 | P a g e
You can use raise to throw an exception in any part
of your code.
Example: Raising a Built-in Exception
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero!")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(e)
#Output: Cannot divide by zero!
In this example:
• A ZeroDivisionError is raised if the divisor (b) is 0.
• The exception is caught in the except block and prints a
custom error message.
Raising Custom Exceptions:
You can also use the raise statement to throw
custom exceptions that you've defined.
Example: Raising a User-Defined Exception
class NegativeAgeError(Exception):
pass
31 | P a g e
def check_age(age):
if age < 0:
raise NegativeAgeError("Age cannot be negative!")
print(f"Age is: {age}")
try:
check_age(-1)
except NegativeAgeError as e:
print(e)
#Output: Age cannot be negative!
In this example:
• A custom exception NegativeAgeError is defined, which
inherits from Exception.
• The raise statement is used to throw this exception when an
invalid age (negative) is encountered.
Raising Exceptions with from Keyword:
In Python, you can use the raise ... from syntax to
raise one exception while preserving the context of
a previous exception. This is helpful when you want
to raise a new exception but keep a reference to the
original exception for debugging purposes.
Example: Raising a User-Defined Exception
def example_func():
try:
x=1/0
32 | P a g e
except ZeroDivisionError as e:
raise ValueError("Something went wrong!") from e
try:
example_func()
except ValueError as e:
print(e)
print(f"Original cause: {e.__cause__}")
#Output: Something went wrong!
Original cause: division by zero
In this example:
• A ZeroDivisionError occurs and is caught.
• A ValueError is raised using the from keyword, preserving the
original ZeroDivisionError as the cause.
• The __cause__ attribute is used to display the original
exception that led to the new one.
2. Class Attributes –
• These attributes are shared across all instances
of a class.
• They are defined directly in the class, outside of
any method, and are the same for all instances
unless specifically overridden by an instance.
Example:
class Dog:
species = "Canine" # Class attribute
33 | P a g e
def __init__(self, name):
self.name = name # Instance attribute
dog1 = Dog("Rex")
dog2 = Dog("Buddy")
print(dog1.species) # Output: Canine
print(dog2.species) # Output: Canine
3. Dynamic Attributes –
Attributes can be dynamically added to an object at
runtime, even after the object has been created.
Example:
class Book:
def __init__(self, title):
self.title = title
my_book = Book("Python Programming")
print(my_book.title) # Output: Python Programming
# Adding a dynamic attribute
my_book.author = "John Doe"
print(my_book.author) # Output: John Doe
34 | P a g e