Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Exceptions & Modules
PYTHON

Exceptions & Modules

Practice 8 Python questions covering exception handling, modules, imports, packaging, and Python error behavior.

1. What is the output of the following try-except-else-finally block?

def check():
    try:
        return 1
    finally:
        return 2

print(check())

Output: 2

The finally block has one unbreakable guarantee: its code always runs before the function actually returns. And if that code contains a return statement, the finally’s return wins.

Execution goes like this: the try block hits return 1. But before the function can hand 1 back to the caller, Python runs the finally block — as it must. The finally contains return 2, which establishes a new return value. That new value is what the function returns.

So the function returns 2, and the return 1 in the try is effectively discarded.

This is a deliberate trap: it looks like 1 is coming out, but a return inside finally overrides any return in the try or except. It’s why linters flag return inside finally — the behavior is nearly always a bug. The interview answer is simply 2.

Answer:

2

The finally block has one unbreakable guarantee: its code always runs before the function actually returns. And if that code contains a return statement, the finally’s return wins.

Execution goes like this: the try block hits return 1. But before the function can hand 1 back to the caller, Python runs the finally block — as it must. The finally contains return 2, which establishes a new return value. That new value is what the function returns.

So the function returns 2, and the return 1 in the try is effectively discarded.

This is a deliberate trap: it looks like 1 is coming out, but a return inside finally overrides any return in the try or except. It’s why linters flag return inside finally — the behavior is nearly always a bug. The interview answer is simply 2.

2. What happens when key modification occurs on a dictionary while iterating over it directly?

Answer: Python raises RuntimeError: dictionary changed size during iteration.

Direct iteration over a dict — for k in d: — is sensitive to the dictionary’s size. If you add or delete keys while iterating, the dict’s internal structure changes in a way the iterator can’t safely track. Python detects this and raises RuntimeError rather than producing unpredictable results.

Two details matter:

  • Changing values is fine — d[k] = new_value doesn’t change the set of keys, so iteration proceeds normally.
  • Adding or removing keys is the problem. The safe pattern is to iterate over a snapshot of the keys — for k in list(d): — and modify the original dict inside the loop.

The interview answer: adding or deleting keys during direct dict iteration raises RuntimeError: dictionary changed size during iteration.

Answer:

Python raises RuntimeError: dictionary changed size during iteration.

Direct iteration over a dict — for k in d: — is sensitive to the dictionary’s size. If you add or delete keys while iterating, the dict’s internal structure changes in a way the iterator can’t safely track. Python detects this and raises RuntimeError rather than producing unpredictable results.

Two details matter:

  • Changing values is fine — d[k] = new_value doesn’t change the set of keys, so iteration proceeds normally.
  • Adding or removing keys is the problem. The safe pattern is to iterate over a snapshot of the keys — for k in list(d): — and modify the original dict inside the loop.

The interview answer: adding or deleting keys during direct dict iteration raises RuntimeError: dictionary changed size during iteration.

3. What happens if an exception is raised inside a context manager’s enter method?

Answer: __exit__ is not called, and the exception propagates up.

The with statement’s contract: __enter__ runs first and returns the managed object; then the block body runs; then __exit__ is guaranteed to run once the block has been entered. If __enter__ itself fails, the block was never entered, so there is nothing to clean up via __exit__.

Sequence: with calls __enter__. If __enter__ raises, the exception immediately propagates to the caller. The with body never executes, and __exit__ is never invoked.

This makes sense — __enter__ is typically acquiring a resource (opening a file, acquiring a lock, starting a transaction). If acquisition fails, there’s nothing acquired, so the exit/cleanup path has no reason to run. The exception simply travels outward.

The interview answer: an exception in __enter__ skips the block and __exit__ entirely; the exception propagates.

Answer:

__exit__ is not called, and the exception propagates up.

The with statement’s contract: __enter__ runs first and returns the managed object; then the block body runs; then __exit__ is guaranteed to run once the block has been entered. If __enter__ itself fails, the block was never entered, so there is nothing to clean up via __exit__.

Sequence: with calls __enter__. If __enter__ raises, the exception immediately propagates to the caller. The with body never executes, and __exit__ is never invoked.

This makes sense — __enter__ is typically acquiring a resource (opening a file, acquiring a lock, starting a transaction). If acquisition fails, there’s nothing acquired, so the exit/cleanup path has no reason to run. The exception simply travels outward.

The interview answer: an exception in __enter__ skips the block and __exit__ entirely; the exception propagates.

4. How do you trigger explicit garbage collection manually in Python?

Answer: gc.collect().

Python’s memory is managed primarily by reference counting — when an object’s reference count drops to zero, it’s freed immediately, no collector needed. But reference counting can’t handle reference cycles: two objects referencing each other never hit zero, so they’d leak.

The gc module’s collector handles those cycles. Normally it runs automatically on a schedule. To force a run, call gc.collect(). It performs a cycle detection pass, frees unreachable cyclic objects, and returns the number of objects it collected.

When would you call it manually? On resource-constrained systems where you want the collection to happen at a controlled time, or in long-running processes where the automatic schedule is too slow. In normal application code it’s rarely necessary.

The interview answer: gc.collect() forces the cyclic garbage collector to run now. (Note: sys.gc doesn’t exist, and del just removes a reference — it doesn’t collect.)

