experienced

Experienced interview questions

The Global Interpreter Lock is a mutex inside the standard CPython interpreter that allows only one thread to execute Python bytecode at any given moment, even on a machine with multiple cores. This means pure Python threads do not give a genuine speedup for CPU-heavy work, since they still take turns rather than truly running in parallel, though threads remain effective for I O bound work where a thread spends most of its time waiting rather than computing.

Python

Multiprocessing is the better choice for CPU-bound work, since each process gets its own Python interpreter and memory space, sidestepping the GIL entirely and allowing genuine parallel execution across multiple cores. Threading remains a better fit for I O bound work, such as waiting on network calls or file access, where the overhead of starting a whole new process for each task would outweigh any benefit, and sharing memory between threads is simpler than coordinating separate processes.

Python

A decorator is a function that takes another function as input and returns a new function that usually wraps the original with extra behaviour, such as logging, timing or checking permissions before the original code runs. The at-sign syntax placed above a function definition is simply shorthand for passing that function into the decorator and reassigning the result back to the same name, which is why decorators can transparently add behaviour without changing the decorated function's own code.

Python

A generator is a special kind of function that produces a sequence of values lazily, one at a time, instead of computing and returning them all at once. Each time yield is reached, the function's state is paused and the yielded value is handed back to the caller, resuming exactly where it left off the next time a value is requested. This makes generators extremely memory efficient for large or even infinite sequences, since only one value needs to exist in memory at a time.

Python

A function returning a list computes every value up front and holds the entire result in memory before handing it back, while a generator produces each value only when asked, keeping memory use low regardless of how many values there ultimately are. The trade-off is that a generator can only be iterated through once from start to finish, whereas a list can be looped over repeatedly or accessed by index at any time.

Python

A context manager defines what should happen when entering and leaving a with block, using __enter__ to run setup code and __exit__ to run cleanup code, guaranteeing the cleanup runs even if an exception occurs inside the block. This pattern is how files close themselves automatically and how locks release themselves reliably, and a custom class can support the same behaviour simply by implementing those two methods itself.

Python

A metaclass is the class of a class, controlling how classes themselves are constructed, in the same way an ordinary class controls how its objects are constructed. Most Python code never needs to touch metaclasses directly, since the default one handles everything transparently, but frameworks sometimes use a custom metaclass to automatically register subclasses, validate class definitions, or inject behaviour into every class built from it.

Python

A descriptor is an object that customises what happens when an attribute is accessed, assigned or deleted on another class, by implementing special methods such as get, set or delete. The built-in property decorator is actually implemented using descriptors behind the scenes, and writing a custom descriptor is useful when the same attribute-access behaviour, such as validation or lazy computation, needs to be reused across several different classes.

Python

A closure is a function that remembers and can still access variables from the scope it was originally defined in, even after that outer scope has already finished running. This happens naturally whenever a function is defined inside another function and returned or passed elsewhere, and it is the underlying mechanism that makes many decorator patterns and callback-based code work correctly without needing to pass extra state around explicitly.

Python

Method resolution order, often shortened to MRO, is the specific sequence Python follows when searching through a class and all of its parent classes to find which version of a method or attribute to actually use. With multiple inheritance, more than one parent might define the same method, so Python uses a consistent algorithm, called C3 linearization, to decide a predictable order rather than leaving it ambiguous, and this order can be inspected directly on any class.

Python

asyncio achieves concurrency within a single thread by having functions voluntarily pause at await points, letting the event loop run other pending work while one task is waiting on something like a network response. Threads instead achieve concurrency by letting the operating system switch between multiple threads of execution. asyncio avoids the overhead and complexity of thread synchronization, but it only helps for I O bound work and requires the whole call chain to be written in a compatible, cooperative style.

Python

Concurrency means multiple tasks are making progress over the same period of time, potentially by interleaving on a single core, while parallelism means multiple tasks are genuinely executing at the exact same instant on separate cores. Python threads and asyncio both provide concurrency without true parallelism for CPU-bound work because of the GIL, while multiprocessing achieves real parallelism by using entirely separate processes and interpreters.

Python

Default argument values are evaluated only once, at the moment the function is defined, not each time the function is called, so a mutable default such as an empty list is actually the same shared object reused across every call that does not explicitly supply its own. Any modification made to it during one call quietly persists and shows up unexpectedly the next time the function is called. The standard fix is to default the argument to None and create a fresh mutable object inside the function body instead.

Python

