IT 243

Programming with Python

TU BIM · Semester 5 · BIM curriculum effective from 2021

Requirement
required
Credits
3
Practice sets
5 sets

Practice sets and model questions

5 complete syllabus-aligned practice examinations with model-style questions and step-by-step solutions. Independently prepared by Paper Khoj, not issued by TU.

practice sets loaded.

Practice Set 1

Report problem

PAPER KHOJ

Practice Examination — Set 1

For BIM · Semester 5

Based on the prescribed TU Faculty of Management syllabus and examination structure

Independently prepared by Paper Khoj. This is not an official Tribhuvan University or Dean's Office question paper.

Course: IT 243 · Programming with Python

Level: Bachelor of Information Management (BIM) · Semester 5

Full Marks: 60

Time: 3 hrs.

Candidates are required to give their answers in their own words as far as practicable. The figures in the margin indicate full marks.

Group 'A'

Brief Answer Questions. Attempt all questions.

[5 × 2 = 10]
  1. [2]

    Distinguish between mutable and immutable data types in Python with two concrete examples of each.

    View model answerHide model answer

    Mutable vs. Immutable Types in Python:

    • Mutable Objects: In-place modifications to the memory contents are permitted without changing the object’s identity (id()).
      • Examples: list (e.g. [1, 2, 3]), dict (e.g. {"key": "val"}), and set.
    • Immutable Objects: Once instantiated in heap memory, the internal value cannot be altered. Any operation that modifies the value allocates a new object with a distinct memory address.
      • Examples: int / float, str (e.g. "Kathmandu"), and tuple (e.g. (10, 20)).
  2. [2]

    Explain the LEGB rule for scope resolution in Python.

    View model answerHide model answer

    The LEGB Scope Resolution Rule:

    Python resolves variable references by searching hierarchical namespaces in sequential priority order:

    1. L - Local: Variables declared inside the currently executing function/lambda.
    2. E - Enclosing: Namespaces of enclosing or nested outer functions (closures).
    3. G - Global: Variables defined at the top level of the module or explicitly bound with the global keyword.
    4. B - Built-in: Predefined core built-ins (e.g., len, range, ValueError, print).
  3. [2]

    What is the difference between is and == operators in Python?

    View model answerHide model answer

    is vs. == Operators:

    • == (Equality Operator): Evaluates whether the values/contents of two objects are equivalent by calling __eq__().
      • Example: [1, 2] == [1, 2] evaluates to True.
    • is (Identity Operator): Evaluates whether two variables reference the exact same memory address (id(a) == id(b)).
      • Example: [1, 2] is [1, 2] evaluates to False (separate heap allocations).
  4. [2]

    How do list comprehensions differ from generator expressions in memory consumption?

    View model answerHide model answer

    List Comprehension vs. Generator Expression:

    • List Comprehension ([x for x in data]): Eagerly evaluates and allocates memory for the entire list in RAM at once (O(N)O(N) space complexity).
    • Generator Expression ((x for x in data)): Lazily computes and yields one element on-demand per iteration using the iterator protocol (__next__()), consuming constant O(1)O(1) auxiliary memory regardless of dataset size.
  5. [2]

    What is the purpose of *args and **kwargs in Python function parameters?

    View model answerHide model answer

    *args and **kwargs:

    • *args (Variable Positional Arguments): Packs an arbitrary number of non-keyword arguments into an immutable tuple.
    • **kwargs (Variable Keyword Arguments): Packs an arbitrary number of named keyword arguments into a standard key-value dict.
    • They provide dynamic variadic function signatures, widely utilized in wrapper decorators and subclass constructor overrides.

Group 'B'

Descriptive Answer Questions. Attempt any SIX questions.

