Back to Notes

Python Benchmark Test

Python Benchmark Test

Scheduled: Saturday Apr 19, 2026 Duration: 2 hours 30 min total (timed per section) Setup: No notes. No IDE autocomplete. Use plain editor or paper. Goal: Know exactly where you stand before interview prep intensifies.


Pre-Test Rules

  • Start fresh. No warm-up.
  • Write answers first, THEN check wiki/run code.
  • Mark each answer: ✅ got it | ⚠️ partial | ❌ missed
  • Honest self-score matters more than a good score.

Section 1 — Python Internals Verbal (30 min)

Write 2-3 sentence answers. No code required.

Q1. What is the GIL? When does it matter and when does it not?

Q2. What's the difference between threading and multiprocessing in Python? When do you pick each?

Q3. What does async/await do under the hood? Is it parallel? What's the event loop?

Q4. What is a context manager? Give an example of writing one with __enter__/__exit__ AND one with @contextmanager.

Q5. What happens when you run this?

def append_to(element, to=[]):
    to.append(element)
    return to

print(append_to(1))
print(append_to(2))
print(append_to(3))

Why? How do you fix it?

Q6. Difference between copy.copy() and copy.deepcopy()? When does shallow copy cause bugs?

Q7. What is __slots__? When would you use it?

Q8. What does @property do? Write a class with a getter and setter using it.

Q9. What is the difference between @classmethod and @staticmethod? Give one use case each.

Q10. What does functools.wraps do and why is it needed in decorators?


Section 2 — Python Coding: DSA (60 min)

Implement in Python. No imports except standard library. Time yourself per problem.


Problem 1 (15 min) — Two Sum II (sorted array)

Given a sorted array and a target, return 1-indexed positions of two numbers that sum to target. Exactly one solution exists.

def two_sum(numbers: list[int], target: int) -> list[int]:
    pass

# Test cases
assert two_sum([2,7,11,15], 9) == [1,2]
assert two_sum([2,3,4], 6) == [1,3]
assert two_sum([-1,0], -1) == [1,2]

Expected: Two pointer, O(n) time, O(1) space.


Problem 2 (20 min) — Longest Substring Without Repeating Characters

def length_of_longest_substring(s: str) -> int:
    pass

# Test cases
assert length_of_longest_substring("abcabcbb") == 3
assert length_of_longest_substring("bbbbb") == 1
assert length_of_longest_substring("pwwkew") == 3
assert length_of_longest_substring("") == 0

Expected: Sliding window + set, O(n) time.


Problem 3 (25 min) — Valid Parentheses + Next Greater Element

3a. Valid parentheses (10 min):

def is_valid(s: str) -> bool:
    pass

assert is_valid("()") == True
assert is_valid("()[]{}")  == True
assert is_valid("(]") == False
assert is_valid("([)]") == False
assert is_valid("{[]}") == True

3b. Next Greater Element (15 min): Given array, find next greater element for each position. -1 if none exists.

def next_greater_element(nums: list[int]) -> list[int]:
    pass

assert next_greater_element([2,1,2,4,3]) == [4,2,4,-1,-1]
assert next_greater_element([1,3,2]) == [3,-1,-1]

Expected: Monotonic stack, O(n).


Section 3 — OOP + LLD (35 min)

No boilerplate lookup. Write from memory.


Problem 4 (35 min) — LRU Cache

Implement LRU Cache with O(1) get and put.

class LRUCache:
    def __init__(self, capacity: int):
        pass

    def get(self, key: int) -> int:
        # return value, or -1 if not found
        pass

    def put(self, key: int, value: int) -> None:
        # insert or update. evict LRU if over capacity.
        pass

# Test
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
assert cache.get(1) == 1       # returns 1
cache.put(3, 3)                # evicts key 2
assert cache.get(2) == -1      # not found
assert cache.get(3) == 3
cache.put(4, 4)                # evicts key 1
assert cache.get(1) == -1
assert cache.get(3) == 3
assert cache.get(4) == 4

Follow-up (verbal): How would you make this thread-safe?


Section 4 — Async + FastAPI Concepts (25 min)

Written answers only. No code required.

Q11. What's the difference between these two FastAPI routes? Which is better and why?

@app.get("/sync")
def sync_handler():
    result = requests.get("https://api.example.com/data")
    return result.json()

@app.get("/async")
async def async_handler():
    async with httpx.AsyncClient() as client:
        result = await client.get("https://api.example.com/data")
    return result.json()

Q12. What does Depends() do in FastAPI? Write the signature for a dependency that validates a JWT token and returns the current user.

Q13. You have a FastAPI endpoint that sends a welcome email after user signup. The email can take 2 seconds. How do you prevent this from blocking the response? Write the endpoint signature.

Q14. What is Pydantic's model_validator vs field_validator? When do you use each?

Q15. If a FastAPI endpoint raises an unhandled exception, what HTTP status code does the client receive by default? How do you return a custom error response with 422?


Scoring Rubric

Score each answer: 2 = full | 1 = partial | 0 = missed

