1. Difference Between List and Tuple?
Lists and tuples are both sequence data types in Python that can store multiple items.
However, the key difference lies in their mutability and usage:
| Feature | List | Tuple |
|---|---|---|
| Mutability | Mutable (can be changed after creation) | Immutable (cannot be changed once created) |
| Syntax | Defined using [] | Defined using () |
| Performance | Slower (due to mutability) | Faster (due to immutability) |
| Use Case | Suitable for data that changes | Suitable for fixed data |
| Methods | Many (e.g., append(), remove(), sort()) | Few (e.g., count(), index()) |
🧩 Example:
# List Example
my_list = [1, 2, 3]
my_list.append(4) # Modifying the list
print("List:", my_list)
# Tuple Example
my_tuple = (1, 2, 3)
# my_tuple.append(4) # ❌ Error: Tuples are immutable
print("Tuple:", my_tuple)Output:
List: [1, 2, 3, 4]
Tuple: (1, 2, 3)2. What is a Decorator? Explain With Example?
A decorator in Python is a special function that allows you to modify or enhance the behavior of another function without changing its code.
They are often used for logging, authentication, performance monitoring, or input validation.
Decorators wrap another function inside themselves, execute some code before or after the wrapped function runs, and then return the result.
🧩 Example 1: Logging Function Calls
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Function '{func.__name__}' called with arguments: {args} {kwargs}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' returned: {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
add(3, 5)Output:
Function 'add' called with arguments: (3, 5) {}
Function 'add' returned: 8🧩 Example 2: Timing a Function
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def slow_function():
time.sleep(2)
print("Finished slow function")
slow_function()Output:
Finished slow function
slow_function executed in 2.0003 seconds🧩 Example 3: Authentication Check
def require_login(func):
def wrapper(user):
if not user.get("is_logged_in"):
print("Access Denied: User not logged in.")
return
return func(user)
return wrapper
@require_login
def dashboard(user):
print(f"Welcome {user['name']}! Accessing dashboard...")
user1 = {"name": "Dheeraj", "is_logged_in": True}
user2 = {"name": "Guest", "is_logged_in": False}
dashboard(user1)
dashboard(user2)Output:
Welcome Dheeraj! Accessing dashboard...
Access Denied: User not logged in.💬 Key Takeaways:
- Decorators use the syntax
@decorator_nameplaced above the function definition. - They are used to extend functionality of functions without modifying their code.
- They work with functions, methods, and even classes.
3. Difference Between List and Dict Comprehension?
List and dictionary comprehensions are concise ways to create lists and dictionaries in Python.
Both are used for creating new sequences or mappings from existing iterables, but they differ in structure and output type.
| Feature | List Comprehension | Dict Comprehension |
|---|---|---|
| Purpose | Creates a new list | Creates a new dictionary |
| Syntax | [expression for item in iterable if condition] | {key: value for item in iterable if condition} |
| Output Type | List | Dictionary |
| Use Case | When you need a list of computed values | When you need key-value pairs generated dynamically |
🧩 Example:
# List Comprehension Example
numbers = [1, 2, 3, 4, 5]
squares = [n**2 for n in numbers]
print("List Comprehension:", squares)
# Dict Comprehension Example
squares_dict = {n: n**2 for n in numbers}
print("Dict Comprehension:", squares_dict)Output:
List Comprehension: [1, 4, 9, 16, 25]
Dict Comprehension: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}4. How Memory Managed In Python?
Python uses an automatic memory management system that handles memory allocation and deallocation internally.
It relies on a private heap where all Python objects and data structures are stored, and it uses techniques like reference counting and garbage collection to manage memory efficiently.
Key Components of Python Memory Management
| Component | Description |
|---|---|
| 1. Private Heap Space | All Python objects (like lists, dicts, etc.) are stored in a private heap area managed by the Python interpreter. |
| 2. Memory Manager | Allocates memory from the private heap and ensures efficient memory utilization. |
| 3. Object-specific Allocators | Each object type (list, dict, class, etc.) has its own allocator to manage memory. |
| 4. Garbage Collector | Automatically frees memory by removing objects that are no longer referenced. |
| 5. Reference Counting | Each object keeps track of how many references point to it. When the count reaches zero, the object is destroyed. |
🧩 Example 1: Reference Counting
import sys
a = [1, 2, 3]
b = a # Another reference to the same object
c = a # Another reference again
print(sys.getrefcount(a)) # Shows number of referencesOutput (may vary):
4(The count includes references by a, b, c, and the argument passed to getrefcount().)
🧩 Example 2: Garbage Collection
import gc
class Demo:
def __del__(self):
print("Object destroyed")
obj = Demo()
del obj # Explicitly deleting the object
gc.collect() # Forces garbage collectionOutput:
Object destroyed🧩 Example 3: Circular References
import gc
class A:
def __init__(self):
self.ref = None
a1 = A()
a2 = A()
a1.ref = a2
a2.ref = a1
del a1
del a2
gc.collect() # Garbage collector handles circular referenceOutput:
# No output, but memory is reclaimed by garbage collectorMemory Optimization Tips
- Use generators instead of lists for large data (saves memory).
- Use
delto delete unnecessary variables. - Use
sys.getsizeof()to check object size. - Use weak references (
weakrefmodule) to avoid reference cycles. - Avoid keeping unused large objects in memory.
In Summary:
- Python automatically manages memory through reference counting and garbage collection.
- The Python memory manager and garbage collector work together to keep memory usage efficient.
- Developers can influence memory management using
gc,sys, andweakrefmodules when needed.
5. Difference Between Generators and Iterators?
Both generators and iterators are used to iterate over data in Python, but they differ in how they are implemented and used.
| Feature | Iterator | Generator |
|---|---|---|
| Definition | An object that implements the __iter__() and __next__() methods. | A special type of iterator created using a function with yield or generator expression. |
| Creation | Created using classes or the iter() function. | Created automatically using a yield statement inside a function. |
| Memory Usage | Can be memory-heavy if it stores large data. | Memory-efficient — generates items on the fly. |
| Return Type | Returns data using the return statement. | Returns data using the yield keyword. |
| Syntax Simplicity | More complex (requires class implementation). | Easier and cleaner to implement. |
| Reusability | Once exhausted, needs to be recreated manually. | Once exhausted, also needs to be recreated. |
🧩 Example 1: Iterator
# Creating an iterator using a class
class Counter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self):
if self.current > self.high:
raise StopIteration
else:
num = self.current
self.current += 1
return num
counter = Counter(1, 5)
for num in counter:
print(num)Output:
1
2
3
4
5🧩 Example 2: Generator
# Creating a generator using yield
def counter(low, high):
while low <= high:
yield low
low += 1
for num in counter(1, 5):
print(num)Output:
1
2
3
4
5🧩 Example 3: Generator Expression (Compact Form)
squares = (x*x for x in range(5))
print(next(squares))
print(next(squares))Output:
0
1In Summary:
- Iterators are objects following the iterator protocol (
__iter__()and__next__()). - Generators are a simpler way to create iterators using
yield. - Generators are more memory-efficient and easier to implement than custom iterators.
6. What is __init__ Keyword in Python?
The __init__ method in Python is a special (built-in) method known as the constructor.
It is automatically called when a new object of a class is created and is used to initialize the object’s attributes.
| Feature | Description |
|---|---|
| Purpose | Initializes an object’s attributes at the time of creation. |
| Syntax | def __init__(self, parameters): |
| Called Automatically | Yes, when a new instance of a class is created. |
| Special Method | One of Python’s “dunder” (double underscore) methods. |
| Return Type | Does not return anything (returns None by default). |
🧩 Example 1: Basic Use of __init__
class Person:
def __init__(self, name, age):
self.name = name # instance variable
self.age = age # instance variable
def show(self):
print(f"My name is {self.name} and I am {self.age} years old.")
# Creating object
p1 = Person("Dheeraj", 24)
p1.show()Output:
My name is Dheeraj and I am 24 years old.🧩 Example 2: Default Values in __init__
class Car:
def __init__(self, brand="Tesla", color="Black"):
self.brand = brand
self.color = color
car1 = Car()
car2 = Car("BMW", "Blue")
print(car1.brand, car1.color)
print(car2.brand, car2.color)Output:
Tesla Black
BMW Blue🧩 Example 3: Using __init__ for Computation During Initialization
class Rectangle:
def __init__(self, length, width):
self.area = length * width # Calculated at initialization
rect = Rectangle(5, 3)
print("Area of rectangle:", rect.area)Output:
Area of rectangle: 15In Summary:
__init__is the constructor method that initializes instance variables.- It is called automatically when an object is created.
- It helps in setting up the initial state of an object.
- It’s one of the most commonly used dunder (double underscore) methods in Python classes.
7. What is __init__.py in Python?
The __init__.py file is a special Python file used to mark a directory as a Python package.
When a directory contains this file, Python treats it as a package, allowing you to import modules from that directory.
| Feature | Description |
|---|---|
| Purpose | Marks a directory as a Python package so it can be imported. |
| File Name | Must be named __init__.py. |
| Optional Since | Python 3.3 (namespace packages can exist without it). |
| Can Contain Code | Yes — used for initialization code, imports, or package-level variables. |
| Execution | Runs automatically when the package or any module inside it is imported. |
🧩 Example 1: Basic Package Structure
# File: calculator.py (This is a module)
def add(a, b):
return a + b
def subtract(a, b):
return a - bUsage:
import calculator
print(calculator.add(5, 3))
print(calculator.subtract(10, 4))import my_package.module1
my_package.module1.greet()Output:
Initializing my_package
Hello from module1🧩 Example 2: Importing Functions in __init__.py
# __init__.py
from .module1 import greet
from .module2 import add
# module1.py
def greet():
print("Hello, Dheeraj!")
# module2.py
def add(a, b):
return a + bUsage:
from my_package import greet, add
greet()
print(add(5, 10))Output:
Hello, Dheeraj!
15🧩 Example 3: Package Initialization Code
# __init__.py
print("Package is being initialized...")
config = {"version": "1.0", "author": "Dheeraj"}Usage:
import my_package
print(my_package.config)Output:
Package is being initialized...
{'version': '1.0', 'author': 'Dheeraj'}In Summary:
__init__.pydefines a package and allows imports from it.- It can contain initialization logic, shared variables, or shortcut imports.
- From Python 3.3 onward, it’s optional, but still commonly used for explicit package control and clean imports.
7. Difference Between Modules and Packages in Python
Both modules and packages are used to organize and structure Python code, especially in large projects.
However, they differ in scope and organization level.
| Feature | Module | Package |
|---|---|---|
| Definition | A single Python file that contains code (functions, variables, classes). | A collection (directory) of modules organized together. |
| File Type | A single .py file. | A directory containing an __init__.py file (and possibly multiple modules/sub-packages). |
| Purpose | To logically organize and reuse code. | To structure multiple modules into a hierarchy. |
| Import Syntax | import module_name | import package_name.module_name |
| Example | math.py, os.py, or a custom utils.py file. | A folder like numpy, pandas, or my_app/. |
| Contains | Code definitions, variables, and functions. | Multiple modules and possibly other packages. |
🧩 Example 1: Module Example
# File: calculator.py (This is a module)
def add(a, b):
return a + b
def subtract(a, b):
return a - bUsage:
import calculator
print(calculator.add(5, 3))
print(calculator.subtract(10, 4))Output:
8
6🧩 Example 2: Package Example
my_package/
│
├── __init__.py
├── math_operations.py
└── string_operations.py# math_operations.py
def square(num):
return num * num
# string_operations.py
def greet(name):
return f"Hello, {name}!"Usage:
from my_package import math_operations, string_operations
print(math_operations.square(5))
print(string_operations.greet("Dheeraj"))Output:
25
Hello, Dheeraj!🧩 Example 3: Nested Package Structure
project/
│
├── __init__.py
├── utils/
│ ├── __init__.py
│ ├── file_utils.py
│ └── math_utils.pyUsage:
from project.utils.math_utils import add_numbersIn Summary:
- A module is a single file of Python code.
- A package is a collection of modules organized in a folder (with an
__init__.pyfile). - Modules help organize code logically, while packages help organize modules hierarchically.
8. Difference Between range and xrange in Python
The difference between range and xrange mainly applies to Python 2.
In Python 3, the xrange() function was removed, and range() now behaves like xrange() used to.
Both are used to generate a sequence of numbers, but they differ in memory usage and performance.
| Feature | range (Python 2) | xrange (Python 2) |
|---|---|---|
| Return Type | Returns a list of numbers. | Returns an iterator (or generator-like object). |
| Memory Usage | Uses more memory (stores all numbers in memory). | Uses less memory (generates numbers on demand). |
| Speed | Slower for large ranges (due to list creation). | Faster and more memory-efficient. |
| Iteration | Can be iterated multiple times. | Can be iterated only once (like a generator). |
| Availability | Works in both Python 2 and 3. | Only available in Python 2. Removed in Python 3. |
🧩 Example 1: Using range in Python 2
# Python 2 example
nums = range(5)
print(nums)Output (Python 2):
[0, 1, 2, 3, 4]🧩 Example 2: Using xrange in Python 2
# Python 2 example
nums = xrange(5)
print(nums)Output (Python 2):
xrange(0, 5)(It returns an xrange object, not a list.)
To see the numbers:
for i in xrange(5):
print(i)Output:
0
1
2
3
4🧩 Example 3: Modern Python 3 Equivalent
In Python 3, there is no xrange(), and range() behaves like xrange() did in Python 2.
nums = range(5)
print(nums) # Returns a range object
print(list(nums)) # Converts it to a listOutput (Python 3):
range(0, 5)
[0, 1, 2, 3, 4]In Summary:
- In Python 2,
range()→ returns a list (uses more memory).xrange()→ returns an iterator (memory efficient).
- In Python 3,
- Only
range()exists, and it behaves likexrange()(lazy evaluation, efficient).
- Only
9. What are Generators? Explain with Example?
Generators in Python are a simple way to create iterators using functions and the yield keyword.
They allow you to generate values one at a time, instead of returning them all at once — making them memory-efficient and faster for large datasets.
| Feature | Description |
|---|---|
| Definition | A function that returns an iterator object which generates values on the fly. |
| Keyword Used | yield instead of return. |
| Memory Usage | Low — values are produced only when needed (lazy evaluation). |
| Execution State | Remembers its state between each yield call. |
| Use Case | Handling large data, streaming, infinite sequences, etc. |
🧩 Example 1: Basic Generator
def my_generator():
for i in range(3):
yield i
# Using the generator
gen = my_generator()
print(next(gen)) # First value
print(next(gen)) # Second value
print(next(gen)) # Third valueOutput:
0
1
2(If you call next(gen) again, it raises StopIteration.)
🧩 Example 2: Using Generators with Loops
def countdown(n):
while n > 0:
yield n
n -= 1
for num in countdown(5):
print(num)Output:
5
4
3
2
1🧩 Example 3: Generator Expression (Compact Syntax)
Generators can also be created using parentheses () instead of square brackets [].
squares = (x**2 for x in range(5))
print(next(squares))
print(next(squares))
print(list(squares)) # Convert remaining items to listOutput:
0
1
[4, 9, 16]🧩 Example 4: Infinite Generator
Generators can create infinite sequences because they produce values lazily.
def infinite_numbers():
n = 1
while True:
yield n
n += 1
gen = infinite_numbers()
for i in range(5):
print(next(gen))Output:
1
2
3
4
5In Summary:
- Generators are iterators built using functions and
yield. - They save memory by producing one value at a time instead of generating all at once.
- Ideal for streaming large data, pipelines, and infinite sequences.
yieldpauses the function, andnext()resumes it from where it left off.
10. What are In-Built Data Types in Python OR Explain Mutable and Immutable Data Types?
Python provides several built-in data types that define the kind of value a variable can hold.
These data types can be broadly categorized into Mutable and Immutable types based on whether their values can be changed after creation.
🔹 Built-in Data Types in Python
| Category | Data Types | Description |
|---|---|---|
| Numeric | int, float, complex | Used for numeric values like integers, decimals, and complex numbers. |
| Sequence | str, list, tuple, range | Ordered collections of items. |
| Set | set, frozenset | Unordered collections of unique items. |
| Mapping | dict | Collection of key-value pairs. |
| Boolean | bool | Represents True or False. |
| Binary | bytes, bytearray, memoryview | Used to handle binary data. |
| None Type | NoneType | Represents the absence of a value (None). |
🔹 Mutable vs Immutable Data Types
| Type | Description | Examples |
|---|---|---|
| Mutable | Can be changed after creation (modifications like adding, updating, or deleting elements are allowed). | list, dict, set, bytearray |
| Immutable | Cannot be changed once created (any modification creates a new object). | int, float, bool, str, tuple, frozenset, bytes |
🧩 Example 1: Mutable Data Type (List)
my_list = [1, 2, 3]
print("Before:", my_list)
my_list.append(4)
print("After:", my_list)Output:
Before: [1, 2, 3]
After: [1, 2, 3, 4](The same list object is modified — hence mutable.)
🧩 Example 2: Immutable Data Type (String)
my_str = "Hello"
print("Before:", my_str)
my_str = my_str + " World"
print("After:", my_str)Output:
Before: Hello
After: Hello World(A new string object is created — hence immutable.)
🧩 Example 3: Checking Mutability Using id()
x = 10
print("Before:", id(x))
x += 5
print("After:", id(x))Output:
Before: 140705237854448
After: 140705237854768(The object ID changed — integers are immutable.)
🧩 Example 4: Mutable Dictionary
person = {"name": "Dheeraj", "age": 24}
print("Before:", person)
person["age"] = 25
print("After:", person)Output:
Before: {'name': 'Dheeraj', 'age': 24}
After: {'name': 'Dheeraj', 'age': 25}(Same dictionary object is modified — mutable.)
🔹 In Summary
- Mutable Data Types → Can be changed after creation (
list,dict,set). - Immutable Data Types → Cannot be changed once created (
int,str,tuple). - Python internally creates new objects when an immutable value is modified.
- Understanding mutability is crucial for memory management, hashing, and performance optimization.
