Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

Top 50 - Part 1
PYTHON

Top 50 - Part 1

Practice the first 15 questions from a comprehensive set of 50 important Python programming interview questions.

1. What is the Global Interpreter Lock (GIL) in CPython?

Answer: A mutex that prevents multiple native threads from executing Python bytecode at the same time — only one thread runs Python code in a given interpreter at any moment.

The GIL exists because CPython’s memory management (its reference counting, primarily) is not thread-safe on its own. A single mutex around bytecode execution guarantees that internal data structures are never corrupted by two threads touching them simultaneously. It trades away parallelism for safety and simplicity.

The consequences are practical:

  • CPU-bound threads don’t speed up. Two threads doing pure computation won’t run in parallel — they take turns, and you may even see a slowdown from context switching.
  • I/O-bound threads benefit. While a thread waits on a socket or file, it releases the GIL, and another thread runs. So threaded I/O concurrency works well.
  • The workaround is processes. multiprocessing spawns separate interpreters, each with its own GIL, giving true parallelism for CPU work.

Two things worth clarifying in an interview: the GIL is a property of CPython, not of Python the language — other implementations (PyPy, Jython) don’t have it. And the GIL is not a garbage-collection lock; it’s about interpreter state in general.

The interview answer: the GIL is a CPython mutex that lets only one thread execute bytecode at a time, making CPU-bound threads non-parallel while I/O-bound concurrency still works.

2. 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.

3. How does Method Resolution Order (MRO) handle class resolution in Python 3?

Answer: Python uses the C3 Linearization algorithm to compute the MRO.

When you access a method on an instance, Python must decide which class in the inheritance hierarchy provides it. The MRO is the ordered list of classes searched, and C3 is the algorithm that builds it.

C3 produces a linearization with three properties:

  • Subclasses come before their parents.
  • The order respects each base class’s own MRO.
  • A class never appears more than once.

The result avoids the problems of naive depth-first search in diamond inheritance — where class D(C, B) and both C and B inherit from A. A naive DFS could reach A twice or visit a subclass through the wrong branch. C3 guarantees a consistent, sensible order: roughly, D, then C’s line, then B’s line, with A last.

You can always inspect the order directly with ClassName.__mro__ or ClassName.mro(). If a hierarchy can’t be linearized consistently, Python raises a TypeError at class definition time.

The interview answer: Python 3 resolves methods with the C3 linearization algorithm, which builds a consistent single ordering that respects parent orders and handles diamond inheritance correctly.

4. What will [i for i in range(5) if i % 2 == 0 else 0] produce?

Answer: A SyntaxError.

The comprehension as written puts an else at the end, after the for clause — and that’s invalid. A trailing if in a comprehension is a filter: ... if condition keeps only the elements passing the condition. Filters don’t take else.

There are two distinct constructs:

  • A filter at the end: [i for i in range(5) if i % 2 == 0] — yields [0, 2, 4].
  • A ternary expression before the for: [i if i % 2 == 0 else 0 for i in range(5)] — evaluates the ternary for every element, yielding [0, 0, 2, 0, 4].

The question’s code mixes the two, putting an else where only a filter can go. The parser rejects it. The lesson: the if...else ternary lives on the left of for, the if filter lives on the right — and you can’t attach an else to the filter.

5. Which of the following data structures is thread-safe for FIFO queuing operations?

Answer: queue.Queue.

The FIFO semantics are easy — a plain list can do first-in-first-out with append and pop(0). The hard part is thread safety, and that’s what queue.Queue is built for.

queue.Queue is a FIFO queue designed for producer-consumer patterns across threads. It wraps its internal storage with a lock and condition variables, so multiple producers and consumers can push and pull concurrently without races. It also adds blocking APIs — get() waits until an item is available, put() can block when the queue is full, with optional timeouts.

The other candidates fail the thread-safety test:

  • collections.deque — a fast double-ended queue, but not thread-safe for concurrent mutation.
  • list and dict — not safe for concurrent modification either.

The interview answer: queue.Queue is the thread-safe FIFO for multi-threaded work. For single-threaded high-throughput FIFO, deque is the faster choice — but concurrent access calls for queue.Queue.

6. What is the output of the following code?

x = 256
y = 256
z = 257
w = 257
print(x is y, z is w)

Output: True False

This is about integer interning in CPython.

CPython pre-allocates a pool of small integer objects for the range -5 to 256. When you assign 256 to a variable, it doesn’t create a new object — it hands you the cached one. So x = 256 and y = 256 both point to the same pre-existing object, and x is y is True.

257 is outside that cached range. Each assignment z = 257 and w = 257 creates a fresh object (in CPython’s interactive/simple cases), so z and w are different objects and z is w is False.

The output is True False.

The deeper lesson: is compares identity, and interning is an implementation detail. Relying on it is fragile — the exact behavior can differ between interactive sessions and compiled code, and across Python versions. The reliable rule remains: use == for value comparison, is only for singletons like None.

7. What happens when calling super().init() in a child class?

Answer: It invokes the parent (or next-in-line) class initializer, following the class’s MRO.

super() doesn’t literally mean “the parent class” in the naive sense. It returns a proxy that resolves to the next class in the instance’s Method Resolution Order — which is usually the direct parent, but in cooperative multiple inheritance can be a sibling class.

Two things happen when you call it:

  • The parent’s __init__ runs, initializing the inherited state so the child can build on top of it.
  • Which __init__ actually runs is decided by the MRO, not by reading the source. This matters in diamond hierarchies where super() chains through multiple classes.

The rules are strict: super().__init__() should be called with the arguments the parent expects, and it’s typically the first thing a child’s __init__ does. Calling it makes the child’s initialization complete instead of leaving inherited attributes unset.