[6 × 5 = 30]
  1. [5]

    Write a Python program that reads a text file, counts the frequency of each distinct word (ignoring case and punctuation), and writes the top 5 most frequent words to a JSON file.

    View model answerHide model answer

    Word Frequency Counter & JSON Exporter:

    import json
    import re
    from collections import Counter
    
    def analyze_word_frequencies(input_filepath: str, output_filepath: str) -> None:
        try:
            with open(input_filepath, 'r', encoding='utf-8') as f:
                content = f.read()
    
            # Normalize text: convert to lowercase and extract alphabetic words
            words = re.findall(r'\b[a-z]+\b', content.lower())
    
            # Calculate frequencies
            word_counts = Counter(words)
            top_5 = dict(word_counts.most_common(5))
    
            # Output to JSON
            with open(output_filepath, 'w', encoding='utf-8') as out_f:
                json.dump({
                    "total_distinct_words": len(word_counts),
                    "top_5_frequent_words": top_5
                }, out_f, indent=4)
    
            print(f"Analysis complete. Top 5 words saved to {output_filepath}: {top_5}")
    
        except FileNotFoundError:
            print(f"Error: Input file '{input_filepath}' not found.")
        except Exception as e:
            print(f"An unexpected error occurred: {e}")
    
    if __name__ == "__main__":
        analyze_word_frequencies("sample.txt", "top_words.json")
    

    Key Engineering Details:

    1. Context Manager (with open): Guarantees deterministic file descriptor closure even upon runtime exceptions.
    2. Regular Expression (\b[a-z]+\b): Strips out commas, periods, numbers, and quotation marks seamlessly.
    3. collections.Counter: Executes an optimal O(N)O(N) hash-map counting algorithm.
  2. [5]

    Explain Python decorators. Write a custom decorator @execution_timer that calculates and prints the elapsed execution time of any decorated function in milliseconds.

    View model answerHide model answer

    Python Decorators & Timing Implementation:

    A decorator is a higher-order function that takes another function as an argument, extends its runtime behavior without modifying its source code, and returns a callable object.

    import time
    from functools import wraps
    from typing import Callable, Any
    
    def execution_timer(func: Callable) -> Callable:
        @wraps(func)  # Preserves func name, docstring, and annotations
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            start_time = time.perf_counter()
            result = func(*args, **kwargs)
            end_time = time.perf_counter()
    
            elapsed_ms = (end_time - start_time) * 1000.0
            print(f"[BENCHMARK] Function '{func.__name__}' executed in {elapsed_ms:.3f} ms")
            return result
        return wrapper
    
    # Demonstration
    @execution_timer
    def compute_heavy_sum(limit: int) -> int:
        return sum(x * x for x in range(limit))
    
    if __name__ == "__main__":
        res = compute_heavy_sum(1_000_000)
        print(f"Result: {res}")
    

    Architectural Highlights:

    • @wraps(func): Essential for preventing introspection loss (wrapper.__name__ stays "compute_heavy_sum").
    • time.perf_counter(): Provides the highest available fractional resolution clock for elapsed wall-time measurement.
  3. [5]

    Demonstrate Object-Oriented Programming in Python by implementing an abstract class Employee and subclasses FullTimeEmployee and ContractEmployee demonstrating encapsulation and polymorphism.

    View model answerHide model answer

    Abstract Classes and Polymorphism in Python:

    from abc import ABC, abstractmethod
    
    class Employee(ABC):
        def __init__(self, emp_id: str, name: str, base_salary: float):
            self._emp_id = emp_id          # Protected attribute
            self._name = name
            self._base_salary = base_salary
    
        @property
        def name(self) -> str:
            return self._name
    
        @abstractmethod
        def calculate_monthly_pay(self) -> float:
            """Abstract method enforced on all concrete subclasses."""
            pass
    
    class FullTimeEmployee(Employee):
        def __init__(self, emp_id: str, name: str, base_salary: float, allowance: float):
            super().__init__(emp_id, name, base_salary)
            self.__allowance = allowance    # Private attribute
    
        def calculate_monthly_pay(self) -> float:
            return self._base_salary + self.__allowance
    
    class ContractEmployee(Employee):
        def __init__(self, emp_id: str, name: str, hourly_rate: float, hours_worked: float):
            super().__init__(emp_id, name, 0.0)
            self.hourly_rate = hourly_rate
            self.hours_worked = hours_worked
    
        def calculate_monthly_pay(self) -> float:
            return self.hourly_rate * self.hours_worked
    
    # Polymorphic processing demo
    if __name__ == "__main__":
        staff: list[Employee] = [
            FullTimeEmployee("EMP01", "Sujan Karki", 65000.0, 15000.0),
            ContractEmployee("CNT02", "Pooja Shrestha", 850.0, 120.0)
        ]
    
        for member in staff:
            print(f"Staff: {member.name:15} | Gross Pay: NPR {member.calculate_monthly_pay():,.2f}")
    
  4. [5]

    Explain the difference between shallow copy and deep copy in Python. Demonstrate with code modifying a nested list.

    View model answerHide model answer

    Shallow Copy vs. Deep Copy:

    • Shallow Copy (copy.copy()): Constructs a new compound object and populates it with references to the original object’s child elements. Mutating a nested collection mutates both objects.
    • Deep Copy (copy.deepcopy()): Recursively copies all referenced compound objects, allocating independent memory for all child elements.
    import copy
    
    original = [[10, 20], [30, 40]]
    shallow = copy.copy(original)
    deep = copy.deepcopy(original)
    
    # Mutate nested sub-list in original
    original[0][0] = 999
    
    print(f"Original: {original}")  # [[999, 20], [30, 40]]
    print(f"Shallow:  {shallow}")   # [[999, 20], [30, 40]] -> Reflects mutation!
    print(f"Deep:     {deep}")      # [[10, 20], [30, 40]]  -> Completely isolated!
    
  5. [5]

    What are Python generator functions? How does the yield statement operate, and why are generators memory efficient?

    View model answerHide model answer

    Python Generators and the yield Mechanism:

    A generator function contains one or more yield statements. When invoked, it returns an iterator object without executing the body immediately.

    1. State Preservation: When execution encounters yield, the generator suspends execution, saves its local stack frame (variables, instruction pointer), and sends the evaluated value to the caller.
    2. Resumption: When next() is called again, execution resumes immediately after the suspended yield statement.
    3. Memory Efficiency: Instead of assembling a collection of millions of elements in RAM (O(N)O(N) space), generators stream items lazily on demand (O(1)O(1) space).
    def fibonacci_stream(limit: int):
        a, b = 0, 1
        count = 0
        while count < limit:
            yield a
            a, b = b, a + b
            count += 1
    
    # Consuming the stream
    for num in fibonacci_stream(6):
        print(num, end=" ")  # Outputs: 0 1 1 2 3 5
    
  6. [5]

    Explain how custom exception classes are created in Python. Write a program that defines InsufficientFundsError and raises it in an ATM withdrawal class.

    View model answerHide model answer

    Custom Exception Implementation:

    class BankingException(Exception):
        """Base exception for domain banking errors."""
        pass
    
    class InsufficientFundsError(BankingException):
        """Raised when an account balance is insufficient for a withdrawal."""
        def __init__(self, current_balance: float, requested_amount: float):
            self.current_balance = current_balance
            self.requested_amount = requested_amount
            self.shortage = requested_amount - current_balance
            super().__init__(f"Withdrawal denied: Shortage of NPR {self.shortage:,.2f} "
                             f"(Current Balance: NPR {current_balance:,.2f}, Attempted: NPR {requested_amount:,.2f})")
    
    class BankAccount:
        def __init__(self, account_no: str, balance: float):
            self.account_no = account_no
            self.balance = balance
    
        def withdraw(self, amount: float) -> float:
            if amount <= 0:
                raise ValueError("Withdrawal amount must be strictly positive.")
            if amount > self.balance:
                raise InsufficientFundsError(self.balance, amount)
            self.balance -= amount
            return self.balance
    
    # Execution & verification
    if __name__ == "__main__":
        acc = BankAccount("NABIL-0129", 5000.0)
        try:
            acc.withdraw(7500.0)
        except InsufficientFundsError as err:
            print(f"[REJECTED] {err}")
        except Exception as err:
            print(f"System Error: {err}")
    