A circular import happens when two modules end up importing each other, either directly or indirectly through a longer chain, so that loading one requires the other to already be fully loaded, which it is not yet. Common fixes include moving the import statement inside the function that actually needs it rather than at the top of the file, restructuring the code so shared logic lives in a third module both can depend on, or combining the two modules if they are tightly coupled enough to belong together anyway.

Python

A memory leak in Python usually comes from objects that remain unintentionally reachable, such as a growing global list or cache that nothing ever clears, references held in closures or callbacks that outlive their intended purpose, or circular references between objects that override the del method in a way that prevents the cycle-detecting collector from cleaning them up. The garbage collector reclaims memory for genuinely unreachable objects, but it cannot help with memory that a program is still technically holding onto by mistake.

Python

Python primarily relies on reference counting, immediately freeing an object the moment its reference count drops to zero, meaning nothing in the program can reach it any more. This alone cannot handle groups of objects that reference each other in a cycle but are otherwise unreachable from the rest of the program, so a separate cyclic garbage collector periodically scans for and cleans up exactly these reference cycles that plain reference counting would otherwise miss entirely.

Python

A weak reference lets one object refer to another without increasing its reference count, meaning the referenced object can still be garbage collected normally even while the weak reference to it exists. This is useful for building caches or observer-style relationships where you want to keep track of an object without being the reason it stays alive in memory longer than it otherwise would.

Python

__slots__ declares a fixed set of attribute names a class is allowed to have, which stops Python from creating the usual per-instance dictionary that normally stores an object's attributes. This noticeably reduces memory usage for classes that create a very large number of instances, since each object skips the overhead of that dictionary, though it comes at the cost of no longer being able to freely add new attributes that were not declared up front.

Python

__str__ controls how an object is displayed when printed for a human-friendly reader, __repr__ controls the more precise, developer-facing representation shown in a debugger or interactive session, __eq__ defines what it means for two objects of that class to be considered equal, and __hash__ defines how the object can be used as a dictionary key or stored in a set. Implementing these consistently is what lets a custom class behave naturally alongside Python's built-in types and collections.

Python

__str__ is meant to produce a readable, informal description of an object aimed at an end user, the kind of text you would want printed to a screen. __repr__ is meant to be unambiguous and, ideally, precise enough that evaluating it would recreate an equivalent object, aimed at developers debugging in a console. If only __repr__ is defined, Python falls back to using it for both purposes, which is why it is good practice to always implement it even when a friendlier __str__ also exists.

Python

Type hints let a function or variable declaration describe what type of value is expected, improving editor autocompletion and making the intent of the code clearer to anyone reading it. Crucially, Python itself does not enforce these hints at runtime at all, since it remains a dynamically typed language underneath; instead, a separate static analysis tool such as mypy is normally run against the codebase to actually catch type mismatches before the program ever executes.

Python

The property decorator lets a method be accessed using the same simple attribute syntax as a plain field, while still allowing custom logic to run behind the scenes whenever that value is read, and optionally when it is set or deleted. This is useful for adding validation or computed values to an attribute later on without breaking any existing code that already accesses it as if it were a plain, ordinary attribute.

Python

An instance method automatically receives the specific object it was called on as its first argument and can read or change that object's own data. A class method instead receives the class itself as its first argument, useful for logic that relates to the class as a whole rather than any one instance, such as an alternative constructor. A static method receives neither automatically and behaves like a plain function that has simply been grouped inside the class for organizational purposes.

Python

An abstract base class defines a shared interface that subclasses are required to implement, using the abc module to mark certain methods as abstract so that Python actively prevents creating an instance of any subclass that has not provided a real implementation for each one. This is useful for enforcing a consistent contract across several related classes, catching a missing implementation immediately rather than letting it surface later as a confusing runtime error.

Python

A common approach overrides the __new__ method on a class to check whether an instance has already been created and, if so, return that same existing instance instead of building a new one, effectively guaranteeing only one object of that class ever exists. A simpler alternative many Python developers reach for instead is just using a module-level variable, since a module is only ever imported once, which naturally gives the same single-instance guarantee with far less code.

Python

The factory pattern centralises object creation behind a function or method that decides which specific class to instantiate based on some input, rather than having calling code directly choose and construct a particular class itself. In Python this is often just a plain function that returns different class instances depending on a parameter, since Python's dynamic nature usually makes a full factory class hierarchy, common in more rigid languages, unnecessary overhead.

Python

Monkey patching means modifying or replacing a class or module's behaviour at runtime, after it has already been defined, often to fix or extend third-party code without touching its source. It is risky because the change is invisible to anyone reading the original code, can silently break if the patched library is later updated, and can cause confusing bugs if applied inconsistently across a codebase, so it is generally reserved for narrow, well-documented situations such as testing.

