National Institute of Electronics and Information Technology
(Deemed to be University)
Programming in Python: Assignment 2
Comprehensive Solutions
Q1. Floating-Point Comparison: 0.1 + 0.2 == 0.3
Python (and most programming languages) use IEEE 754 binary floating-point arithmetic. In this
system, numbers like 0.1 and 0.2 cannot be represented exactly in binary, leading to tiny rounding
errors.
Why it Fails
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
The value 0.1 in binary is actually
0.1000000000000000055511151231257827021181583404541015625, and similarly for 0.2. Their sum
slightly exceeds 0.3, causing the equality check to fail.
Solutions
• Use [Link]() (Python 3.5+):
import math
[Link](0.1 + 0.2, 0.3) # True
• Use the round() function:
round(0.1 + 0.2, 10) == round(0.3, 10) # True
• Use the decimal module for exact precision:
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') == Decimal('0.3') # True
Q2. Mutual Exclusion in if-elif-else
Mutual exclusion means only one branch of a conditional ladder executes — once a condition is True,
all subsequent branches are skipped regardless of whether their conditions would also be True.
How it Works
x = 15
if x > 20:
print('Greater than 20') # skipped
elif x > 10:
print('Greater than 10') # EXECUTES
elif x > 5:
print('Greater than 5') # skipped even though True
else:
print('5 or less') # skipped
Even though x = 15 satisfies both x > 10 and x > 5, only the first matching branch (x > 10) runs. This
guarantees exactly one block executes, making the branches mutually exclusive.
Q3. Role of Indentation and Avoiding Mixed Spaces/Tabs
In Python, indentation is not merely stylistic — it is syntactically significant. It defines code blocks (the
body of if, for, while, def, class, etc.). Python uses indentation instead of braces {}.
Why Indentation Matters
for i in range(3):
print(i) # inside the loop
print('Done') # outside the loop
Why Mixing Spaces and Tabs is Dangerous
• Python 3 raises a TabError if spaces and tabs are mixed inconsistently.
• Different editors display tabs at different widths (4, 8 spaces), causing visual misalignment.
• Code may appear correct visually but fail at runtime.
Best Practice: Use 4 spaces per indentation level (PEP 8 standard). Configure your editor to insert
spaces when the Tab key is pressed.
Q4. The pass Keyword in Python
pass is a null statement — it does nothing when executed. It is used as a placeholder where Python
syntax requires a statement but no action is needed.
Common Use Cases
• Empty function or class definition:
def future_feature():
pass # TODO: implement later
• Empty loop body:
for i in range(10):
pass # intentional no-op
• Empty exception handler:
try:
risky_operation()
except ValueError:
pass # silently ignore
Without pass, Python would raise an IndentationError or SyntaxError for an empty block.
Q5. range() — start, stop, step Values and Defaults
The range type represents an arithmetic sequence of integers. It takes up to three arguments:
Syntax
range(stop)
range(start, stop)
range(start, stop, step)
Parameters
• start: First value of the sequence. Default = 0
• stop: Sequence ends before this value (exclusive). Required.
• step: Increment between values. Default = 1. Can be negative.
Examples
list(range(5)) # [0, 1, 2, 3, 4] (start=0, step=1)
list(range(2, 8)) # [2, 3, 4, 5, 6, 7] (step=1)
list(range(0, 10, 2)) # [0, 2, 4, 6, 8]
list(range(10, 0, -2)) # [10, 8, 6, 4, 2]
Q6. How len() Works on range Without Storing Elements
range objects are lazy — they do not store elements in memory. Instead, they compute values on
demand using the formula:
length = max(0, ceil((stop - start) / step))
How len() Computes It
When len() is called on a range, Python uses the __len__() method of the range object, which applies
the arithmetic formula directly:
r = range(2, 20, 3)
len(r) # = ceil((20 - 2) / 3) = ceil(6) = 6
# Elements would be: 2, 5, 8, 11, 14, 17
This is O(1) time complexity — regardless of how large the range is, len() computes instantly without
iterating.
Q7. Converting Negative Indices Using len
Python allows negative indexing for sequences. A negative index -n is equivalent to len(sequence) - n.
Conversion Formula
effective_index = len(sequence) + negative_index
Example
r = range(10, 60, 10) # [10, 20, 30, 40, 50], len=5
r[-1] # = r[5 + (-1)] = r[4] = 50
r[-2] # = r[5 + (-2)] = r[3] = 40
r[-5] # = r[5 + (-5)] = r[0] = 10
If the resulting effective index is still negative (e.g., index -6 on a range of length 5), Python raises an
IndexError.
Q8. Indexing with Subscript on range Without Storing Elements
Like len(), subscript indexing on a range object uses arithmetic, not iteration.
Formula for Positive Index
value = start + (index * step)
Example
r = range(5, 50, 5)
# Sequence: 5, 10, 15, 20, 25, 30, 35, 40, 45
r[0] # = 5 + (0 * 5) = 5
r[3] # = 5 + (3 * 5) = 20
r[8] # = 5 + (8 * 5) = 45
This makes range extremely memory-efficient. range(0, 1_000_000_000) uses the same small amount
of memory as range(0, 10), because no list is ever created.
Q9. in and not in Operators for Membership
The in and not in operators test whether a value belongs to a collection (list, tuple, set, dict, string,
range, etc.).
Syntax and Examples
value in collection # True if value found
value not in collection # True if value NOT found
'a' in 'apple' # True
5 in [1, 3, 5, 7] # True
10 not in range(5) # True
'key' in {'key': 1} # True (checks dict keys)
Performance Notes
• list / tuple: O(n) linear search
• set / dict: O(1) hash-based lookup (much faster)
• range: O(1) arithmetic check
Q10. in Operator with for Loop
The in keyword serves dual purpose in Python: membership testing (Q9) and iteration in for loops.
Iterating Over a Collection
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(fruit)
# Output: apple, banana, cherry
Iterating Over a String
for ch in 'Python':
print(ch, end=' ')
# Output: P y t h o n
Iterating Over a Dictionary
d = {'x': 1, 'y': 2}
for key in d: # iterates over keys
print(key, d[key])
The for...in construct works with any iterable object — any object that implements __iter__(). This
includes lists, tuples, sets, dicts, strings, files, range, and custom objects.
Q11. while Loop — Syntax, Control Flow, and Flowchart
Syntax
while condition:
# body
# (update to eventually make condition False)
Control Flow Description (Flowchart)
• START: Evaluate the condition.
• If condition is True: execute the loop body, then go back to START.
• If condition is False: exit the loop and continue with the next statement.
The condition is checked before every iteration (including the first). If it is False initially, the body never
executes.
Example
count = 1
while count <= 5:
print(count)
count += 1 # update — prevents infinite loop
# Output: 1 2 3 4 5
Warning: Always ensure the loop variable is modified in the body; otherwise the condition never
becomes False and the loop runs forever (infinite loop).
Q12. break vs continue in Loop Control
break — Exits the Loop Immediately
for i in range(10):
if i == 5:
break # exits loop
print(i)
# Output: 0 1 2 3 4
continue — Skips Current Iteration
for i in range(10):
if i % 2 == 0:
continue # skip even numbers
print(i)
# Output: 1 3 5 7 9
Key Differences
• break: Terminates the entire loop; execution jumps to the statement after the loop.
• continue: Terminates only the current iteration; the loop continues with the next iteration.
• Both can be used in for and while loops.
Q13. else Block with for and while Loops
Python allows an optional else clause after a for or while loop. This else block runs only if the loop
completes normally (without hitting a break statement).
With for Loop
for i in range(5):
if i == 10:
break
else:
print('Loop completed without break') # This prints
With while Loop — break Prevents else
n = 0
while n < 5:
if n == 3:
break
n += 1
else:
print('No break occurred') # This does NOT print
Practical Use Case — Searching
for item in collection:
if item == target:
print('Found!')
break
else:
print('Not found') # runs only if never broke out
Q14. Function Definition vs Invocation; Formal vs Actual Parameters
Definition
A function definition creates the function and specifies its name, parameters, and body. The code inside
does not execute at this point.
def greet(name, greeting='Hello'): # formal parameters
return f'{greeting}, {name}!'
Invocation (Call)
A function invocation (call) executes the function body with specific argument values.
result = greet('Alice', 'Hi') # actual arguments
print(result) # Hi, Alice!
Formal vs Actual Parameters
• Formal parameters: names listed in the def statement (name, greeting). They are local variables
inside the function.
• Actual arguments: values passed at the call site ('Alice', 'Hi'). They are bound to the formal
parameters.
Q15. return Statement and Tuple Packing with Multiple Return Values
Role of return
• Immediately exits the function and sends a value back to the caller.
• If return is missing, Python implicitly returns None.
def add(a, b):
return a + b # returns an int
def no_return():
x = 5 # no return statement
print(no_return()) # None
Tuple Packing with Multiple Returns
When a function returns multiple values separated by commas, Python automatically packs them into a
tuple — this is called tuple packing.
def stats(numbers):
return min(numbers), max(numbers), sum(numbers)/len(numbers)
lo, hi, avg = stats([3, 1, 4, 1, 5]) # tuple unpacking
print(lo, hi, avg) # 1 5 2.8
result = stats([3, 1, 4, 1, 5])
print(type(result)) # <class 'tuple'>
Q16. Local Scope and the global Keyword
Local Scope
Variables defined inside a function exist only within that function (local scope). They are created when
the function is called and destroyed when it returns.
def f():
x = 10 # local to f
print(x)
f() # 10
# print(x) # NameError: x not defined here
The global Keyword
To read or modify a module-level (global) variable inside a function, declare it with the global keyword.
Without it, an assignment inside the function creates a new local variable.
counter = 0
def increment():
global counter # refer to the global variable
counter += 1
increment()
increment()
print(counter) # 2
Q17. Direct and Indirect Recursion with Base Case
Recursion occurs when a function calls itself. Every recursive function must have a base case to stop
the recursion; without it, the function calls itself forever (stack overflow).
Direct Recursion
A function calls itself directly.
def factorial(n):
if n == 0: # base case
return 1
return n * factorial(n - 1) # recursive call
print(factorial(5)) # 120
Indirect Recursion
Function A calls function B, which calls function A again (a cycle).
def is_even(n):
if n == 0: return True
return is_odd(n - 1) # calls is_odd
def is_odd(n):
if n == 0: return False
return is_even(n - 1) # calls is_even
print(is_even(4)) # True
Role of Base Case
The base case is the condition that stops the recursion. Without it, the function recurses indefinitely
until Python raises RecursionError: maximum recursion depth exceeded.
Q18. Lambda (Anonymous Functions) and Closures (Nested Functions)
Lambda — Anonymous Function
A lambda is a small, anonymous function defined with the lambda keyword. It can take any number of
arguments but has exactly one expression.
square = lambda x: x ** 2
print(square(5)) # 25
# Useful inline with map, filter, sorted
nums = [3, 1, 4, 1, 5]
print(sorted(nums, key=lambda x: -x)) # [5, 4, 3, 1, 1]
Closure — Nested Function
A closure is an inner function that captures and remembers variables from its enclosing (outer)
function's scope, even after the outer function has finished executing.
def make_multiplier(factor):
def multiply(n): # inner function
return n * factor # captures 'factor'
return multiply # returns the closure
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
The inner function multiply forms a closure over factor. Each call to make_multiplier() creates an
independent closure with its own captured value of factor.
Q19. from, import, and as Keywords for Python Modules
import — Import Entire Module
import math
print([Link](16)) # 4.0 (must use module prefix)
from ... import — Import Specific Names
from math import sqrt, pi
print(sqrt(16)) # 4.0 (no prefix needed)
print(pi) # 3.141592653589793
as — Alias for Module or Name
import numpy as np # alias module
from datetime import datetime as dt # alias name
arr = [Link]([1, 2, 3])
today = [Link]()
Import All (Use with Caution)
from math import * # imports everything, can cause name conflicts
Best Practice: Use import module or from module import name. Avoid import * in production code as it
pollutes the namespace and makes it unclear where names come from.
Q20. The Circular Import Problem and How to Avoid It
A circular import occurs when two or more modules import each other, directly or indirectly, creating a
dependency cycle.
Example of the Problem
# module_a.py
from module_b import func_b # imports B at load time
def func_a(): return 'A'
# module_b.py
from module_a import func_a # imports A at load time
def func_b(): return 'B'
When Python loads module_a, it starts loading module_b, which tries to import from module_a — but
module_a is not fully loaded yet. This causes an ImportError or results in missing names.
Solutions
• Restructure code: Move shared code into a third module ([Link]) that both import from.
• Defer imports: Place the import inside the function body instead of at the top level.
def func_a():
from module_b import func_b # imported only when called
return func_b()
• Use import module instead of from module import name: This allows partial module state to
work.
# module_b.py
import module_a # safer — defers attribute lookup
def func_b(): return module_a.func_a()
• Architectural fix (best practice): Design modules with clear dependency directions (a DAG — no
cycles).
Programming in Python: Assignment 2 — Comprehensive Solutions