Python Full-Stack Interview Questions 41–45 (property, weakref, memoization, asyncio, coroutines)

This lesson answers five practical Python interview questions with friendly, beginner-focused explanations, real-world analogies, and runnable examples you can paste into an editor. Each section includes short lists to summarize key points and example output so you can verify behavior quickly.

41. What are Python property decorators?

Property decorators provide a clean way to expose methods as attribute- like accessors on an object. Instead of calling obj.get_x(), you can write obj.x. The pattern groups getter, setter, and deleter logic under a single attribute name while keeping control over validation or computed values. Think of a property as a locked cabinet drawer: you open it like an attribute, but behind the scenes the cabinet performs checks and logic.

  • @property defines a getter; @x.setter and@x.deleter define the matching setter/deleter.
  • Properties make APIs simpler and encapsulate validation while keeping attribute syntax.
  • Use properties when you need computed values or to add checks while keeping a simple attribute interface.

Example: a temperature class that stores Celsius internally but exposes Fahrenheit as a property.

class Temperature:
def __init__(self, celsius: float):
    self._celsius = float(celsius)

@property
def celsius(self) -> float:
    """Get temperature in Celsius."""
    return self._celsius

@celsius.setter
def celsius(self, value: float) -> None:
    """Set temperature ensuring a realistic range for demo purposes."""
    if value < -273.15:
        raise ValueError("Temperature cannot be below absolute zero")
    self._celsius = float(value)

@property
def fahrenheit(self) -> float:
    """Computed property: convert Celsius to Fahrenheit."""
    return (self._celsius * 9 / 5) + 32


t = Temperature(25)
print("Celsius:", t.celsius)
print("Fahrenheit:", t.fahrenheit)
t.celsius = 0
print("After set, Fahrenheit:", t.fahrenheit) 

Expected output:

# Output:

Celsius: 25.0
Fahrenheit: 77.0
After set, Fahrenheit: 32.0 

42. What are Python weak references?

A weak reference is a reference to an object that does not increase its reference count. This allows the object to be garbage-collected when there are no strong references left. Use weak references for caches, registries, or observer patterns where you don't want the referenced object kept alive solely because it appears in a container.

  • The weakref module provides weakref.ref and containers like WeakValueDictionary.
  • When the object is collected, a weak reference returns None(or a callback runs), letting you detect that the object is gone.
  • Weak refs are useful to avoid memory leaks when you maintain global caches or back-references.

Example: create an object, keep a weak reference, then delete the strong reference and observe collection.

import weakref

class Node:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"Node({self.name!r})"

# Create a Node instance and a weak reference to it
n = Node("alpha")
w = weakref.ref(n)  # weak reference does not increase reference count

print("Strong ref:", n)
print("Weak ref points to:", w())

# Remove strong reference; object may be garbage-collected
del n

# After possible garbage collection, weakref returns None if object is gone
print("Weak ref after deleting strong ref:", w())

Expected output (object may be collected immediately; typical output shown):

# Output:

Strong ref: Node('alpha')
Weak ref points to: Node('alpha')
Weak ref after deleting strong ref: None 

43. What is memoization in Python?

Memoization is an optimization that caches function results for given inputs so repeated calls return the stored result instead of recomputing. It's especially helpful for expensive or recursive calculations. A built-in convenience is functools.lru_cache, which provides an LRU (least recently used) cache for pure functions.

  • Memoization trades memory for CPU: store results to avoid repeat work.
  • Works best for functions without side effects and with limited input space.
  • Be mindful of cache size for long-running programs (use maxsize).

Example: Fibonacci with and without memoization to show speed and correctness.

from functools import lru_cache
import time

# Plain recursive Fibonacci function
def fib_plain(n):
    if n < 2:
        return n
    return fib_plain(n-1) + fib_plain(n-2)

# Memoized Fibonacci function using lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
    if n < 2:
        return n
    return fib_memo(n-1) + fib_memo(n-2)

n = 30

# Measure time for plain recursion
t0 = time.time()
print("plain:", fib_plain(n))
t1 = time.time()
print("plain time:", t1 - t0)

# Measure time for memoized recursion
t0 = time.time()
print("memo:", fib_memo(n))
t1 = time.time()
print("memo time:", t1 - t0)

Expected output (times vary; memo version is dramatically faster):

# Output (example):

plain: 832040
plain time: 0.8
memo: 832040
memo time: 0.001 

44. Explain async/await and the asyncio event loop in Python.

async and await are language tools to write asynchronous (non-blocking) code that looks sequential. The asyncioevent loop schedules and runs asynchronous tasks. Instead of creating many threads, async code cooperatively yields control at awaitpoints so a single-threaded loop can run many concurrent operations (especially IO-bound work).

  • Mark a function with async def to make it a coroutine.
  • Use await to pause until another awaitable completes.
  • The event loop (usually via asyncio.run()) manages the scheduling of awaitables.

Example: two simulated network operations run concurrently usingasyncio.sleep (non-blocking).

import asyncio
import time

# Define an asynchronous task
async def task(name, delay):
    print(f"{name} started")
    await asyncio.sleep(delay)  # non-blocking sleep
    print(f"{name} finished after {delay}s")
    return name

# Main coroutine to run multiple tasks concurrently
async def main():
    t0 = time.time()
    results = await asyncio.gather(
        task("A", 1),
        task("B", 2),
    )
    print("Results:", results)
    print("Total:", time.time() - t0)

# Run the main coroutine
asyncio.run(main())

Expected output (timings approximate; total ~2s not 3s):

# Output:

A started
B started
A finished after 1s
B finished after 2s
Results: ['A', 'B']
Total: 2.00... 

45. What are coroutines, tasks, and futures — how do they differ?

These are related asyncio concepts: a coroutine is an async function object (defined with async def), a task wraps a coroutine and schedules it on the event loop, and a future is a low-level object representing a result that will be available later. Tasks are a kind of future; both let you check for completion and retrieve results.

  • Coroutine: the awaitable code you write (not running until scheduled).
  • Task: created via asyncio.create_task()or by gather; it runs the coroutine in the loop.
  • Future: a promise-like object representing a value that will be set later; tasks are implemented on top of futures.

Example: create a coroutine but show the difference between awaiting it directly and scheduling it as a task.

import asyncio

# Define a simple coroutine
async def coro(name, delay):
    await asyncio.sleep(delay)
    return f"{name} done"

async def main():
    # Coroutine object (not yet running)
    c = coro("direct", 1)

    # Awaiting the coroutine runs it immediately on the event loop
    res_direct = await c
    print("direct await result:", res_direct)

    # Schedule as a Task (runs concurrently)
    t = asyncio.create_task(coro("task", 1))
    print("task scheduled:", isinstance(t, asyncio.Task))

    # Await the task later
    res_task = await t
    print("task result:", res_task)

    # Low-level Future example
    fut = asyncio.get_event_loop().create_future()
    # Set result manually (simulating work completion)
    fut.set_result("future value")
    print("future result:", fut.result())

# Run the main coroutine
asyncio.run(main())

Expected output:

# Output:

direct await result: direct done
task scheduled: True
task result: task done
future result: future value 

Closing tips: when explaining these in interviews, use simple analogies: properties = controlled drawers; weakrefs = windowed references that do not keep objects alive; memoization = a cheat-sheet for repeated work; asyncio = a single manager juggling many IO tasks; coroutine/task/future = plan, assigned worker, and delivery promise. Demonstrate one small example live — interviewers value clarity and a correct short demo.

🚀 Deep Dive With AI Scholar