Python

A virtual environment creates an isolated set of installed packages specific to one project, preventing dependency versions required by different projects on the same machine from conflicting with each other. Without one, installing a package globally can silently break another project that needed a different, incompatible version of the same library, and virtual environments make a project's exact dependencies easy to reproduce on another machine or in deployment.

Python

A requirements file is typically just a flat list of package names and versions used to recreate an environment with a tool like pip, without describing anything about the project itself. A pyproject.toml file is a broader, standardized configuration file that can describe the project's metadata, its dependencies, and how it should be built and packaged, and modern Python tooling is increasingly consolidating around it as the single source of truth for a project's configuration.

Python

The standard approach is profiling rather than guessing, using a tool such as cProfile to measure exactly how much time is spent inside each function during a real run of the program. This produces a breakdown showing which functions account for the bulk of the total time, letting effort focus on the actual bottleneck rather than optimizing code that was never a meaningful factor in the slowdown to begin with.

Python

Common strategies include replacing manual loops with built-in functions or comprehensions, which run faster since much of the work happens in optimized internal code rather than the Python interpreter itself, caching the results of expensive repeated calculations, choosing a more appropriate data structure such as a set instead of a list for membership checks, and, for genuinely CPU-heavy numerical work, offloading it to a library like NumPy that performs the heavy lifting in compiled code.

Python

The most common workaround is multiprocessing, which sidesteps the GIL entirely by running separate Python processes, each with its own interpreter and memory. Another option is delegating the CPU-heavy portion of the work to a library implemented in C, such as NumPy, which can release the GIL internally while it computes, allowing other Python threads to keep running in the meantime. A newer option in recent Python releases is an experimental mode that can disable the GIL altogether for true multithreaded parallelism.

Python

An iterable is any object capable of producing an iterator when passed to the iter function, such as a list or a string, but it does not track any position on its own. An iterator is the object actually returned by that call, which does keep track of its current position and produces the next value each time next is called on it, eventually raising a stop signal once no values remain. A for loop is essentially syntactic sugar for repeatedly calling next on an iterator obtained from an iterable.

Python

lru_cache is a decorator that automatically remembers the results of previous calls to a function based on its arguments, returning the cached result instantly instead of recomputing it if the same arguments are seen again. It is especially useful for expensive, pure functions that are called repeatedly with a limited set of inputs, such as recursive calculations, though it should be avoided on functions whose results depend on anything beyond their arguments, since a stale cached answer could then be returned incorrectly.

Python

Chaining one exception from another, by raising a new exception with a from clause pointing at the original one, preserves the original exception as context while still surfacing the new, often more specific one to the caller. This produces a traceback showing both the low-level cause and the higher-level error it triggered, which is far more useful for debugging than losing the original exception entirely when translating it into a different one.

Python

Without a custom __eq__ method defined, Python falls back to comparing objects purely by identity, meaning two separate objects are only considered equal if they are literally the exact same object in memory, regardless of how similar their attributes look. To get meaningful value-based equality for a custom class, __eq__ needs to be implemented explicitly to compare the relevant attributes directly, and __hash__ often needs defining alongside it if the objects are meant to be used in a set or as dictionary keys.

Python

A race condition occurs when two or more threads access and modify shared data at the same time in a way that makes the final outcome depend on unpredictable timing, often producing incorrect results that are hard to reproduce consistently. A lock forces only one thread at a time to enter a protected section of code, so operations on shared data happen safely one after another rather than getting interleaved unpredictably between multiple threads.

Python

A deque, from the collections module, is a fast double-ended structure but is not inherently safe for multiple threads to use without additional locking of your own. The queue module provides thread-safe queue implementations specifically designed for passing data safely between threads, already handling the necessary internal locking, which makes it the more appropriate choice whenever multiple threads need to hand work off to each other rather than a plain unprotected deque.

Python

Global mutable state shared across requests or tasks in a long-running service can lead to subtle bugs where one request's data leaks into or corrupts another's, especially once multiple threads or asynchronous tasks are involved and the assumption that only one operation runs at a time no longer holds. It also makes testing harder, since tests can accidentally influence each other through leftover shared state, which is why most frameworks encourage keeping request-specific data scoped locally rather than stored globally.

Python

asyncio helps specifically with I O bound workloads, where a program spends significant time waiting on external things such as network requests, database queries or disk access, since it can start many such operations and let the event loop switch between them while each one waits, rather than sitting idle on one at a time. It provides no benefit at all for CPU-bound work, since a single core is still doing the actual computation and there is nothing to overlap while it is busy calculating.

Python