SectionMax scoreYour score
Section 1 — Internals (Q1–Q10)20
Section 2 — Coding (P1–P3)12
Section 3 — LLD (P4)8
Section 4 — Async/FastAPI (Q11–Q15)10
Total50

What your score means

ScoreLevelAction
45–50Interview-readyMove on. Python is not your gap.
35–44Strong with gapsIdentify specific weak sections, 1 week polish
25–34Moderate2 weeks focused on weakest 2 sections
Below 25Needs workSpend 1 week on Python before other prep tracks

Answer Key (open AFTER test)

Section 1 Answers

Q1 — GIL: CPython's Global Interpreter Lock ensures only one thread executes Python bytecode at a time. Doesn't matter for I/O-bound work (threads release GIL while waiting on I/O). Kills CPU-bound parallelism — use multiprocessing instead.

Q2 — threading vs multiprocessing: threading shares memory, cheap to create, good for I/O-bound (HTTP calls, DB queries). multiprocessing spawns separate processes with own memory, bypasses GIL, good for CPU-bound (image processing, ML inference). Use concurrent.futures as unified API.

Q3 — async/await: Single-threaded event loop. Coroutines yield control at await points — not parallel, concurrent I/O only. Event loop runs one coroutine at a time but switches when one awaits. Don't put CPU work in async functions — it blocks the loop.

Q4 — context managers:

# Class-based
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, *args):
        print(f"Elapsed: {time.time() - self.start:.2f}s")

# Generator-based
from contextlib import contextmanager
@contextmanager
def timer():
    start = time.time()
    yield
    print(f"Elapsed: {time.time() - start:.2f}s")

Q5 — mutable default arg: Prints [1], [1,2], [1,2,3]. Default [] is evaluated once at function definition, shared across all calls. Fix: def append_to(element, to=None): to = to if to is not None else []

Q6 — copy vs deepcopy: copy.copy() copies the outer object, inner objects are still shared references. copy.deepcopy() recursively copies everything. Shallow causes bugs when nested mutable objects (lists inside dicts) are modified — original changes too.

Q7 — slots: Replaces per-instance __dict__ with fixed attribute list. Reduces memory (no dict overhead), faster attribute access. Use for data-heavy classes with millions of instances. Downside: can't add attributes dynamically.

Q8 — @property:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0: raise ValueError("Radius must be positive")
        self._radius = value

Q9 — classmethod vs staticmethod: @classmethod receives cls — used for alternative constructors (User.from_dict(data)). @staticmethod receives nothing — utility function that logically belongs to the class but doesn't need class/instance state (MathUtils.add(a, b)).

Q10 — functools.wraps: Without @wraps, wrapped function loses its __name__, __doc__, __qualname__. Matters for introspection, debugging, and FastAPI which uses __name__ to generate route names.

Section 2 Answers

P1 — Two Sum II:

def two_sum(numbers, target):
    l, r = 0, len(numbers) - 1
    while l < r:
        s = numbers[l] + numbers[r]
        if s == target:   return [l+1, r+1]
        elif s < target:  l += 1
        else:             r -= 1

P2 — Longest Substring:

def length_of_longest_substring(s):
    seen = set()
    l = res = 0
    for r, c in enumerate(s):
        while c in seen:
            seen.remove(s[l])
            l += 1
        seen.add(c)
        res = max(res, r - l + 1)
    return res

P3a — Valid Parentheses:

def is_valid(s):
    stack = []
    pairs = {')':'(', ']':'[', '}':'{'}
    for c in s:
        if c in '([{':
            stack.append(c)
        elif not stack or stack[-1] != pairs[c]:
            return False
        else:
            stack.pop()
    return not stack

P3b — Next Greater Element:

def next_greater_element(nums):
    res = [-1] * len(nums)
    stack = []  # indices
    for i, n in enumerate(nums):
        while stack and nums[stack[-1]] < n:
            res[stack.pop()] = n
        stack.append(i)
    return res

P4 — LRU Cache:

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache: return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(last=False)

Thread-safe: wrap get/put with threading.Lock().

Section 4 Answers

Q11 — sync vs async route: Async is better. Sync route blocks a thread while waiting on HTTP. Async releases the event loop during await, allowing other requests to proceed. FastAPI runs sync routes in a threadpool — wastes threads on I/O waiting.

Q12 — Depends:

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    user = await db.get_user(payload["user_id"])
    if not user: raise HTTPException(401, "Invalid token")
    return user

Q13 — Background task:

@app.post("/signup", status_code=201)
async def signup(user: UserIn, bg: BackgroundTasks):
    created = await create_user(user)
    bg.add_task(send_welcome_email, created.email)
    return created

Q14 — model_validator vs field_validator: field_validator validates a single field in isolation. model_validator runs after all fields are set — use when validation needs to cross-reference multiple fields (e.g., end_date > start_date).

Q15 — error handling: Unhandled exception → 500. Custom 422:

from fastapi import HTTPException
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_error(req, exc):
    return JSONResponse(status_code=422, content={"error": str(exc)})

Post-Test: Record Your Weak Areas

Date taken: ___________
Total score: ___ / 50

Weakest section: ___________
Specific gaps:
1.
2.
3.

Follow-up pages to re-read:
- [ ]
- [ ]