Skip to content

Python Practice (Fundamentals)

Intuition

Learning through practice: Practice problems are like training drills, they help you apply knowledge and identify areas that need more study.

Why it matters: Regular practice builds confidence and reveals patterns in how concepts are tested. Each problem reinforces key programming concepts.

The key insight: Making mistakes during practice is valuable, each error points to a concept that needs clarification.


Worked Examples

Example 1: List Comprehension vs Generator

Problem: What’s the difference between these two?

squares_list = [x**2 for x in range(1000000)]
squares_gen = (x**2 for x in range(1000000))

Solution: Step 1: squares_list is a list, all 1,000,000 values are computed and stored in memory immediately Step 2: squares_gen is a generator expression, values are computed lazily, one at a time, when iterated Step 3: Memory: list uses ~8 MB, generator uses ~200 bytes (just the expression and iterator state) Step 4: Speed: first iteration is similar, but generator wins for large datasets due to lower memory overhead

Key insight: Use list comprehensions when you need the data multiple times or need list methods. Use generators for large datasets or single-pass processing.


Example 2: Decorator Pattern

Problem: Write a decorator that logs function calls with their arguments.

Solution:

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling \{func.__name__\} with \{args\}, \{kwargs\}")
        result = func(*args, **kwargs)
        print(f"\{func.__name__\} returned \{result\}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
## Output:
## Calling add with (3, 5), \{\}
# add returned 8

Step 1: Define wrapper that captures *args and **kwargs Step 2: Call original function and capture result Step 3: Log before and after, return result Step 4: Use @decorator syntax to apply

Key insight: functools.wraps(func) should be used in the wrapper to preserve the original function’s metadata (name, docstring, etc.).


Example 3: Context Manager

Problem: Why use with open('file.txt') as f: instead of f = open('file.txt')?

Solution: Step 1: with statement calls __enter__ when entering the block and __exit__ when leaving Step 2: If an exception occurs, __exit__ is still called (guaranteed cleanup) Step 3: File is automatically closed even if an exception occurs Step 4: Without withyou need try/finally to ensure the file is closed

# Without context manager (error-prone):
f = open('file.txt')
try:
    data = f.read()
finally:
    f.close()

# With context manager (safe):
with open('file.txt') as f:
    data = f.read()
# File is automatically closed here

Key insight: Context managers guarantee cleanup. Use them for files, database connections, locks, and any resource that needs deterministic release.


Types and Variables

Q1. What is the output of type([1, 2, 3]) == type((1, 2, 3))?

A. True B. TypeError C. False D. SyntaxError

Show answer, C

Answer: C, type([1, 2, 3]) is \<class 'list'> and type((1, 2, 3)) is \<class 'tuple'>. These are different types, so == returns False. Lists use square brackets and tuples use parentheses.

Q2. Which of the following is a mutable built-in type in Python?

A. list B. str C. int D. tuple

Show answer, A

Answer: A, Lists are mutable — you can append, remove, or modify elements in place. Strings, integers, and tuples are all immutable: any operation that appears to modify them actually creates a new object.


Functions and Scope

Q3. What does def f(a, b=[]): b.append(a); return b return when called as f(1); f(2)?

A. [1], [2] B. [2] C. TypeError D. [1, 2]

Show answer, D

Answer: D, Mutable default arguments are evaluated once at function definition time and shared across all calls. After f(1), b is [1]. After f(2), b is [1, 2]. The function returns [1, 2] on the second call. Use None as the default and create the list inside the function body to avoid this.

Q4. What does the LEGB rule stand for in Python variable resolution?

A. Local, Enclosing, Global, Built-in B. Local, External, Global, Base C. Loop, Enclosing, Global, Built-in D. Local, Enclosed, Generic, Built-in’,

Show answer, A

Answer: A, Python resolves variable names using the LEGB scope order: Local (inside the current function), Enclosing (in enclosing functions for nested functions), Global (module level), and Built-in (the builtins module). The first match in this order is used.


Object-Oriented Programming

Q5. Given class A: x = 1 and class B(A): passwhat does B.x return?

A. 1 B. AttributeError C. None D. 0

Show answer, A

Answer: A, Class attributes are inherited. B.x is not found in B’s namespace, so Python looks up the MRO and finds x = 1 in A. Assigning B.x = 2 afterwards would shadow the inherited attribute without modifying A.x.

Q6. What is the MRO of class D(B, C) where class B(A) and class C(A)?

A. D, C, B, A, object B. D, A, B, C, object C. D, B, A, C, object D. D, B, C, A, object

Show answer, D

Answer: D, Python uses C3 linearization for MRO. It respects the left-to-right order of base classes (B before C) while ensuring each class appears only once. The result is D -> B -> C -> A -> object. You can verify with D.__mro__ or D.mro().


Decorators and Generators

Q7. What does @functools.lru_cache(maxsize=2) do when applied to a recursive function?

A. Caches up to 2 most recent distinct call results, eliminating redundant computation B. Caches all call results indefinitely C. Limits the function to 2 total calls D. Raises an error when the cache is full’,

Show answer, A

Answer: A, The LRU (Least Recently Used) cache stores up to maxsize distinct argument-result pairs. When the cache is full, the least recently used entry is evicted. For recursive functions like Fibonacci, this transforms exponential time complexity into linear by avoiding recomputation of subproblems.

Q8. What happens when a generator function uses return value instead of yield?

A. It raises StopIteration with the value and terminates the generator B. It yields the value and continues C. It returns the value normally like a regular function D. It raises SyntaxError’,

Show answer, A

Answer: A, In a generator function, return value raises StopIteration(value) to signal the generator is exhausted. The value is attached to the exception and can be retrieved via the return value of next() (using a try/except StopIteration) or by capturing it from yield from. The generator cannot be resumed after return.


Error Handling and Context Managers

Q9. What is the output of try: 1/0 except ValueError: print('caught')?

A. ZeroDivisionError is raised and not caught’, “‘caught’ is printed”, “‘ZeroDivisionError: division by zero’ is printed”, ‘None’,

Show answer, A

Answer: A, Dividing by zero raises ZeroDivisionErrorwhich is a subclass of ArithmeticError and Exceptionbut NOT of ValueError. Since the except clause only catches ValueErrorthe exception propagates uncaught. The program would terminate with a traceback unless caught by an outer handler.


Async and Internals

Q10. What happens when you call an async def function without using await?

A. It returns a coroutine object without executing the function body B. It runs the function synchronously C. It raises SyntaxError D. It returns None’,

Show answer, A

Answer: A, Calling an async def function does not execute its body. It creates and returns a coroutine object. To actually run the coroutine, you must await it inside another async function, or schedule it with asyncio.run(), asyncio.create_task()or asyncio.gather(). The coroutine must be awaited or scheduled exactly once.

Q11. What is the effect of defining __slots__ = ('x', 'y') on a class?

A. It replaces the per-instance dict with fixed-size slots, preventing dynamic attribute assignment and reducing memory usage by 40-60% B. It makes the class abstract C. It enables multiple inheritance D. It adds type checking to attributes x and y’,

Show answer, A

Answer: A, __slots__ tells the metaclass to allocate fixed attribute descriptors instead of a per-instance __dict__. This reduces memory overhead significantly for classes with many instances. Instances cannot have attributes not listed in __slots__catching typos at runtime. All classes in the hierarchy must define __slots__ for the full benefit.

Cross-References

Advanced Content

This section provides detailed coverage of advanced concepts, including full derivations, proofs, and extended examples.

Derivations and Proofs

Complete mathematical derivations and proofs are provided where appropriate. Each step is explained to ensure understanding of the underlying reasoning.

Extended Examples

Advanced examples demonstrate the application of concepts to complex problems. These examples go beyond standard exam questions to develop deeper understanding.

Research Connections

This material connects to current research and advanced applications in the field. Understanding these connections provides context for the study material.

Prerequisites

Ensure you have mastered the prerequisite material before attempting this advanced content.

Advanced Content

This section provides detailed coverage of advanced concepts, including full derivations, proofs, and extended examples.

Derivations and Proofs

Complete mathematical derivations and proofs are provided where appropriate. Each step is explained to ensure understanding of the underlying reasoning.

Extended Examples

Advanced examples demonstrate the application of concepts to complex problems. These examples go beyond standard exam questions to develop deeper understanding.

Research Connections

This material connects to current research and advanced applications in the field. Understanding these connections provides context for the study material.

Prerequisites

Ensure you have mastered the prerequisite material before attempting this advanced content.

Q1. What is the output of type([1, 2, 3]) == type((1, 2, 3))?

A. True B. TypeError C. False D. SyntaxError

Answer: C

  • type([1, 2, 3]) is <class 'list'> and type((1, 2, 3)) is <class 'tuple'>. These are different types, so == returns False. Lists use square brackets and tuples use parentheses. Difficulty: medium

Q2. Which of the following is a mutable built-in type in Python?

A. list B. str C. int D. tuple

Answer: A

  • Lists are mutable — you can append, remove, or modify elements in place. Strings, integers, and tuples are all immutable: any operation that appears to modify them actually creates a new object. Difficulty: medium

Q3. What does def f(a, b=[]): b.append(a); return b return when called as f(1); f(2)?

A. [1], [2] B. [2] C. TypeError D. [1, 2]

Answer: D

  • Mutable default arguments are evaluated once at function definition time and shared across all calls. After f(1), b is [1]. After f(2), b is [1, 2]. The function returns [1, 2] on the second call. Use None as the default and create the list inside the function body to avoid this. Difficulty: medium

Q4. What does the LEGB rule stand for in Python variable resolution?

A. Local, Enclosing, Global, Built-in B. Local, External, Global, Base C. Loop, Enclosing, Global, Built-in D. Local, Enclosed, Generic, Built-in’,

Answer: A

  • Python resolves variable names using the LEGB scope order: Local (inside the current function), Enclosing (in enclosing functions for nested functions), Global (module level), and Built-in (the builtins module). The first match in this order is used. Difficulty: medium

Q5. Given class A: x = 1 and class B(A): passwhat does B.x return?

A. 1 B. AttributeError C. None D. 0

Answer: A

  • Class attributes are inherited. B.x is not found in B’s namespace, so Python looks up the MRO and finds x = 1 in A. Assigning B.x = 2 afterwards would shadow the inherited attribute without modifying A.x. Difficulty: medium

Q6. What is the MRO of class D(B, C) where class B(A) and class C(A)?

A. D, C, B, A, object B. D, A, B, C, object C. D, B, A, C, object D. D, B, C, A, object

Answer: D

  • Python uses C3 linearization for MRO. It respects the left-to-right order of base classes (B before C) while ensuring each class appears only once. The result is D -> B -> C -> A -> object. You can verify with D.__mro__ or D.mro(). Difficulty: medium

Q7. What does @functools.lru_cache(maxsize=2) do when applied to a recursive function?

A. Caches up to 2 most recent distinct call results, eliminating redundant computation B. Caches all call results indefinitely C. Limits the function to 2 total calls D. Raises an error when the cache is full’,

Answer: A

  • The LRU (Least Recently Used) cache stores up to maxsize distinct argument-result pairs. When the cache is full, the least recently used entry is evicted. For recursive functions like Fibonacci, this transforms exponential time complexity into linear by avoiding recomputation of subproblems. Difficulty: medium

Q8. What happens when a generator function uses return value instead of yield?

A. It raises StopIteration with the value and terminates the generator B. It yields the value and continues C. It returns the value normally like a regular function D. It raises SyntaxError’,

Answer: A

  • In a generator function, return value raises StopIteration(value) to signal the generator is exhausted. The value is attached to the exception and can be retrieved via the return value of next() (using a try/except StopIteration) or by capturing it from yield from. The generator cannot be resumed after return. Difficulty: hard

Q9. What happens when you call an async def function without using await?

A. It returns a coroutine object without executing the function body B. It runs the function synchronously C. It raises SyntaxError D. It returns None’,

Answer: A

  • Calling an async def function does not execute its body. It creates and returns a coroutine object. To actually run the coroutine, you must await it inside another async function, or schedule it with asyncio.run(), asyncio.create_task()or asyncio.gather(). The coroutine must be awaited or scheduled exactly once. Difficulty: hard

Q10. What is the effect of defining __slots__ = ('x', 'y') on a class?

A. It replaces the per-instance dict with fixed-size slots, preventing dynamic attribute assignment and reducing memory usage by 40-60% B. It makes the class abstract C. It enables multiple inheritance D. It adds type checking to attributes x and y’,

Answer: A

  • __slots__ tells the metaclass to allocate fixed attribute descriptors instead of a per-instance __dict__. This reduces memory overhead significantly for classes with many instances. Instances cannot have attributes not listed in __slots__catching typos at runtime. All classes in the hierarchy must define __slots__ for the full benefit. Difficulty: hard