17 Modules • 425 Questions

Python Technical Interview Masterclass

Curated by Vinod Kumar Kayartaya (30+ Years Experience as Software Trainer, Senior Developer & AI Architect). Complete preparation suite spanning language syntax, CPython internals, async architectures, and system design.

Members-Only Access

This comprehensive 17-module Python interview masterclass is exclusively available to authenticated members. Please sign in with your Google account to unlock full questions, runnable code examples, and architectural explanations.

Module 01 of 17

Language Fundamentals & Syntax

25 In-Depth QuestionsBeginner to AdvancedRunnable Examples & Edge Cases
25 Questions
Beginner

01. Is Python an interpreted or compiled language? Explain the CPython execution model.

  • Difficulty: Beginner
  • Frequency: ⭐⭐⭐⭐⭐ (Universal fundamental)
  • Concept / Summary: Python is both. Source code (.py) is compiled into intermediate bytecode (.pyc), which is subsequently interpreted and executed by a virtual machine (the Python Virtual Machine, or PVM).
  • Detailed Explanation:
    1. Source Code (.py): You write high-level human-readable code.
    2. Parsing & AST: CPython's parser turns the source text into a parse tree, then builds an Abstract Syntax Tree (AST).
    3. Compilation to Bytecode: The compiler transforms the AST into low-level, platform-independent instructions known as bytecode (stored in __pycache__/*.pyc files). Bytecode instructions are stack-based machine codes (e.g., LOAD_NAME, BINARY_OP, RETURN_VALUE).
    4. PVM Execution: The Python Virtual Machine (an evaluation loop written in C, historically ceval.c) iterates through the bytecode instructions and executes native machine operations on the host CPU.
  • Code Example:
    PYTHON
    import dis
    
    def add(a, b):
        return a + b
    
    # Inspect bytecode generated by CPython
    dis.dis(add)
    # Output:
    #   RESUME                   0
    #   LOAD_FAST                0 (a)
    #   LOAD_FAST                1 (b)
    #   BINARY_OP                0 (+)
    #   RETURN_VALUE
    
  • Common Pitfalls & Edge Cases:
    • Mistakenly claiming Python is purely interpreted with no compilation stage.
    • Assuming .pyc files contain raw CPU machine code (they contain bytecode, not machine assembly).
  • Follow-up Questions:
    • What are alternative Python runtimes (PyPy, Jython, IronPython)?
    • How does PyPy achieve higher speeds than CPython? (Answer: JIT compiler vs. standard bytecode interpreter).

Intermediate

02. What does "everything is an object" mean in Python?

  • Difficulty: Intermediate

  • Frequency: ⭐⭐⭐⭐⭐

  • Concept / Summary: In Python, all values—primitive types (integers, floats, booleans), collections, functions, classes, instances, and even imported modules—are first-class objects instantiated from a class (or type) and managed via a C-level PyObject structure.

  • Detailed Explanation:
    In languages like C or Java, primitive types (e.g., int, char, boolean) are raw memory values distinct from reference objects. In Python, even 1 is an instance of <class 'int'>.

    Every object has:

    1. An Identity: Unique integer memory address (id(obj)).
    2. A Type: Defines supported methods and behavior (type(obj)).
    3. A Value: The data payload stored in memory.
    4. A Reference Count: For CPython's memory management.

    Because functions and classes are objects, you can pass them as arguments, return them from functions, attach attributes dynamically, and store them in collections.

  • Code Example:

    PYTHON
    def greet():
        return "Hello, World!"
    
    # Functions are objects: can assign attributes, pass as data
    greet.language = "English"
    greet.version = 1.0
    
    print(isinstance(greet, object))      # True
    print(isinstance(5, object))          # True
    print(greet.language)                 # English
    print(type(int))                      # <class 'type'>
    
  • Common Pitfalls & Edge Cases:

    • Thinking primitive types in Python are unboxed raw primitives.
    • Adding attributes to built-in objects directly (e.g., (1).custom = 'fail'), which raises AttributeError because built-in types have no __dict__ unless subclassed.
  • Follow-up Questions:

    • How is PyObject defined at the C level in cpython/Include/object.h?
    • What is the relationship between type and object? (isinstance(object, type) and isinstance(type, object) are both True).

Beginner / Intermediate

03. How is Python typed? Differentiate between Strongly vs. Weakly typed, and Statically vs. Dynamically typed.

  • Difficulty: Beginner / Intermediate
  • Frequency: ⭐⭐⭐⭐⭐
  • Concept / Summary: Python is dynamically typed (types are associated with runtime values, not variable names) and strongly typed (types are strictly enforced without implicit incompatible coercions).
  • Detailed Explanation:
    • Dynamic vs. Static:
      • Static Typing (C++, Java, Go): Variable types are declared and verified at compile time.
      • Dynamic Typing (Python, Ruby, JavaScript): Variables are mere labels bound to objects. The type is determined when the code executes. A variable name can bind to an int now and a str later.
    • Strong vs. Weak:
      • Strong Typing (Python, Rust): Implicit conversion between incompatible types is disallowed. "5" + 5 raises TypeError.
      • Weak Typing (JavaScript, C, PHP): The runtime automatically coerces types in operations (e.g., in JavaScript "5" + 5 evaluates to "55", "5" - 2 evaluates to 3).
    • Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." Python cares about whether an object implements a particular interface or method (__len__, __iter__), not its explicit inheritance hierarchy.
  • Code Example:
    PYTHON
    x = 42         # x references an integer
    x = "hello"    # Dynamic: x now references a string (valid)
    
    # Strongly typed: No implicit conversion
    try:
        result = "Price: $" + 100
    except TypeError as e:
        print(f"Caught expected error: {e}")
        # Fix: Explicit type casting
        result = "Price: $" + str(100)
    
  • Common Pitfalls & Edge Cases:
    • Confusing Python's typing module (type hints) with static enforcement. Type hints in Python do not enforce types at runtime; they are metadata for linters (mypy, pyright).
  • Follow-up Questions:
    • What is gradual typing?
    • What is the runtime performance cost of dynamic type dispatch?

Beginner

04. What is the difference between Mutable and Immutable objects? Name common examples of each.

  • Difficulty: Beginner

  • Frequency: ⭐⭐⭐⭐⭐

  • Concept / Summary: Immutable objects cannot be modified after creation; any modification yields a new object. Mutable objects allow altering their internal state in-place without changing their memory identity (id()).

  • Detailed Explanation:

    • Immutable Types: int, float, complex, bool, str, tuple, frozenset, bytes.
    • Mutable Types: list, dict, set, bytearray, custom user classes (by default).

    Immutability guarantees thread safety against unexpected side-effects, enables safe caching, and makes objects eligible to be hashable (usable as dictionary keys or set elements), provided all nested elements are also immutable.

  • Code Example:

    PYTHON
    # Immutable demonstration (str)
    s = "hello"
    old_id = id(s)
    s += " world"
    print(id(s) == old_id)  # False: A new string object was created
    
    # Mutable demonstration (list)
    lst = [1, 2, 3]
    old_id = id(lst)
    lst.append(4)
    print(id(lst) == old_id)  # True: Mutated in-place at the same memory address
    
  • Common Pitfalls & Edge Cases:

    • A tuple is immutable, but if it contains a mutable element (like a list), the nested list can be modified:
      PYTHON
      t = (1, [2, 3])
      t[1].append(4)  # Works: t is now (1, [2, 3, 4])
      # However, hash(t) raises TypeError because its content is not fully immutable!
      
  • Follow-up Questions:

    • What makes an object hashable in Python?
    • Why can't a list be used as a key in a dictionary?

Intermediate

05. Explain Python's parameter passing mechanism ("Pass by Object Reference" / "Pass by Assignment").

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐⭐
  • Concept / Summary: Python uses pass-by-assignment (also called call-by-object-reference). When an argument is passed to a function, the function parameter receives a copy of the reference (pointer) to the object, never a direct copy of the object itself.
  • Detailed Explanation:
    • If you pass an immutable object (int, str, tuple) and reassign the parameter inside the function, the parameter simply points to a new object. The caller's variable remains unchanged.
    • If you pass a mutable object (list, dict, set) and modify it in-place (e.g. lst.append()), the modification reflects in the caller's scope because both point to the same memory location.
    • If you reassign a mutable parameter inside the function (lst = [10, 20]), you break the binding; subsequent operations do not affect the caller.
  • Code Example:
    PYTHON
    def modify_in_place(items):
        items.append("new_item")  # Modifies caller's object
    
    def reassign_parameter(items):
        items = ["brand_new"]     # Local rebinding only
    
    data = ["initial"]
    modify_in_place(data)
    print(data)  # ['initial', 'new_item']
    
    reassign_parameter(data)
    print(data)  # Still ['initial', 'new_item']
    
  • Common Pitfalls & Edge Cases:
    • Believing Python is pass-by-value or pass-by-reference. It behaves like pass-by-value where the "value" being copied is the memory reference.
  • Follow-up Questions:
    • How do you guarantee a function cannot mutate an incoming list? (Pass tuple(data) or data.copy()).

Beginner / Intermediate

06. What is the difference between `==` and `is`? When should each be used?

  • Difficulty: Beginner / Intermediate
  • Frequency: ⭐⭐⭐⭐⭐
  • Concept / Summary: == checks for value equality (invoking __eq__), verifying if two objects have equivalent content. is checks for reference identity, verifying if two variables point to the exact same location in memory (id(a) == id(b)).
  • Detailed Explanation:
    • Use == when comparing values (numbers, strings, lists, custom objects).
    • Use is when comparing against singletons: x is None, x is not None, or checking sentinel objects.
    • Integer Caching / Small Integer Interning: CPython pre-allocates an array of integer objects for values between -5 and 256. Therefore, a = 250; b = 250; a is b evaluates to True, but a = 1000; b = 1000; a is b may evaluate to False (depending on code-block compilation scope). Never use is to compare numbers!
  • Code Example:
    PYTHON
    list_a = [1, 2, 3]
    list_b = [1, 2, 3]
    list_c = list_a
    
    print(list_a == list_b)  # True: identical content
    print(list_a is list_b)  # False: distinct objects in memory
    print(list_a is list_c)  # True: both reference the same object
    
    # Recommended usage:
    result = None
    if result is None:
        print("Safe singleton check")
    
  • Common Pitfalls & Edge Cases:
    • Writing if x is True: instead of if x: or if bool(x):.
    • Expecting 1000 is 1000 to be guaranteed across all contexts and Python implementations.
  • Follow-up Questions:
    • What is string interning and when does CPython intern strings automatically?

Intermediate

07. Explain Variable Scope and the LEGB Rule in Python. How do `global` and `nonlocal` differ?

  • Difficulty: Intermediate

  • Frequency: ⭐⭐⭐⭐⭐

  • Concept / Summary: Python resolves variable names by searching scopes in a strict hierarchical order known as LEGB: Local $\rightarrow$ Enclosing $\rightarrow$ Global $\rightarrow$ Built-in.

  • Detailed Explanation:

    1. L (Local): Names assigned inside a function or lambda (not marked global or nonlocal).
    2. E (Enclosing): Names in the local scope of enclosing (outer) functions, from innermost to outermost (closures).
    3. G (Global): Names defined at the top level of the current module file.
    4. B (Built-in): Built-in namespace containing functions and exceptions (len, range, Exception).

    Modifying Outer Variables:

    • By default, referencing an outer variable is allowed, but assigning to a variable creates a new local variable.
    • global <var>: Instructs Python that assignments to <var> target the module-level global scope.
    • nonlocal <var>: (Python 3+) Targets the nearest enclosing non-global scope (used in nested functions / closures).
  • Code Example:

    PYTHON
    def outer():
        count = 0
        def inner():
            nonlocal count  # Binds to outer's count
            count += 1
            return count
        return inner
    
    counter = outer()
    print(counter())  # 1
    print(counter())  # 2
    
    # UnboundLocalError trap
    x = 10
    def faulty_read_and_write():
        try:
            # Python sees assignment below and marks 'x' as local for entire function!
            print(x)
            x = 20
        except UnboundLocalError as e:
            print(f"Error: {e}")
    
    faulty_read_and_write()
    
  • Common Pitfalls & Edge Cases:

    • The UnboundLocalError: Any assignment to a name inside a function makes that name local across the entire body, even before the line of assignment.
  • Follow-up Questions:

    • Do if statements, for loops, or try blocks create their own scope in Python? (Answer: No, only functions, classes, and comprehensions/generator expressions create scope).

Beginner / Intermediate

08. What is the "Mutable Default Argument" trap in Python functions? How do you prevent it?

  • Difficulty: Beginner / Intermediate

  • Frequency: ⭐⭐⭐⭐⭐

  • Concept / Summary: In Python, default arguments are evaluated once at the moment the function is defined (def), not each time the function is called. Using a mutable object (list, dictionary, set) as a default creates a shared state across all invocations.

  • Detailed Explanation:
    When Python executes a def statement, it compiles the code object and binds default arguments into the function's __defaults__ attribute tuple. If that object is mutable, modifications persist across subsequent calls where the argument is omitted.

    The Idiomatic Solution: Use None as the sentinel default value, and instantiate the mutable object inside the function body.

  • Code Example:

    PYTHON
    # BUGGY CODE:
    def bad_append(val, lst=[]):
        lst.append(val)
        return lst
    
    print(bad_append(1))  # [1]
    print(bad_append(2))  # [1, 2] -- Unexpected retention!
    
    # IDIOMATIC SOLUTION:
    def safe_append(val, lst=None):
        if lst is None:
            lst = []
        lst.append(val)
        return lst
    
    print(safe_append(1))  # [1]
    print(safe_append(2))  # [2]
    
  • Common Pitfalls & Edge Cases:

    • Intentionally using mutable defaults for caching / memoization (discouraged; prefer @functools.lru_cache).
  • Follow-up Questions:

    • Where are default parameter values stored on a function object? (func.__defaults__ for positional, func.__kwdefaults__ for keyword-only).

Beginner

09. What constitutes Truthy and Falsy values in Python? How do `bool()` and `__bool__` / `__len__` work?

  • Difficulty: Beginner

  • Frequency: ⭐⭐⭐⭐

  • Concept / Summary: Every object in Python has an inherent boolean truth value. An object is evaluated as False if it meets defined "falsy" conditions; otherwise, it evaluates to True.

  • Detailed Explanation:
    Exhaustive List of Built-in Falsy Values:

    • Constants: None, False.
    • Numeric zeroes: 0, 0.0, 0j, Decimal(0), Fraction(0, 1).
    • Empty sequences & collections: "", (), [], {}, set(), range(0).

    Custom Object Evaluation Protocol:
    When bool(obj) is called:

    1. Python checks for the __bool__() method. If defined, it must return True or False.
    2. If __bool__() is not defined, Python falls back to __len__(). If len(obj) == 0, it is False; otherwise True.
    3. If neither method is defined, the object is considered True by default.
  • Code Example:

    PYTHON
    class CustomContainer:
        def __init__(self, data):
            self.data = data
    
        def __len__(self):
            return len(self.data)
    
    empty_box = CustomContainer([])
    full_box = CustomContainer([1, 2])
    
    print(bool(empty_box))  # False (via __len__)
    print(bool(full_box))   # True (via __len__)
    
  • Common Pitfalls & Edge Cases:

    • Defining __bool__ to return a non-boolean (raises TypeError).
    • Checking if len(items) > 0: instead of idiomatic if items:.
    • Checking if x: when x = 0 is a valid meaningful number (check if x is not None: instead).
  • Follow-up Questions:

    • What happens if both __bool__ and __len__ are implemented? (__bool__ takes priority).

Intermediate

10. How does Short-Circuit Evaluation work with `and` and `or`? What values do they actually return?

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: Python's logical operators and and or evaluate from left to right and stop evaluation immediately once the outcome is guaranteed. Crucially, they do not return boolean literals (True/False); they return the actual operand object that determined the outcome.
  • Detailed Explanation:
    • a and b: Evaluates a. If a is falsy, returns a immediately (short-circuits, b is never evaluated). If a is truthy, evaluates and returns b.
    • a or b: Evaluates a. If a is truthy, returns a immediately (short-circuits, b is never evaluated). If a is falsy, evaluates and returns b.
  • Code Example:
    PYTHON
    # Returning actual operand values
    print([] and "hello")   # [] (first operand is falsy)
    print([1, 2] and "hi")  # "hi" (first operand is truthy, returns second)
    print("default" or 42)  # "default" (first operand is truthy)
    print("" or 42)         # 42 (first operand is falsy)
    
    # Preventing exceptions via short-circuiting:
    user_input = None
    # Safe: len() is never executed if user_input is None
    if user_input is not None and len(user_input) > 0:
        print("Valid input")
    
    # CLASSIC INTERVIEW GOTCHA:
    num = 10
    # BUG: '10 == 1' is False, then evaluates truthiness of 2 -> 2 is Truthy!
    if num == 1 or 2 or 3:
        print("Always triggers!")  # Triggers even though num is 10!
    
    # CORRECT WAY:
    if num in (1, 2, 3):
        print("Won't trigger")
    
  • Common Pitfalls & Edge Cases:
    • Writing if val == 'a' or 'b': expecting it to check if val equals 'a' or 'b'. Because 'b' is a non-empty string, it evaluates to truthy, making the condition unconditionally true.
  • Follow-up Questions:
    • How does Python's any() and all() relate to short-circuiting? (Both short-circuit: any() on first truthy, all() on first falsy).

Advanced

11. Why does `t = ([],); t[0] += [1, 2]` raise a `TypeError` yet still mutate the list inside the tuple?

  • Difficulty: Advanced

  • Frequency: ⭐⭐⭐⭐⭐ (Famous Python brain-teaser)

  • Concept / Summary: The += operator on a list calls __iadd__, which mutates the list in-place (succeeds). Python then attempts to store the returned reference back into the tuple element (t[0] = ...), which raises a TypeError because tuples are immutable.

  • Detailed Explanation:
    Under the hood, the bytecode for target[index] += value performs:

    1. ROT_TWO / fetch element: retrieves t[0] (the list).
    2. In-place addition: executes list.__iadd__([1, 2]), which appends [1, 2] in-place and returns self (reference to the same list).
    3. STORE_SUBSCR: executes t[0] = <result of __iadd__>.
      Because t is a tuple, its tp_as_mapping->mp_ass_subscript slot is NULL. This immediately raises TypeError: 'tuple' object does not support item assignment.

    The mutation happened before the assignment was attempted!

  • Code Example:

    PYTHON
    t = ([1, 2],)
    try:
        t[0] += [3, 4]
    except TypeError as e:
        print(f"Exception raised: {e}")
    
    print(t)  # Output: ([1, 2, 3, 4],) -- The list WAS modified!
    
  • Common Pitfalls & Edge Cases:

    • Storing mutable objects inside tuples defeats the conceptual integrity of immutability.
  • Follow-up Questions:

    • What happens if you do t[0].extend([3, 4]) instead? (No error is raised because no reassignment t[0] = ... is attempted).

Intermediate / Advanced

12. How do Chained Comparisons work in Python? What makes `False == False in [False]` tricky?

  • Difficulty: Intermediate / Advanced

  • Frequency: ⭐⭐⭐⭐

  • Concept / Summary: In Python, comparison operators can be chained arbitrarily: a < b < c translates to (a < b) and (b < c), with the middle operand b evaluated only once. All comparison operators (including ==, <, >, in, is) have the same precedence and chain together.

  • Detailed Explanation:
    Consider:
    False == False in [False]
    Many candidates assume it evaluates as (False == False) in [False] $\rightarrow$ True in [False] $\rightarrow$ False.

    However, in Python, == and in are both comparison operators of equal precedence.
    Thus, Python chains them:
    (False == False) and (False in [False])

    1. False == False is True.
    2. False in [False] is True.
    3. True and True evaluates to True!
  • Code Example:

    PYTHON
    # Chained comparisons evaluation:
    print(False == False in [False])  # Evaluates to True!
    
    # Proof of equivalence:
    step1 = (False == False)
    step2 = (False in [False])
    print(step1 and step2)           # True
    
    # Single evaluation guarantee:
    def get_value():
        print("Called once!")
        return 5
    
    print(1 < get_value() < 10)
    # Output:
    # Called once!
    # True
    
  • Common Pitfalls & Edge Cases:

    • Writing 1 < x < 10 in languages like C/Java evaluates as (1 < x) < 10 (comparing boolean 0 or 1 with 10). Python explicitly avoids this trap via semantic chaining.
  • Follow-up Questions:

    • What does 1 == 1 == 1 evaluate to? ((1 == 1) and (1 == 1) $\rightarrow$ True). What about (1 == 1) == 1? (True == 1 $\rightarrow$ True, because bool subclasses int and True == 1).

Intermediate

13. What is the difference between Shallow Copy and Deep Copy?

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐⭐
  • Concept / Summary: A shallow copy constructs a new compound collection object, but inserts references to the child objects found in the original. A deep copy constructs a new compound object and recursively duplicates every nested object.
  • Detailed Explanation:
    • Assignment (b = a): No copy. Both variables share the exact same reference (b is a).
    • Shallow Copy (copy.copy(a), list.copy(), a[:]): Top-level container is cloned. If the container holds nested mutable structures (e.g. lists within a list), modifying a nested list affects both copies.
    • Deep Copy (copy.deepcopy(a)): Complete recursive duplication. Also handles circular references safely via an internal memoization dictionary.
  • Code Example:
    PYTHON
    import copy
    
    original = [[1, 2, 3], [4, 5, 6]]
    
    # Shallow Copy
    shallow = copy.copy(original)
    # Deep Copy
    deep = copy.deepcopy(original)
    
    original[0].append(999)
    
    print("Original:", original)  # [[1, 2, 3, 999], [4, 5, 6]]
    print("Shallow: ", shallow)   # [[1, 2, 3, 999], [4, 5, 6]] -> affected!
    print("Deep:    ", deep)      # [[1, 2, 3], [4, 5, 6]]       -> isolated!
    
  • Common Pitfalls & Edge Cases:
    • Deep copying objects with open sockets, file descriptors, or database connections will raise TypeError or unexpected state errors.
    • Performance: deepcopy has significant CPU and memory overhead on complex data graphs.
  • Follow-up Questions:
    • How can a custom class control its copy behavior? (Implement __copy__() and __deepcopy__(memo)).

Intermediate

14. What is the Walrus Operator (`:=`), and what are its best use cases?

  • Difficulty: Intermediate

  • Frequency: ⭐⭐⭐⭐ (Introduced in Python 3.8 / PEP 572)

  • Concept / Summary: The walrus operator (:=) is the assignment expression operator. It enables assigning values to variables inside expressions where statements were previously disallowed (such as if statements, while loops, and list comprehensions).

  • Detailed Explanation:
    Before Python 3.8, assignment was exclusively a statement (x = 10), unable to return a value. The walrus operator evaluates an expression, binds the result to a variable name, and returns that value simultaneously.

    Primary Benefits:

    • Reduces redundant computations (e.g., computing len(), regex matching, or querying an API).
    • Keeps variable scopes tighter and code cleaner without awkward pre-loop initialization.
  • Code Example:

    PYTHON
    import re
    
    # Use Case 1: Regex matching
    pattern = re.compile(r"user_(\d+)")
    text = "Found record: user_4821 in database"
    
    if match := pattern.search(text):
        user_id = match.group(1)
        print(f"User ID found: {user_id}")
    
    # Use Case 2: Reading streams / chunks
    # Instead of while True: chunk = f.read(1024); if not chunk: break
    # with open("data.txt") as f:
    #     while chunk := f.read(1024):
    #         process(chunk)
    
    # Use Case 3: List comprehension filtering + transformation
    raw_data = [" 12 ", "  ", "45", "invalid", "100"]
    # Calculate once, test, and store
    clean_ints = [val for item in raw_data if (val := item.strip()).isdigit()]
    print(clean_ints)  # ['12', '45', '100']
    
  • Common Pitfalls & Edge Cases:

    • Overusing it leads to cryptic, hard-to-read one-liners.
    • Walrus expressions inside comprehensions leak their target variable into the enclosing function scope (unlike regular comprehension loop variables).
  • Follow-up Questions:

    • Why was PEP 572 so controversial in the Python community? (Led to Guido van Rossum stepping down as BDFL).

Intermediate

15. Explain Positional-Only (`/`) and Keyword-Only (`*`) parameters in function definitions.

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary:
    • Parameters before / are positional-only (cannot be passed by name).
    • Parameters after * are keyword-only (must be passed by name).
    • Parameters between / and * can be passed either way.
  • Detailed Explanation:
    • Syntax: def func(pos_only, /, standard, *, kw_only): pass
    • Why Positional-Only (/)? Introduced in Python 3.8 (PEP 570).
      • Allows library authors to rename internal parameter names in future versions without breaking callers.
      • Mimics C-accelerated built-ins like len(obj) (you cannot call len(obj=x)).
    • Why Keyword-Only (*)? Introduced in Python 3.0 (PEP 3102).
      • Enforces readability at the call site for flags and configuration options (e.g. open("file", mode="r", encoding="utf-8")).
  • Code Example:
    PYTHON
    def configure_server(host, port, /, timeout=30, *, secure=False, cert=None):
        return f"Server({host}:{port}, timeout={timeout}, secure={secure})"
    
    # Valid calls:
    print(configure_server("localhost", 8080))
    print(configure_server("localhost", 8080, 60, secure=True))
    
    # INVALID CALLS:
    # configure_server(host="localhost", port=8080)
    # -> TypeError: configure_server() got some positional-only arguments passed as keyword arguments: 'host, port'
    
    # configure_server("localhost", 8080, 30, True)
    # -> TypeError: configure_server() takes from 2 to 3 positional arguments but 4 were given
    
  • Common Pitfalls & Edge Cases:
    • Forgetting that bare * takes no arguments itself—it is simply a delimiter separating standard parameters from keyword-only parameters.
  • Follow-up Questions:
    • How do *args and **kwargs interact with / and *?

Advanced

16. How does Structural Pattern Matching (`match-case`) work in Python 3.10+?

  • Difficulty: Advanced
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: Introduced in Python 3.10 (PEP 634), match-case is not merely a switch-case statement. It performs structural pattern matching, validating shapes, destructuring sequences, dictionaries, and objects, and binding variables conditionally with guard expressions.
  • Detailed Explanation:
    Unlike traditional C-style switch statements that only compare scalar values:
    • Supports sequence matching ([first, *rest]).
    • Supports mapping matching ({"status": 200, "data": data}).
    • Supports class/type pattern matching (Point(x, y)).
    • Supports guards via if clauses.
    • The wildcard _ acts as the default / fall-through arm.
  • Code Example:
    PYTHON
    def handle_command(command):
        match command.split():
            case ["quit"]:
                return "Exiting application"
            case ["go", ("north" | "south" | "east" | "west") as direction]:
                return f"Moving {direction}"
            case ["get", item]:
                return f"Picked up {item}"
            case ["drop", *items] if len(items) > 1:
                return f"Dropping multiple items: {', '.join(items)}"
            case _:
                return "Invalid command"
    
    print(handle_command("go north"))       # Moving north
    print(handle_command("drop sword key"))  # Dropping multiple items: sword, key
    
  • Common Pitfalls & Edge Cases:
    • Using an unqualified variable name in a case (case status:), which captures the value and binds it rather than comparing against an existing variable. To match a constant, use dotted attributes (case HttpStatus.OK:).
  • Follow-up Questions:
    • How does pattern matching handle custom classes? (Using __match_args__).

Intermediate

17. What are the differences between `None`, `...` (Ellipsis), `NotImplemented`, and `pass`?

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary:
    • None: A singleton object representing the absence of a value or a default return value.
    • ... (Ellipsis): A singleton object commonly used for multidimensional array slicing (NumPy) or type hints (callable arguments Callable[..., int]).
    • NotImplemented: A special singleton returned by rich comparison dunders (__eq__, __lt__) to signal to Python that it should attempt the reflected operation on the other operand.
    • pass: A syntax keyword (statement) representing a no-op placeholder.
  • Detailed Explanation:
    • NotImplemented vs NotImplementedError:
      • NotImplemented is a value (return NotImplemented). It tells CPython: "I don't know how to compare with this type; ask the other object via its reflected method (e.g. __gt__ for __lt__). If both fail, raise TypeError."
      • NotImplementedError is an exception subclass of RuntimeError (raise NotImplementedError), used in abstract methods.
  • Code Example:
    PYTHON
    class Vector:
        def __init__(self, x):
            self.x = x
    
        def __eq__(self, other):
            if not isinstance(other, Vector):
                return NotImplemented  # Allows other to attempt its own __eq__
            return self.x == other.x
    
    # Ellipsis in type hinting and slicing:
    from typing import Callable
    Handler = Callable[..., str]  # Accepts any arguments, returns str
    
    # pass vs ... as body
    def stub_pass():
        pass
    
    def stub_ellipsis():
        ...
    
  • Common Pitfalls & Edge Cases:
    • Raising NotImplemented instead of NotImplementedError (raise NotImplemented raises a TypeError on modern Python because NotImplemented is not an Exception).
  • Follow-up Questions:
    • How does NumPy utilize ... for tensor indexing? (tensor[..., 0]).

Intermediate / Advanced

18. How does the `del` keyword work? Does `del obj` immediately free memory?

  • Difficulty: Intermediate / Advanced
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: del does not directly deallocate memory or destroy objects. It unbinds a name from the current namespace and decrements the object's reference count by 1. The object is only destroyed when its reference count drops to zero (or during cyclic garbage collection).
  • Detailed Explanation:
    1. del x: Removes the symbol x from the local or global namespace dictionary (locals() or globals()).
    2. The target object's reference counter ob_refcnt is decremented.
    3. If ob_refcnt == 0, CPython triggers tp_dealloc, invoking __del__() (if implemented) and immediately reclaiming the memory via PyObject_Free.
    4. If other variables, lists, or cycles still reference the object, it stays alive in memory.
  • Code Example:
    PYTHON
    class Resource:
        def __del__(self):
            print("Memory deallocated!")
    
    a = Resource()
    b = a  # Two references (a and b)
    
    del a  # Reference count drops to 1; NOT destroyed yet
    print("After del a")
    
    del b  # Reference count drops to 0; triggers __del__ immediately
    print("After del b")
    # Output:
    # After del a
    # Memory deallocated!
    # After del b
    
  • Common Pitfalls & Edge Cases:
    • Relying on __del__() for critical resource cleanups (e.g. database connections, file locks). If cyclic references exist or the interpreter exits abruptly, __del__ might be delayed or skipped. Always prefer Context Managers (with).
  • Follow-up Questions:
    • What happens when two objects hold references to each other (cyclical reference) and you del both variables? (The cyclic garbage collector must detect and collect them).

Advanced

19. How are strings represented in Python 3, and how does string interning work?

  • Difficulty: Advanced
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: Since Python 3.3 (PEP 393 - Flexible String Representation), strings are stored in memory using the most compact representation necessary: Latin-1 (1 byte per char), UCS-2 (2 bytes per char), or UCS-4 (4 bytes per char), depending on the maximum code point in the string.
  • Detailed Explanation:
    • Memory Efficiency: If a string contains only ASCII/Latin-1 characters, Python uses 1 byte per character instead of wasting 4 bytes per character.
    • Immutability: Python strings are immutable. Any modification (concatenation, slicing, replace()) allocates a new string.
    • String Interning: CPython automatically interns string constants that look like valid identifiers (alphanumeric + underscores). Interned strings point to the same memory address, allowing the interpreter to replace costly character-by-character string comparisons (strcmp) with simple pointer comparisons (is).
    • You can manually intern strings using sys.intern(s).
  • Code Example:
    PYTHON
    import sys
    
    s1 = "hello_world"
    s2 = "hello_world"
    print(s1 is s2)  # True: automatically interned identifier-like string
    
    # Non-identifier strings are generally not interned automatically at runtime:
    str_a = "".join(["hello", "!", "!"])
    str_b = "".join(["hello", "!", "!"])
    print(str_a == str_b)  # True (equal values)
    print(str_a is str_b)  # False (different memory addresses)
    
    # Explicit interning:
    str_a = sys.intern(str_a)
    str_b = sys.intern(str_b)
    print(str_a is str_b)  # True: now identical pointers
    
  • Common Pitfalls & Edge Cases:
    • Concatenating strings in a loop via += causes $O(N^2)$ memory copying. The idiomatic high-performance pattern is ''.join(list_of_strings).
  • Follow-up Questions:
    • What is the difference between str and bytes in Python 3?

Intermediate

20. What are Augmented Assignment Operators (`+=`, `*=`) and how do they differ from binary operations?

  • Difficulty: Intermediate
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: Augmented assignment operators (+=, -=, *=, etc.) attempt to modify the target operand in-place via in-place dunder methods (__iadd__, __imul__). If in-place modification is not supported (e.g., on immutable types), they fall back to standard binary operators (__add__, __mul__), reassigning the variable to a newly created object.
  • Detailed Explanation:
    • For Mutable Objects (list):
      • lst += [4] invokes lst.__iadd__([4]), mutating the existing list in-place (equivalent to lst.extend([4])).
      • lst = lst + [4] invokes list.__add__, allocating a brand-new list and copying elements from both.
    • For Immutable Objects (int, str, tuple):
      • x += 1 cannot mutate the integer. It evaluates x + 1 (a new integer) and rebinds x to it.
  • Code Example:
    PYTHON
    # Mutable: += modifies in-place
    list1 = [1, 2]
    list2 = list1
    list1 += [3]
    print(list1 is list2)  # True: list2 sees the change!
    
    # Mutable: + creates a new object
    list1 = [1, 2]
    list2 = list1
    list1 = list1 + [3]
    print(list1 is list2)  # False: list1 was rebound to a new list
    
  • Common Pitfalls & Edge Cases:
    • Assuming a += b is always syntactically identical to a = a + b.
  • Follow-up Questions:
    • What happens when a class defines __add__ but not __iadd__? (Python falls back to __add__).

Intermediate / Advanced

21. How does Python prevent integer overflow? What is arbitrary-precision arithmetic?

  • Difficulty: Intermediate / Advanced
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: In Python 3, integers have arbitrary precision (unlimited size). Python integers are not restricted by hardware registers (like 32-bit or 64-bit limits in C/Java); their maximum size is limited solely by available system memory.
  • Detailed Explanation:
    • In CPython, an integer is represented internally as a C struct: PyLongObject.
    • The actual integer magnitude is stored as an array of "digits" (typically 30 bits per digit on 64-bit platforms).
    • When an arithmetic calculation exceeds a single digit, CPython allocates additional digits dynamically and carries over the arithmetic using multi-precision algorithms.
    • In Python 2, there were two distinct integer types: int (32/64-bit) and long (arbitrary precision). Python 3 unified them under int.
  • Code Example:
    PYTHON
    import sys
    
    # Extremely large integer calculation (would overflow 64-bit integers)
    big_num = 2 ** 200
    print(big_num)
    # 1606938044258990275541962092341162602522202993782792835301376
    
    # Inspecting memory size scaling
    print(sys.getsizeof(0))        # 28 bytes (base PyLongObject overhead)
    print(sys.getsizeof(1))        # 28 bytes
    print(sys.getsizeof(2**30))    # 32 bytes (allocated extra digit)
    print(sys.getsizeof(2**60))    # 36 bytes
    
  • Common Pitfalls & Edge Cases:
    • Floats in Python do not have arbitrary precision! Floats follow IEEE 754 double precision (64-bit), so 1e308 * 10 results in OverflowError: float overflow or inf.
  • Follow-up Questions:
    • If Python integers cannot overflow, why do libraries like NumPy have fixed 32-bit and 64-bit integer types that can overflow? (For raw CPU SIMD optimization and C-speed vectorization).

Intermediate

22. Why should `assert` never be used for security checks or user input validation?

  • Difficulty: Intermediate

  • Frequency: ⭐⭐⭐⭐⭐

  • Concept / Summary: When Python is run in optimized mode (e.g. python -O or python -OO), the internal constant __debug__ is set to False, and the interpreter completely strips out all assert statements from the generated bytecode.

  • Detailed Explanation:
    assert <condition>, <message> compiles to bytecode that is completely bypassed in optimized production environments.

    If you use assert to verify user permissions, authenticate passwords, or validate payload schemas, running with -O completely disables those checks, opening severe security vulnerabilities.

    Proper Use of assert:

    • Internal invariant checks (sanity testing code paths that should logically be impossible).
    • Unit tests (pytest leverages AST rewriting on assert).
    • For validation, always raise explicit exceptions (ValueError, PermissionError).
  • Code Example:

    PYTHON
    # DANGEROUS CODE:
    def delete_user(admin_user, target_user_id):
        # IF RUN WITH python -O, THIS CHECK DISAPPEARS!
        assert admin_user.is_admin, "Unauthorized access!"
        database.delete(target_user_id)
    
    # SECURE, PRODUCTION-GRADE CODE:
    def delete_user_secure(admin_user, target_user_id):
        if not admin_user.is_admin:
            raise PermissionError("Unauthorized access!")
        database.delete(target_user_id)
    
  • Common Pitfalls & Edge Cases:

    • Writing assert(condition, message) with parentheses. In Python, (condition, message) is parsed as a 2-element tuple. A non-empty tuple is always truthy, so assert (False, "Error") will never trigger an AssertionError!
  • Follow-up Questions:

    • What is the difference between -O and -OO flags in Python? (-OO strips both asserts and docstrings).

Beginner / Intermediate

23. What are the key architectural differences between Python 2 and Python 3?

  • Difficulty: Beginner / Intermediate
  • Frequency: ⭐⭐⭐⭐
  • Concept / Summary: Python 3 (released in 2008) intentionally introduced backward-incompatible changes to fix core design flaws of Python 2. Key changes include Unicode by default, print becoming a function, true integer division, iterator-based built-ins, and unified integer types.
  • Detailed Explanation:
    1. Text vs. Binary Data:
      • Python 2: str was ASCII/bytes; unicode was a distinct type. Mixing them caused silent conversions and notorious UnicodeDecodeError bugs.
      • Python 3: str is strictly Unicode; bytes is raw 8-bit binary data. Explicit .encode() and .decode() are required.
    2. Print Statement vs Function:
      • Python 2: print "Hello" (statement).
      • Python 3: print("Hello", end="\n", file=...) (function).
    3. Division Operator (/):
      • Python 2: 5 / 2 performed floor integer division (2).
      • Python 3: 5 / 2 performs true float division (2.5). Floor division requires 5 // 2.
    4. Iterators vs Lists:
      • Python 2: range(), zip(), map(), dict.keys() returned concrete list objects (costly memory consumption).
      • Python 3: Return memory-efficient lazy iterators or view objects.
    5. Exception Handling:
      • Python 2: raise Exception, "message", except Exception, e.
      • Python 3: raise Exception("message"), except Exception as e.
  • Code Example:
    PYTHON
    # Python 3 division & Unicode guarantees
    print(5 / 2)       # 2.5 (True division)
    print(5 // 2)      # 2   (Floor division)
    
    # Explicit bytes vs string boundary
    raw_bytes = b"hello"
    text = raw_bytes.decode("utf-8")  # Explicit decoding required
    print(type(text))                 # <class 'str'>
    
  • Common Pitfalls & Edge Cases:
    • Assuming Python 2 code will run in Python 3 with simple formatting tools without auditing binary/text data handling.
  • Follow-up Questions:
    • How did Python 3 handle dictionary ordering? (Python 3.6+ guarantees insertion order retention as a language specification in 3.7+).

Beginner

24. How does the Ternary Operator work in Python, and what are its common traps?

  • Difficulty: Beginner

  • Frequency: ⭐⭐⭐⭐

  • Concept / Summary: Python's conditional expression (ternary operator) uses the syntax:
    result = <value_if_true> if <condition> else <value_if_false>.
    It evaluates lazily—only the branch matching the condition is evaluated.

  • Detailed Explanation:
    Unlike C/Java (condition ? true_val : false_val), Python places the true value first, followed by the condition, followed by the else clause.

    Short-Circuit Execution:
    If the condition is True, <value_if_false> is never evaluated (and vice versa), which prevents unintended errors.

  • Code Example:

    PYTHON
    status_code = 200
    message = "Success" if status_code == 200 else "Failure"
    
    # Lazy evaluation prevents ZeroDivisionError
    divisor = 0
    result = 100 / divisor if divisor != 0 else 0
    print(result)  # 0
    
    # ANTI-PATTERN / TRAP (Tuple Indexing Trick):
    # (false_val, true_val)[condition]
    # TRAP: This evaluates BOTH branches eagerly!
    try:
        bad_res = (0, 100 / divisor)[divisor != 0]  # Raises ZeroDivisionError!
    except ZeroDivisionError:
        print("Tuple trick failed because both branches are evaluated!")
    
  • Common Pitfalls & Edge Cases:

    • Nested ternary expressions become difficult to read:
      a if cond1 else b if cond2 else c (Use standard if-elif-else when logic gets complex).
  • Follow-up Questions:

    • What is the precedence of conditional expressions compared to arithmetic and logical operators?

Advanced / Modern

25. What are the major syntax additions introduced in Python 3.12 and 3.13?

  • Difficulty: Advanced / Modern
  • Frequency: ⭐⭐⭐⭐ (High value for demonstrating up-to-date modern Python knowledge)
  • Concept / Summary: Python 3.12 and 3.13 introduced significant syntactic modernizations:
    1. PEP 701 (Python 3.12): Syntactic formalization of f-strings (nested quotes, backslashes, expressions, and inline comments now allowed).
    2. PEP 695 (Python 3.12): Native Type Parameter Syntax (type statement and generic functions without TypeVar).
    3. PEP 703 (Python 3.13): Experimental Free-threaded Python (no-GIL build).
    4. Improved Error Tracebacks: Precise underline locations highlighting exact failing expressions.
  • Detailed Explanation:
    • F-string Enhancements (PEP 701):
      Previously, f-strings could not reuse the quote characters of the outer f-string, could not contain backslashes (\n), and could not contain # comments. In Python 3.12+, f-strings use the standard PEG parser without arbitrary limitations.
    • Type Parameter Syntax (PEP 695):
      Replaces boilerplate T = TypeVar('T') with clean, native generic syntax: def first[T](items: list[T]) -> T: ... and type Number = int | float.
  • Code Example:
    PYTHON
    # 1. Advanced F-strings (Python 3.12+)
    items = ["apple", "banana", "cherry"]
    # Nested quotes and backslashes inside f-string:
    formatted = f"Items: {', \n'.join([f'{item.upper()}' for item in items])}"
    print(formatted)
    
    # 2. Native Type Parameter Syntax (Python 3.12+)
    # Instead of: T = TypeVar('T'); def get_first(lst: list[T]) -> T:
    def get_first[T](lst: list[T]) -> T | None:
        return lst[0] if lst else None
    
    # Type alias statement:
    type Point2D = tuple[float, float]
    
  • Common Pitfalls & Edge Cases:
    • Using PEP 695 syntax in projects required to support older runtimes (< 3.12) causes immediate SyntaxError.
  • Follow-up Questions:
    • What are the architectural implications of free-threaded Python (PEP 703) on extension modules and thread safety?