INTERNAL ASSIGNMENT
NAME TUBA HASAN
ROLL NUMBER 2314101583
PROGRAM BCA
SEMESTER 5
COURSE NAME PYTHON PROGRAMMING
COURSE CODE DCA3104
SESSION APRIL 2025
SET : I
ANS 1 : a) Mutable and Immutable Datatypes in Python
In Python, the distinction between mutable and immutable datatypes is crucial and relates to whether
the content of an object can be changed after it is created.
Immutable Datatypes:Immutable objects cannot be modified after they are created. If you perform
an operation that seems to change an immutable object, Python actually creates a new object with the
desired changes, and the original object remains untouched. This property makes immutable objects
suitable for use as dictionary keys or elements in a set, as their hash value never [Link]
of immutable datatypes include:
● Numbers: int, float, complex
● Strings: str
● Tuples: tuple
● Frozensets: frozenset
# Example of immutable string
s = "hello"
print(s) # Output: hello
s = s + " world" # A new string object is created
print(s) # Output: hello world
In the example above, s initially points to the string "hello". When s + " world" is executed, a new
string "hello world" is created, and s is then made to point to this new [Link] original "hello"
object is unchanged in memory (and eventually garbage collected if no other references exist).
Mutable Datatypes:Mutable objects, on the other hand, can be modified after they are created.
Operations that change a mutable object directly alter the object in memory without creating a new
one. This means that if multiple variables refer to the same mutable object, changes made through
one variable will be visible through all other variables referencing that same object.7 Examples of
mutable datatypes include:
Lists: list
Dictionaries: dict
Sets: set
# Example of mutable list
my_list = [1, 2, 3]
print(my_list) # Output: [1, 2, 3]
my_list.append(4) # The existing list object is modified
print(my_list) # Output: [1, 2, 3, 4]
Here, append(4) directly modifies the my_list object in place. No new list object is created for this
operation.
b) How do Membership and Identity Operators Work?Python's membership and identity
operators provide ways to check for the presence of elements within sequences and to compare
whether two variables refer to the exact same object in memory.
Membership Operators:Membership operators are used to test whether a value or variable is found
in a sequence (like strings, lists, tuples, or sets).Python provides two membership operators:
● in: Evaluates to True if the specified value is found in the sequence, otherwise False.
● not in: Evaluates to True if the specified value is not found in the sequence, otherwise False.
These operators are highly intuitive and commonly used for checking the existence of an
element without iterating through the entire sequence manually.
Example of Membership Operators:
my_fruits = ["apple", "banana", "cherry"]
print("apple" in my_fruits) # Output: True
print("grape" in my_fruits) # Output: False
print("orange" not in my_fruits) # Output: True
my_string = "hello world" print("world" in my_string) # Output: True
In the example, "apple" in my_fruits checks if the string "apple" exists as an element within the
my_fruits list.
Identity Operators:Identity operators are used to compare the memory locations of two objects, i.e.,
whether two variables refer to the exact same object.10 Python provides two identity operators:
● is: Evaluates to True if both variables point to the same object in memory, otherwise False.
● is not: Evaluates to True if both variables do not point to the same object in memory,
otherwise False.
It's important to distinguish identity operators from equality operators (== and !=). Equality operators
check if two objects have the same value, whereas identity operators check if they are the same
object.
Example of Identity Operators:
list1 = [1, 2, 3] list2 = [1, 2, 3] list3 = list1
print(list1 == list2) # Output: True (values are equal)
print(list1 is list2) # Output: False (different objects in memory)
print(list1 is list3) # Output: True (list3 refers to the same object as list1)
a = 10 b = 10 print(a is b) # Output: True (for small integers, Python often optimizes by using the
same object)
c = 257 d = 257 print(c is d) # Output: False (for larger integers, distinct objects are typically
created)
In the example, list1 is list2 is False because even though they have the same content, they are two
distinct list objects in memory. However, list1 is list3 is True because list3 = list1 makes list3 directly
reference the exact same list object as list1.
ANS 2: a) How instance variables are different from class variables?
In object-oriented programming with Python, both instance variables and class variables are used to
store data within classes and objects, but they differ significantly in terms of their ownership, scope,
and how they are accessed and modified.
Instance Variables:Instance variables are unique to each instance (object) of a class. They are
defined within the methods of a class (typically in the __init__ method using
self.variable_name).Each time you create a new object from a class, that object gets its own separate
copy of all the instance variables. Changes made to an instance variable in one object do not affect
the instance variables of other objects. They are ideal for storing data that varies from one object to
another, such as an individual's name, age, or a specific product's price.
class Dog: def __init__(self, name, age):
[Link] = name # Instance variable [Link] = age # Instance variable
dog1 = Dog("Buddy", 3)dog2 = Dog("Lucy", 5)
print([Link]) # Output: Buddy print([Link]) # Output: Lucy
[Link] = 4 # Modifies only dog1's age
print([Link]) # Output: 4
print([Link]) # Output: 5 (Lucy's age is unchanged)
Here, name and age are instance variables. dog1 and dog2 each have their own name and age values.
Class Variables: Class variables, also known as static variables, are shared by all instances of a
[Link] are defined directly within the class body, outside of any methods. There is only one copy
of a class variable, regardless of how many objects are created from the class. If a class variable is
modified, that change is reflected across all instances of the class. They are typically used for data
that is common to all objects of a certain type, such as a species name, a universal constant, or a
default value.
class Dog:
species = "Canis familiaris" # Class variable
def __init__(self, name, age):
[Link] = name [Link] = age
dog1 = Dog("Buddy", 3) dog2 = Dog("Lucy", 5)
print([Link]) # Output: Canis familiaris print([Link]) # Output: Canis familiaris
[Link] = "Domestic Dog" # Modifies the class variable
print([Link]) # Output: Domestic Dog (reflects change for all instances)
print([Link]) # Output: Domestic Dog
Here, species is a class variable. Both dog1 and dog2 share the same species value, and a change to
[Link] affects both.
b) Explain the use of following string functions with examples: - upper(), lower(), isdigit(),
isalpha(), split(), join()
Python provides a rich set of built-in string methods for common text manipulation tasks.
● upper():
The upper() method returns a new string where all the characters in the original string are
converted to uppercase. It does not modify the original string, as strings are immutable.
text = "Hello World"upper_text = [Link]() print(upper_text) # Output: HELLO WORLD
print(text) # Output: Hello World (original remains unchanged)
● lower():
Similar to upper(), the lower() method returns a new string where all characters in the original
string are converted to lowercase.
text = "Python Programming" lower_text = [Link]() print(lower_text) # Output: python
programming
● isdigit():
The isdigit() method checks if all characters in the string are digits (0-9). It returns True if all
characters are digits and there is at least one character, otherwise False. It's useful for
validating numeric input.
s1 = "12345" s2 = "12.34" s3 = "abc" print([Link]()) # Output: True print([Link]()) #
Output: False (contains '.') print([Link]()) # Output: False
● isalpha():
The isalpha() method checks if all characters in the string are alphabetic (letters from the
alphabet). It returns True if all characters are letters and there is at least one character,
otherwise False.
s1 = "Python" s2 = "Python123" s3 = "Hello World" # Contains space
print([Link]()) # Output: True print([Link]()) # Output: False (contains digits)
print([Link]()) # Output: False (contains space)
● split():
The split() method splits a string into a list of substrings based on a specified [Link] no
delimiter is specified, it splits by whitespace. It's incredibly useful for parsing sentences or
data separated by specific characters.
sentence = "Python is fun to learn" words = [Link]() # Splits by whitespace by default
print(words) # Output: ['Python', 'is', 'fun', 'to', 'learn'] data = "apple,banana,cherry" fruits
= [Link](',') # Splits by comma print(fruits) # Output: ['apple', 'banana', 'cherry']
● join():
The join() method is the inverse of split(). It concatenates a sequence of strings (e.g., a list or
tuple of strings) into a single string. The string on which join() is called acts as the separator
between the elements of the sequence.
words_list = ['Hello', 'World', 'Python'] joined_space = " ".join(words_list)
print(joined_space) # Output: Hello World Python data_parts = ['user', 'data', 'information']
joined_underscore = "_".join(data_parts) print(joined_underscore) # Output:
user_data_information empty_string_join = "".join(['a', 'b', 'c']) print(empty_string_join) #
Output: abc
ANS 3: a) What is a list? Explain insert() and append() methods with example.
In Python, a list is a versatile and fundamental compound datatype that represents an ordered,
mutable collection of items. Lists are defined by enclosing elements in square brackets [], with
elements separated by commas. The items within a list do not need to be of the same datatype; a
single list can contain integers, strings, floats, other lists, or even custom objects. Being mutable
means that after a list is created, its elements can be changed, added, or removed. Lists are dynamic,
meaning their size can grow or shrink as needed, making them highly flexible for storing and
manipulating collections of [Link] are commonly used to store sequences of data where the order
matters and elements might be frequently modified.
Two common methods for adding elements to a list are insert() and append():
● append(item):
The append() method adds a single item to the end of the existing list.5 It's the simplest way
to add an element and is generally efficient as it doesn't require re-indexing existing elements.
my_list = [10, 20, 30]
my_list.append(40)
print(my_list) # Output: [10, 20, 30, 40]
● insert(index, item):
The insert() method adds an item at a specific index within the list. The index argument
specifies the position where the item should be inserted. Existing elements from that index
onwards are shifted to the right to accommodate the new item. This can be less efficient for
very large lists, especially when inserting near the beginning, as it involves shifting many
elements.
my_list = [10, 30, 40]
my_list.insert(1, 20) # Inserts 20 at index 1
print(my_list) # Output: [10, 20, 30, 40]
my_list.insert(0, 5) # Inserts 5 at the beginning (index 0)
print(my_list) # Output: [5, 10, 20, 30, 40]
b) How to create private and protected variables in class? Explain its importance.
In Python, the concepts of "private" and "protected" variables are implemented by convention rather
than strict access modifiers like in some other languages (e.g., Java, C++). Python relies on a naming
convention combined with a mechanism called name mangling to suggest encapsulation.
Protected Variables (Convention): Protected variables are intended to be accessible within the class
itself and by its subclasses, but not directly from outside the class.6 In Python, you denote a
protected variable by prefixing its name with a single underscore (_).
Example: _variable_name
class MyClass:
def __init__(self):
self._protected_var = "I am protected"
obj = MyClass()
print(obj._protected_var) # Accessible, but convention warns against direct access
Importance: The single underscore serves as a strong hint to other developers that this variable is
part of the class's internal implementation and should not be directly accessed or modified from
outside. While technically accessible, violating this convention can lead to unexpected behavior if
the internal implementation changes. It's about maintaining good code design and discouraging direct
manipulation that might break the object's internal state.
Private Variables (Name Mangling):
Private variables are intended to be strictly internal to the class and inaccessible from outside the
class or its subclasses. In Python, you achieve this by prefixing the variable name with double
underscores (__).7
Example: __private_variable:
class MyClass: def __init__(self):
self.__private_var = "I am private"
def get_private(self):M return self.__private_var
obj = MyClass()
# print(obj.__private_var) # This would raise an AttributeError
print(obj.get_private()) # Accessible via a method within the class
Importance: When Python encounters a variable named __variable_name within a class, it
automatically "mangles" the name to _ClassName__variable_name. This makes it much harder,
though not impossible, to directly access the variable from outside the class. The primary importance
of private variables is to enforce encapsulation more strongly. They prevent external code from
accidentally or intentionally modifying the internal state of an object in a way that could lead to
inconsistencies or bugs. By forcing interactions through methods (like get_private()), the class can
control how its internal data is accessed and modified, ensuring data integrity and allowing for future
internal changes without affecting external code.
SET II :
ANS 4: a) How do variable length and keyword arguments work? Explain with program.
In Python, functions can accept a variable number of arguments, which is incredibly flexible for
situations where you don't know beforehand how many arguments a function might receive. This is
achieved using two special syntaxes: *args for variable-length positional arguments and **kwargs
for variable-length keyword arguments.
Variable-Length Positional Arguments (*args):The *args syntax allows a function to accept an
arbitrary number of positional arguments. When a parameter in a function definition is prefixed with
a single asterisk (*), it collects all the extra positional arguments passed to the function into a tuple.
The name args is a convention, but you could use any valid variable name after the *. This is useful
when you want to perform an operation on a collection of items without knowing how many items
will be provided.
Variable-Length Keyword Arguments (**kwargs):The **kwargs syntax allows a function to accept
an arbitrary number of keyword arguments. When a parameter is prefixed with a double asterisk
(**), it collects all the extra keyword arguments passed to the function into a dictionary. The keys of
this dictionary are the keyword names, and the values are their corresponding values. kwargs is also a
convention. This is particularly useful when you want to pass optional configuration parameters or
named settings to a function.
Program Example: def student_info(name, *grades, **details): """
This function demonstrates variable-length positional and keyword arguments.
:param name: A required positional argument for the student's name.
:param grades: *args collects all extra positional arguments (grades) into a tuple.
:param details: **kwargs collects all extra keyword arguments (details) into a dictionary.
"""
print(f"Student Name: {name}")
if grades:
print(f"Grades: {grades} (Type: {type(grades)})")
print(f"Average Grade: {sum(grades) / len(grades):.2f}")
if details:
print("Additional Details:")
for key, value in [Link]():
print(f" {key}: {value}")
# --- Calling the function ---
# Example 1: Only required argument
print("--- Example 1 ---") student_info("Alice") # Output: Student Name: Alice
# Example 2: Required argument + variable positional arguments
print("\n--- Example 2 ---") student_info("Bob", 85, 90, 78) # Output: # Student Name: Bob #
Grades: (85, 90, 78) (Type: <class 'tuple'>) # Average Grade: 84.33
# Example 3: Required argument + variable keyword arguments
print("\n--- Example 3 ---")
student_info("Charlie", city="New York", major="Computer Science")
# Output: # Student Name: Charlie # Additional Details:# city: New York # major: Computer
Science
# Example 4: All types of arguments print("\n--- Example 4 ---")
student_info("David", 92, 88, 95, 89, country="USA", hobbies="coding, reading", age=20)
# Output: # Student Name: David # Grades: (92, 88, 95, 89) (Type: <class 'tuple'>) # Average Grade:
91.00 # Additional Details:
# country: USA # hobbies: coding, reading # age: 20
In this program, *grades collects 85, 90, 78 into a tuple (85, 90, 78). **details collects city="New
York", major="Computer Science" into a dictionary {'city': 'New York', 'major': 'Computer Science'}.
This demonstrates the flexibility of defining functions that can handle varying inputs.
b) Explain differences between remove(), discard() and pop() method for deleting elements
from set.
Sets in Python are unordered collections of unique elements. When it comes to removing elements
from a set, Python provides three distinct methods: remove(), discard(), and pop(). Their primary
differences lie in how they handle the absence of an element and whether they return the removed
element.
● remove(element): The remove() method deletes a specified element from the set. If the
element is present in the set, it is removed. However, if the element is not found in the set, the
remove() method will raise a KeyError. This makes remove() suitable when you are certain
that the element you wish to delete exists in the set, and you want an error to be explicitly
raised if it's missing.
my_set = {10, 20, 30}
my_set.remove(20)
print(my_set) # Output: {10, 30}
# my_set.remove(50) # This would raise a KeyError
● discard(element): Like remove(), the discard() method also deletes a specified element from
the set. The key difference is its behavior when the element is not found. If the element is
present, it's removed. If the element is not found in the set, discard() does nothing and does
not raise an error. This makes discard() safer to use when you're unsure if the element exists
in the set, as it won't interrupt program execution.
my_set = {10, 20, 30}
my_set.discard(20)
print(my_set) # Output: {10, 30}
my_set.discard(50) # No error, set remains unchanged
print(my_set) # Output: {10, 30}
● pop():
The pop() method removes and returns an arbitrary element from the set. Since sets are
unordered, there's no guarantee which element will be removed. If the set is empty, calling
pop() will raise a KeyError. This method is useful when you simply need to remove any
element from the set, perhaps to process it, and don't care about its specific value or position.
my_set = {10, 20, 30}
removed_element = my_set.pop() # Could be 10, 20, or 30 depending on internal hash
print(removed_element) # Output: (e.g., 10 or 20 or 30)
print(my_set) # Output: (e.g., {20, 30} if 10 was popped)
# empty_set = set() # empty_set.pop() # This would raise a KeyError
In summary, remove() is for when you expect the element to be present, discard() is for when
you don't mind if the element isn't present, and pop() is for when you just need to remove and
retrieve any element from the set.
ANS 5: What is Exception Handling?
Exception handling is a programming construct that allows you to gracefully manage and respond to
errors or exceptional events that occur during the execution of a program. In programming, an
"exception" is an event that disrupts the normal flow of a program's instructions. When an error
occurs that Python cannot handle (like dividing by zero, trying to access a non-existent file, or using
an invalid index for a list), it raises an exception. If this exception is not "handled," the program will
terminate abruptly, displaying an error message (a "traceback").
Exception handling mechanisms, typically implemented using try, except, else, and finally blocks,
allow developers to:
1. Detect Errors: Identify when an exceptional situation arises.
2. Handle Errors: Execute specific code to recover from or respond to the error, preventing the
program from crashing.
3. Maintain Program Flow: Allow the program to continue running even after an error, if
possible.
4. Provide User Feedback: Offer meaningful error messages to users instead of cryptic
tracebacks.
5. Clean Up Resources: Ensure that resources (like open files or network connections) are
properly closed, regardless of whether an error occurred.
How to Handle Multiple Exceptions in Python?
Python's try...except block is designed to handle various types of exceptions. You can handle multiple
exceptions in several ways:
1. Multiple except Blocks:
You can specify multiple except blocks, each designed to catch a different type of exception.
The program will execute the code within the first except block whose exception type
matches the raised exception. This is useful when you need different error handling logic for
different types of errors. try:
num1 = int(input("Enter a numerator: ")) num2 = int(input("Enter a denominator: "))
result = num1 / num2 print(f"Result: {result}")
except ValueError: print("Error: Invalid input. Please enter valid integers."
except ZeroDivisionError: print("Error: Cannot divide by zero.") except Exception as e: #
Catches any other unexpected exceptions print(f"An unexpected error occurred: {e}")
print("Program continues after exception handling.")
2. Single except Block with a Tuple of Exceptions:
If you want to apply the same error handling logic for several different types of exceptions,
you can list them as a tuple in a single except statement.
Try: data = [1, 2, 3] index = int(input("Enter an index: ")) value = data[index]
print(f"Value at index {index}: {value}")
# Simulate another error x = int("abc") # This will raise a ValueError except (IndexError,
ValueError) as e:
print(f"Input Error or Index Error: {e}") except ZeroDivisionError: # Specific handling for
this
print("Cannot divide by zero.") except Exception as e:
print(f"An unexpected error occurred: {e}") print("Program continues after exception handling.")
In this example, both IndexError (if the user enters an out-of-bounds index) and ValueError (if int()
conversion fails) will be caught by the first except block. This approach simplifies code when the
response to multiple error types is [Link] using these methods, you can build robust Python
applications that can gracefully recover from various error scenarios, providing a better user
experience and preventing abrupt program termination.
ANS 6 : a) How to handle missing data using pandas? Explain dropna() and fillna() methods.
Missing data, often represented as NaN (Not a Number) in Pandas, is a common challenge in data
analysis. Pandas provides powerful methods to either remove or impute (fill in) these missing values.
● dropna() Method:The dropna() method is used to remove rows or columns that contain
missing values (NaN). Its primary purpose is to clean up a DataFrame by eliminating
incomplete observations.
○ [Link](axis=0) (default): Removes rows containing at least one NaN value.
○ [Link](axis=1): Removes columns containing at least one NaN value.
○ [Link](how='all'): Removes rows/columns only if all values are NaN.
○ [Link](thresh=n): Keeps rows/columns that have at least n non-NaN values.
● Example: import pandas as pd
import numpy as np
data = {'A': [1, 2, [Link], 4], 'B': [5, [Link], [Link], 8], 'C': [9, 10, 11, 12]} df =
[Link](data) print("Original DataFrame:\n", df) df_cleaned = [Link]() # Drops
rows with any NaN print("\nDataFrame after dropna():\n", df_cleaned)
dropna() is quick but can lead to significant data loss if many rows or columns have scattered
missing values.
● fillna() Method:The fillna() method is used to replace missing values (NaN) with a specified
value or a method of [Link] is crucial when you want to retain data points but
cannot afford to have gaps.
○ [Link](value): Replaces all NaN with a specific value (e.g., 0, mean, median,
mode).
○ [Link](method='ffill') (forward fill): Propagates the last valid observation forward
to next valid.
○ [Link](method='bfill') (backward fill): Uses the next valid observation to fill the
gap.
○ [Link](value={'col1': val1, 'col2': val2}): Fills missing values with different
values for different columns.
● Example:import pandas as pd import numpy as np
data = {'A': [1, 2, [Link], 4], 'B': [5, [Link], [Link], 8], 'C': [9, 10, 11, 12]}
df = [Link](data) print("Original DataFrame:\n", df)
df_filled = [Link](0) # Fills all NaN with 0 print("\nDataFrame after fillna(0):\n", df_filled)
df_mean_filled = [Link]([Link]()) # Fills with column means
print("\nDataFrame after fillna(mean):\n", df_mean_filled)
fillna() is powerful for data imputation, allowing for sophisticated strategies to preserve the
dataset's size while making it complete. The choice between dropna() and fillna() depends on
the context, the amount of missing data, and the potential impact on analysis.
b) What are DDL and DML commands? Explain.
In the context of databases, SQL (Structured Query Language) commands are broadly categorized
into two main types based on their function: Data Definition Language (DDL) and Data
Manipulation Language (DML). These categories govern how database structures are managed and
how data within those structures is handled.
● Data Definition Language (DDL) Commands: DDL commands are used to define, modify,
and manage the structure of database objects.7 They deal with the schema or blueprint of the
database rather than the actual data stored within it. DDL operations are typically irreversible
in terms of structure and require specific permissions as they fundamentally change the
database layout.
Common DDL commands include:
○ CREATE: Used to create new database objects like tables, databases, views, indexes,
or stored procedures.
Example: CREATE TABLE Students (StudentID INT PRIMARY KEY, Name
VARCHAR(50));
○ ALTER: Used to modify the structure of existing database objects, such as adding,
deleting, or modifying columns in a [Link]: ALTER TABLE Students ADD
COLUMN Age INT;
○ DROP: Used to delete existing database objects entirely. This command permanently
removes the object and all data within [Link]: DROP TABLE Students;
○ TRUNCATE: Used to remove all rows from a table, but it keeps the table structure.
It's faster than DELETE for removing all rows because it doesn't log individual row
[Link]: TRUNCATE TABLE Students;
○ RENAME: Used to rename an existing database [Link]: RENAME
TABLE Students TO EnrolledStudents;
● DDL commands implicitly commit changes, meaning they are permanent and cannot be
rolled back.
● Data Manipulation Language (DML) Commands:
DML commands are used to manage and manipulate the data stored within database objects.
They allow users to insert, retrieve, modify, and delete data in the database. DML operations
are transaction-oriented, meaning they can be committed (made permanent) or rolled back
(undone) if necessary.
Common DML commands include:
○ INSERT: Used to add new rows of data into a [Link]: INSERT INTO
Students (StudentID, Name) VALUES (1, 'Alice');
○ SELECT: Used to retrieve data from one or more tables. This is the most frequently
used DML [Link]: SELECT Name, Age FROM Students WHERE
StudentID = 1;
○ UPDATE: Used to modify existing data in one or more rows of a [Link]:
UPDATE Students SET Age = 20 WHERE StudentID = 1;
○ DELETE: Used to remove one or more rows from a [Link]: DELETE FROM
Students WHERE Name = 'Alice';
● DML commands affect the data itself and are often used within transactions that can be
committed or rolled back.