Group 'C'

Comprehensive / Analytical Questions. Attempt all questions.

[2 × 10 = 20]
  1. [10]

    Design and implement a complete student grade management system in Python adhering to object-oriented principles. The system must support loading student records from a CSV file, computing individual GPAs, calculating class statistics (highest, lowest, average GPA), and exporting a formatted analytical report to a text file.

    View model answerHide model answer

    Complete Student Grade Management System:

    import csv
    import statistics
    from typing import List, Dict
    
    class Student:
        def __init__(self, student_id: str, name: str, scores: Dict[str, float]):
            self.student_id = student_id
            self.name = name
            self.scores = scores
    
        def compute_gpa(self) -> float:
            """Converts percentage marks to a standard 4.0 GPA scale."""
            avg_score = statistics.mean(self.scores.values())
            if avg_score >= 90: return 4.0
            elif avg_score >= 80: return 3.6
            elif avg_score >= 70: return 3.2
            elif avg_score >= 60: return 2.8
            elif avg_score >= 50: return 2.4
            else: return 0.0
    
    class GradeBook:
        def __init__(self, course_name: str):
            self.course_name = course_name
            self.students: List[Student] = []
    
        def load_from_csv(self, filepath: str) -> None:
            with open(filepath, mode='r', encoding='utf-8') as f:
                reader = csv.DictReader(f)
                for row in reader:
                    s_id = row['id']
                    name = row['name']
                    # Assume subject headers start with 'sub_'
                    scores = {col: float(val) for col, val in row.items() if col not in ('id', 'name')}
                    self.students.append(Student(s_id, name, scores))
    
        def generate_analytics(self) -> Dict[str, float]:
            if not self.students:
                return {}
            gpas = [s.compute_gpa() for s in self.students]
            return {
                "count": len(gpas),
                "highest_gpa": max(gpas),
                "lowest_gpa": min(gpas),
                "mean_gpa": statistics.mean(gpas),
                "median_gpa": statistics.median(gpas)
            }
    
        def export_report(self, output_path: str) -> None:
            stats = self.generate_analytics()
            with open(output_path, 'w', encoding='utf-8') as f:
                f.write(f"=== TU FoM Grade Report: {self.course_name} ===\n\n")
                f.write(f"{'ID':<10} {'Name':<20} {'GPA':<6}\n")
                f.write("-" * 40 + "\n")
                for s in self.students:
                    f.write(f"{s.student_id:<10} {s.name:<20} {s.compute_gpa():<6.2f}\n")
                f.write("-" * 40 + "\n")
                f.write(f"Total Enrolled: {int(stats['count'])}\n")
                f.write(f"Class Average GPA: {stats['mean_gpa']:.2f}\n")
                f.write(f"Highest GPA:       {stats['highest_gpa']:.2f}\n")
                f.write(f"Lowest GPA:        {stats['lowest_gpa']:.2f}\n")
    

    Architectural Merits:

    1. Separation of Concerns: Student handles individual domain logic; GradeBook orchestrates collection lifecycle and file serialization.
    2. Robust CSV Ingestion: Leverages csv.DictReader to dynamically absorb arbitrary subject column names.
  2. [10]

    Explain the Global Interpreter Lock (GIL) in CPython. Compare Multithreading and Multiprocessing in Python. Write two complete benchmark programs: one executing a CPU-bound task using multiprocessing.Pool and another executing an I/O-bound task using concurrent.futures.ThreadPoolExecutor.

    View model answerHide model answer

    The CPython Global Interpreter Lock (GIL):

    The GIL is a mutual-exclusion lock implemented in CPython that restricts execution of native bytecode to exactly one operating system thread at a time. It ensures memory safety in CPython’s reference-counting garbage collection.

    • Impact on CPU-Bound Tasks: Pure multithreading cannot leverage multi-core CPU architectures. The threads contend for the single GIL lock, resulting in slower execution due to context-switching overhead.
    • Impact on I/O-Bound Tasks: Highly effective because threads release the GIL while waiting for network socket responses, disk I/O, or database queries.

    Comparison: Multithreading vs. Multiprocessing

    Dimension Multithreading (threading / ThreadPoolExecutor) Multiprocessing (multiprocessing / ProcessPoolExecutor)
    Address Space Shared heap/memory between threads Isolated, distinct memory space per process
    GIL Bypass No (Bound by single GIL) Yes (Each process has its own independent Python interpreter and GIL)
    Overhead Low memory footprint; lightweight Higher memory overhead; IPC requires serialization (pickling)
    Ideal Use Case Network requests, file I/O, GUI responsiveness High-performance numerical computing, image processing, cryptography

    Implementation 1: CPU-Bound Processing via multiprocessing.Pool

    from multiprocessing import Pool, cpu_count
    import time
    
    def count_primes_in_range(range_tuple: tuple[int, int]) -> int:
        start, end = range_tuple
        primes_count = 0
        for n in range(start, end):
            if n > 1:
                is_prime = True
                for i in range(2, int(n**0.5) + 1):
                    if n % i == 0:
                        is_prime = False
                        break
                if is_prime:
                    primes_count += 1
        return primes_count
    
    def run_cpu_benchmark():
        chunks = [(1, 500000), (500001, 1000000), (1000001, 1500000), (1500001, 2000000)]
        t0 = time.perf_counter()
        with Pool(processes=cpu_count()) as pool:
            results = pool.map(count_primes_in_range, chunks)
        t1 = time.perf_counter()
        print(f"[MULTIPROCESSING CPU] Total Primes: {sum(results)} in {t1 - t0:.3f} s")
    

    Implementation 2: I/O-Bound Processing via ThreadPoolExecutor

    from concurrent.futures import ThreadPoolExecutor, as_completed
    import time
    import urllib.request
    
    def fetch_url_status(url: str) -> tuple[str, int]:
        req = urllib.request.Request(url, headers={'User-Agent': 'BITM-Bot/1.0'})
        try:
            with urllib.request.urlopen(req, timeout=5) as response:
                return url, response.status
        except Exception as e:
            return url, 500
    
    def run_io_benchmark():
        urls = [
            "https://www.google.com",
            "https://www.github.com",
            "https://www.python.org",
            "https://www.wikipedia.org"
        ]
        t0 = time.perf_counter()
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = [executor.submit(fetch_url_status, u) for u in urls]
            for f in as_completed(futures):
                url, status = f.result()
                print(f"[THREAD I/O] {url} -> HTTP {status}")
        t1 = time.perf_counter()
        print(f"[THREADING I/O] Total Elapsed: {t1 - t0:.3f} s")