The interview answer: super() returns an MRO-resolved proxy; calling __init__ through it invokes the next class’s initializer in the resolution order, not blindly “the parent.”

8. What is the purpose of functools.wraps when creating decorators?

Answer: It preserves the original function’s metadata — __name__, __doc__, __module__, annotations — on the wrapper.

Without wraps, a decorator replaces the function with its wrapper, and the wrapper has its own (usually generic) name and docstring. Tooling breaks: help() shows the wrapper, tracebacks name the wrapper, and introspection like inspect.signature sees the wrong function.

import functools

def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@functools.wraps(func) copies the metadata from func onto wrapper (it internally updates the wrapper’s __dict__ with the original’s). The decorated function then looks like the original — correct name, docstring, and signature — while still running the wrapper’s logic.

The interview answer: wraps copies the original function’s metadata onto the wrapper so the decorated function keeps its identity for debugging and introspection.

9. What will bool(datetime.time(0, 0, 0)) evaluate to in Python 3.5+?

Answer: True.

The trap is historical. Before Python 3.5, a datetime.time object was falsy when its time was midnighttime(0, 0, 0) evaluated to False. This matched the (bad) convention that “zero time” means “no time.”

Python 3.5 fixed it. Since then, every time object — including midnight — is truthy. The rationale: the only clearly-falsy values should be things representing “nothing,” and midnight is a perfectly valid moment in time, not an absence of time.

So bool(datetime.time(0, 0, 0)) is True in Python 3.5+.

The interview point is that this one changed between versions — a reminder that truthiness rules are just behavior Python defines, and they can (and occasionally do) evolve.

10. What is the result of print(format(10, ‘b’))?

Output: 1010

The format() built-in converts a value using a format specification. The 'b' specifier formats an integer as a binary string.

10 in decimal is 1010 in binary (8 + 2). So format(10, 'b') produces the string '1010', and print outputs 1010.

The related specifiers follow the same pattern: 'd' is decimal, 'x' is lowercase hexadecimal, 'o' is octal. So format(10, 'x') would give 'a', and format(10, 'o') gives '12'.

The interview answer: 'b' formats as binary, so 10 becomes '1010'.

11. What will be printed by this code?

def fn(x=[]):
    x.append(1)
    return x

print(fn())
print(fn([2]))
print(fn())

Output: [1], [2, 1], [1, 1]

This is the mutable-default-argument bug in its most complete form — the default list persists across calls, but an explicit argument replaces it for that one call.

  • Call 1, fn(): uses the default list (currently []), appends 1 → returns [1]. The default list now holds [1].
  • Call 2, fn([2]): an explicit list [2] is passed, so the default is untouched. Appends 1 → returns [2, 1]. The default list still holds [1].
  • Call 3, fn(): back to the default, which persists from call 1 as [1]. Appends 1 → returns [1, 1].

Output: [1], [2, 1], [1, 1].

The lesson is the standard one: defaults are created once, so a mutable default accumulates state across calls. The fix — default to None, build a fresh list inside — guarantees each no-argument call starts clean.

12. What does dict.fromkeys([‘a’, ‘b’], []) create?

Answer: A dict whose keys 'a' and 'b' share the exact same list instance.

dict.fromkeys(keys, value) creates a dict with the given keys, all mapped to the same value object. It does not copy the value per key.

So d = dict.fromkeys(['a', 'b'], []) gives {'a': [], 'b': []} — but both d['a'] and d['b'] point to one list. Mutating one:

d['a'].append(1)   # now d['b'] is also [1]

This is the same aliasing trap as the mutable default argument, in dict form. If you need independent lists, build the dict manually with a comprehension: {k: [] for k in ['a', 'b']}.

The interview answer: all keys share one identical list instance; fromkeys assigns the same object, it doesn’t deep-copy per key.

13. 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.)

14. What is the output of print(1 < 2 < 3) and print(1 < 2 > 3)?

Output: True and False

Python supports chained comparisons — a single expression can string together several comparisons, and they’re evaluated as a conjunction.

  • 1 < 2 < 3 is (1 < 2) and (2 < 3). Both are true → True.
  • 1 < 2 > 3 is (1 < 2) and (2 > 3). First true, second false → False.

Two details make chaining correct and efficient:

  • The middle value is evaluated only once (no duplicated side effects).
  • Evaluation short-circuits: if the first comparison fails, the rest aren’t evaluated.

Chaining is more than a curiosity — it’s the natural way to write range checks like 0 <= score <= 100, and it reads clearly. The interview answer: chained comparisons desugar into and-joined comparisons, giving True and False.

15. Which module should be used for high-precision decimal arithmetic?

Answer: decimal.

Floating-point (float) represents numbers in binary and can’t store many decimal values exactly — the classic 0.1 + 0.2 == 0.30000000000000004 problem. For money, tax, and any computation where exact decimal results matter, that’s unacceptable.

The decimal module provides exact decimal arithmetic with configurable precision. Decimal("0.1") + Decimal("0.2") equals Decimal("0.3") exactly. You can set how many significant digits to carry (getcontext().prec = 50), control rounding modes, and get deterministic, human-friendly behavior.

The contrast:

  • math — mathematical functions (sqrt, log, trig), built on floats. Right for scientific computation, wrong for exact decimals.
  • float — native binary floating point. Fast, but lossy.
  • decimal — exact decimal arithmetic. The choice for financial computing.

The interview answer: decimal — exact, configurable-precision decimal arithmetic for money and other precision-critical work.

My Private Notes

Notes are auto-saved locally to this device.