Answer:

gc.collect().

Python’s memory is managed primarily by reference counting — when an object’s reference count drops to zero, it’s freed immediately, no collector needed. But reference counting can’t handle reference cycles: two objects referencing each other never hit zero, so they’d leak.

The gc module’s collector handles those cycles. Normally it runs automatically on a schedule. To force a run, call gc.collect(). It performs a cycle detection pass, frees unreachable cyclic objects, and returns the number of objects it collected.

When would you call it manually? On resource-constrained systems where you want the collection to happen at a controlled time, or in long-running processes where the automatic schedule is too slow. In normal application code it’s rarely necessary.

The interview answer: gc.collect() forces the cyclic garbage collector to run now. (Note: sys.gc doesn’t exist, and del just removes a reference — it doesn’t collect.)

5. What exception is raised when trying to mutate a bytes object?

Answer: TypeError.

bytes is an immutable sequence — like str, its contents cannot be changed after creation. Attempting an item assignment, b[0] = 65, is a mutation, and Python rejects it with a TypeError.

The mutable counterpart is bytearray, which supports in-place modification: ba[0] = 65 works. So the decision between the two mirrors the str vs. list distinction: immutable for safety and hashing, mutable when you need to build up bytes incrementally.

The interview answer: TypeError, because bytes objects are immutable; use bytearray when you need mutable bytes.

Answer:

TypeError.

bytes is an immutable sequence — like str, its contents cannot be changed after creation. Attempting an item assignment, b[0] = 65, is a mutation, and Python rejects it with a TypeError.

The mutable counterpart is bytearray, which supports in-place modification: ba[0] = 65 works. So the decision between the two mirrors the str vs. list distinction: immutable for safety and hashing, mutable when you need to build up bytes incrementally.

The interview answer: TypeError, because bytes objects are immutable; use bytearray when you need mutable bytes.

6. What is the outcome of set([1, 2]) + set([3, 4])?

Answer: TypeError.

Sets do not support the + operator. Addition isn’t defined for them because the natural “add sets together” operations already have dedicated operators:

  • Unionset1 | set2 or set1.union(set2) → all elements from both.
  • Intersectionset1 & set2.
  • Differenceset1 - set2.
  • Symmetric differenceset1 ^ set2.

So set([1, 2]) + set([3, 4]) raises TypeError: unsupported operand type(s) for +: 'set' and 'set'. The interview answer: TypeError — use | or .union() for combining sets.

Answer:

TypeError.

Sets do not support the + operator. Addition isn’t defined for them because the natural “add sets together” operations already have dedicated operators:

  • Unionset1 | set2 or set1.union(set2) → all elements from both.
  • Intersectionset1 & set2.
  • Differenceset1 - set2.
  • Symmetric differenceset1 ^ set2.

So set([1, 2]) + set([3, 4]) raises TypeError: unsupported operand type(s) for +: 'set' and 'set'. The interview answer: TypeError — use | or .union() for combining sets.

7. What is the purpose of init.py in Python package directories?

Answer: It marks the directory as a Python package, and its code runs automatically when the package is imported.

Before Python 3.3, __init__.py was required for a directory to be importable as a package — without it, import mypackage simply failed. With implicit namespace packages (3.3+), it’s optional for basic importing, but it’s still standard practice.

Two jobs it does:

  1. Marks the package — signals to the import system that this directory holds a package.
  2. Initialization — its contents execute once when the package is first imported. You can put package-level imports, __all__ declarations, or setup code there, making the package’s public API explicit.

The interview answer: __init__.py designates a directory as a package and runs its initialization code on import.

Answer:

It marks the directory as a Python package, and its code runs automatically when the package is imported.

Before Python 3.3, __init__.py was required for a directory to be importable as a package — without it, import mypackage simply failed. With implicit namespace packages (3.3+), it’s optional for basic importing, but it’s still standard practice.

Two jobs it does:

  1. Marks the package — signals to the import system that this directory holds a package.
  2. Initialization — its contents execute once when the package is first imported. You can put package-level imports, __all__ declarations, or setup code there, making the package’s public API explicit.

The interview answer: __init__.py designates a directory as a package and runs its initialization code on import.

8. Which statement regarding try…except…else is correct?

Answer: The else block runs only when no exception was raised in the try block.

The full try statement has up to four parts:

  • try — the guarded code.
  • except — runs if a matching exception was raised.
  • else — runs only if the try block completed without any exception.
  • finally — always runs, exception or not.

The subtlety: else is distinct from finally. finally runs unconditionally; else runs only on the clean path. The typical use of else is to hold code that must run only when nothing went wrong — for example, committing a transaction only after the risky work succeeded.

The interview answer: else runs exclusively when no exception occurred in try.

Answer:

The else block runs only when no exception was raised in the try block.

The full try statement has up to four parts:

  • try — the guarded code.
  • except — runs if a matching exception was raised.
  • else — runs only if the try block completed without any exception.
  • finally — always runs, exception or not.

The subtlety: else is distinct from finally. finally runs unconditionally; else runs only on the clean path. The typical use of else is to hold code that must run only when nothing went wrong — for example, committing a transaction only after the risky work succeeded.

The interview answer: else runs exclusively when no exception occurred in try.

My Private Notes

Notes are auto-saved locally to this device.