This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.
Identity, Image, and Privacy Notice
To respect individual privacy, some names, profile photographs, avatars, biographical details, and other identifying information displayed in this guide may be replaced with pseudonyms, licensed stock images, illustrative avatars, composite images, or representative descriptions. Unless a person is expressly identified as an actual contributor, a displayed name, image, or profile should not be understood as depicting or identifying a specific candidate, interviewer, employee, or other real individual. These representations are provided for editorial and illustrative purposes only and do not imply endorsement, employment, participation, or affiliation with this guide or any company mentioned in it. Any resemblance to an actual person is coincidental.
Company Notice
This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.
Content Accuracy and Verification
To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
71. What is the GIL, and how does it affect multithreaded Python programs?Language SpecificHard
i Question Details
Explain the Global Interpreter Lock in CPython, how it affects CPU-bound and I/O-bound threads, when threads can still help, and when multiprocessing or asyncio is a better choice.
Short Interview Answer (30-60 seconds)
The GIL is a lock in the normal CPython runtime. It allows only one thread at a time to execute Python bytecode in one process. Because of this, adding threads usually does not speed up CPU heavy Python code. Threads can still help with network, file, and database work because the runtime releases the GIL while a thread waits for many blocking operations. I use multiprocessing for heavy CPU work and asyncio for many cooperative input and output tasks. Python also has an optional free threaded build, but library support must be checked.
The practical point is that Python threads are useful for waiting work, but they usually do not make CPU heavy Python code run in parallel on the normal CPython build.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
The Global Interpreter Lock, usually called the GIL, is a runtime lock in CPython. A thread must hold this lock before it can execute Python bytecode or safely work with Python objects. This design protects important interpreter state, including memory management details such as reference counts.
A process can contain many threads. The operating system may schedule those threads at the same time. However, in the normal CPython build, only one thread in that process can execute Python bytecode at a given moment. CPython switches the GIL between threads, so the program can still make progress concurrently. This is concurrency, but it is not full parallel execution of Python bytecode.
For CPU heavy work, threads often give little speed improvement. Examples include large pure Python loops, image calculations written in Python, or number processing that keeps the interpreter busy. The threads compete for the same GIL. Thread switching also adds overhead. For this work, separate processes are often better because each process has its own Python interpreter and its own GIL. ProcessPoolExecutor and multiprocessing are common choices.
Threads are still useful for input and output work. CPython releases the GIL around many blocking operations. Examples include waiting for a network response, reading a file, or waiting for a database call. While one thread waits, another thread can run. This makes thread pools useful for existing blocking libraries and for applications that need moderate input and output concurrency.
Asyncio is often better when one application must manage many network connections. It uses an event loop and cooperative tasks. A task gives control back when it reaches await. This can support many connections with fewer operating system threads. The downside is that the libraries must support async operations, and blocking code must not run directly on the event loop.
Some native libraries release the GIL while performing heavy work outside Python. For example, selected operations in scientific or compression libraries may execute native code without holding the GIL. In that case, threads may use more than one CPU core. This depends on the exact library and operation, so it should be measured rather than assumed.
Python now also supports an optional free threaded CPython build. It can run Python threads in parallel without the GIL. It is supported but is not the normal default build. Some extension modules may not support it and can cause the GIL to become enabled again. Shared mutable data still needs locks or another synchronization method. Removing the GIL does not remove race conditions.
My production choice is based on the workload. I use threads for blocking input and output with synchronous libraries. I use asyncio for many cooperative input and output tasks. I use processes for CPU heavy Python work. I consider the free threaded build only after checking package support, thread safety, and measured performance.
Key Insight / Why This Solution Works
First, identify whether the workload spends most of its time computing or waiting. Second, check whether the code runs mainly as Python bytecode or inside a native library that releases the GIL. Third, choose threads for blocking input and output when synchronous libraries are already used. Choose asyncio when many tasks can cooperate through await. Choose multiprocessing or ProcessPoolExecutor for heavy Python computation. Fourth, protect shared mutable data because the GIL does not make a complete operation automatically safe. Finally, measure the real workload. Confirm throughput, latency, CPU use, memory use, and process overhead before deciding that one concurrency model is better.
Example
This example runs the same CPU task with a process pool and several waiting tasks with a thread pool. The CPU function performs pure Python arithmetic, so separate processes allow work to use different interpreter processes. The input and output example uses sleep to represent waiting for a network, file, or database operation. Threads help there because one thread can run while another waits. The example does not claim that sleep is real production input and output. It only demonstrates the scheduling difference. The main function protects process creation and allows the example to run correctly on platforms that start new processes by importing the module.
Code
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
defcpu_task(limit: int) -> int:
total = 0for value inrange(limit):
total += value * value
return total
defio_task(name: str, delay: float) -> str:
time.sleep(delay)
returnf"{name} completed"defmain() -> None:
with ProcessPoolExecutor(max_workers=4) as process_pool:
cpu_results = list(process_pool.map(cpu_task, [2_000_000] * 4))
with ThreadPoolExecutor(max_workers=4) as thread_pool:
futures = [thread_pool.submit(io_task, f"request {index}", 1.0) for index inrange(4)]
io_results = [future.result() for future in futures]
print(cpu_results)
print(io_results)
if __name__ == "__main__":
main()
Where it is used
Threads are useful in web crawlers, file transfer tools, database clients, and services that call several blocking APIs. Asyncio is common in network servers, websocket services, chat systems, and clients that manage many connections. Processes are useful for data transformation, report generation, image processing, and other CPU heavy Python work. Thread pools also help when an application uses a synchronous library inside a larger service. A free threaded build may help programs designed for safe shared memory parallelism, but teams should first confirm that their libraries and extension modules support it.
Why Interviewers Ask This
Interviewers ask this question to check whether the candidate understands CPython runtime behavior and can choose the right concurrency model. A strong answer separates concurrency from parallel execution. It also explains why threads still help with waiting work, why processes help with heavy Python computation, and when asyncio is simpler. In 2026, a strong candidate should also know that CPython offers an optional free threaded build, while the normal build still commonly uses the GIL.
Common interview mistakes
A common mistake is saying that the GIL makes Python completely single threaded. Python can run many threads, but the normal CPython build allows only one thread at a time to execute Python bytecode in one process. Another mistake is saying that the GIL makes shared data safe. Several bytecode steps can still interleave, so locks may be required. Candidates also assume that threads never help CPU work. Native code may release the GIL, so the result depends on the library. Another mistake is using asyncio for blocking code without moving that work away from the event loop. Finally, do not claim that every Python installation is free threaded. The free threaded build is optional.
Interview tip
Start by saying that the answer applies to the normal CPython build. Then separate CPU heavy work from input and output work. Explain threads, asyncio, and processes as workload choices. Mention the optional free threaded build near the end. Also state that the GIL does not replace application locks. This shows both current runtime knowledge and practical engineering judgment.
Interviewer may ask next
Does the GIL make operations on shared Python data thread safe?
No. The GIL protects CPython interpreter state, but it does not make every application operation atomic or logically safe. A statement can involve several bytecode steps. CPython may switch threads between those steps. For example, reading a value, calculating a new value, and writing it back can interleave with another thread. This can create lost updates or inconsistent state. Some individual built in operations appear atomic in the current CPython implementation, but application code should not depend on undocumented implementation details. I use Lock, RLock, Queue, immutable data, or message passing when several threads share mutable state. On a free threaded build, explicit synchronization becomes even more important because Python code can run in parallel. The exact change is not to remove threading. It is to protect the shared state and keep the protected section small. The tradeoff is extra coordination and possible lock contention.
How would your choice change when using the free threaded CPython build?
I would first verify that the running interpreter has the GIL disabled and that every important extension module supports free threading. If those checks pass, CPU heavy Python threads may run in parallel across cores. I could then compare a thread pool with a process pool for the real workload. Threads may reduce process startup, memory, and serialization costs because they share one address space. However, shared mutable state now needs careful synchronization, and some libraries may enable the GIL again when imported. The exact change is that threading becomes a possible choice for CPU parallelism, not only for waiting work. I would still keep asyncio for workloads built around many cooperative input and output operations. I would measure throughput, latency, memory, and lock contention before changing production architecture. The main tradeoff is easier data sharing against greater risk of races and package compatibility problems.
72. How does Python manage memory, including reference counting and cyclic garbage collection?Language SpecificHard
i Question Details
Explain Python's private heap, reference counting, object deallocation, reference cycles, the cyclic garbage collector, and common causes of memory that remains reachable longer than expected.
Short Interview Answer (30-60 seconds)
In CPython, Python objects live in a private heap managed by the Python memory manager. Each object normally has a reference count. When that count reaches zero, CPython can usually destroy the object immediately. Reference counting alone cannot remove a group of objects that only reference each other. The cyclic garbage collector finds those unreachable cycles and clears them. Memory can still grow when objects remain reachable through globals, caches, containers, closures, tasks, or callbacks. Also, freed object memory may stay inside Python for reuse instead of returning to the operating system.
Detailed Explanation
The practical point is that CPython usually frees an object when nothing refers to it, but cycles need a second cleanup system. Memory can also remain allocated when objects are still reachable or when Python keeps freed blocks for reuse.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
Python stores its objects and internal data structures in a private heap. Application code does not directly manage this heap. The Python memory manager requests larger areas of memory from the operating system and then uses internal allocators for Python objects. Different object types may use different allocation strategies because a small integer, a list, and a dictionary have different storage needs.
In normal CPython, reference counting is the first cleanup mechanism. Every object keeps track of how many active references point to it. Assigning an object to another variable usually increases that count. Removing a reference usually decreases it. When the count reaches zero, CPython can run the object cleanup process and release its owned resources immediately.
For example, a local list may become unreachable when a function returns. If no other object refers to that list, its reference count reaches zero. CPython can destroy the list and decrease the counts of the objects stored inside it.
Reference counting cannot solve every case. Two objects can reference each other. Their reference counts stay above zero even after the application loses all outside references. This is a reference cycle. A parent object may refer to a child, while the child refers back to the parent.
CPython therefore includes a cyclic garbage collector. It supplements reference counting. The collector tracks container objects that can participate in cycles, such as lists, dictionaries, class instances, and other objects that hold references. It periodically examines groups of tracked objects. If a group cannot be reached from live application roots, the group is garbage even when its internal reference counts are not zero. The collector can then clear the cycle and allow the objects to be destroyed.
The collector uses generations because most objects die young. New tracked objects are checked more often. Objects that survive collections move to older generations and are checked less often. This reduces the cost of scanning every tracked object during every collection.
An important production point is that memory growth does not always mean the collector is broken. An object cannot be collected while it is still reachable. Common causes include unbounded dictionaries, lists, caches, global variables, closures, registered callbacks, background tasks, retained exceptions, tracebacks, and application sessions that are never removed.
Memory shown by the operating system may also stay high after objects are destroyed. Python allocators often keep freed blocks and arenas so later allocations can reuse them quickly. Some object types also use free lists. Therefore, object memory becoming reusable inside Python does not always mean the process immediately returns that memory to the operating system.
For debugging, I first confirm that memory growth is real and repeatable. I use tracemalloc to compare allocation snapshots. I inspect cache sizes, container lengths, task registries, and object ownership. The gc module can show collection statistics and tracked objects, but forcing gc.collect is not a general fix. The correct fix is usually to remove the unwanted reference, bound the cache, close the resource, or correct the object lifecycle.
Technical Approach
First, identify where Python obtains and stores object memory. Python requests memory from the operating system and manages Python objects inside its private heap. Second, follow the reference count. Creating or storing another reference increases the count. Removing a reference decreases it. A count of zero normally allows immediate cleanup in CPython. Third, check for reference cycles. If objects only refer to each other, their counts may never reach zero. Fourth, let the cyclic garbage collector find tracked container groups that are no longer reachable. Fifth, separate unreachable garbage from reachable memory growth. Inspect globals, caches, containers, callbacks, tasks, closures, and tracebacks. Finally, remember that freed memory may remain in Python allocators for reuse, so process memory and live object memory are not always the same.
Practical Complexity & Trade-offs
Traditional algorithm complexity does not fully describe Python memory management. Reference count updates add a small cost when references are created or removed. Destroying a container can take time related to how many references it owns. Cyclic collection costs depend on how many tracked objects the collector examines. Large object graphs can therefore cause longer collection work. Generations reduce this cost by checking young objects more often and older objects less often. Keeping many reachable objects increases memory use and can make scans more expensive. Python allocators improve speed by reusing freed memory, but the process may keep a larger memory footprint. The practical goal is to control object lifetime and measure real allocation growth.
Where it is used
This knowledge is useful in long running web services, task workers, data pipelines, notebooks, machine learning jobs, and applications that process large files. It helps when designing bounded caches, closing database and file resources, removing completed tasks, and cleaning callback registrations. It is also useful when diagnosing a worker whose memory grows after every job. Developers use tracemalloc, allocation snapshots, object counts, cache metrics, and garbage collector statistics to locate the retaining reference. Understanding reachability also helps when using closures, class relationships, event systems, and dependency containers.
Why Interviewers Ask This
Interviewers ask this question to check whether the candidate understands what happens after Python creates an object. A strong answer separates the private heap, object allocation, reference counting, and cyclic garbage collection. It also shows practical judgment about memory growth. The candidate should know that an object can remain in memory because it is still reachable, even when the program no longer needs it. This matters when debugging long running services, workers, data pipelines, and applications with large caches.
Common interview mistakes
A common mistake is saying that Python uses only garbage collection. Normal CPython mainly uses reference counting and adds cyclic collection for unreachable cycles. Another mistake is saying that del deletes an object. The del statement removes one reference. The object remains alive if another reference still exists. Candidates also assume that every increase in process memory is a leak. Python may keep freed memory for reuse. Another mistake is calling gc.collect repeatedly instead of finding the retaining reference. Disabling the cyclic collector without proving that cycles cannot occur is also risky. Finally, a weak reference does not keep its target alive, but using weak references everywhere is not a substitute for correct ownership.
Interview tip
Explain the answer in four parts. Start with the private heap. Then explain reference counting and immediate cleanup. Next, show why a cycle defeats reference counting and how the cyclic collector handles it. Finish with the production distinction between unreachable garbage, reachable objects, and memory retained by Python allocators. Mention tracemalloc as a practical debugging tool. Do not claim that del directly frees an object or that every high memory value is a leak.
Interviewer may ask next
Why can memory keep growing even when the cyclic garbage collector is running?
The most common reason is that the objects are still reachable. The collector removes unreachable cycles. It cannot remove an object that a live global, cache, list, dictionary, closure, callback, task registry, traceback, or session still references. This is often called logical retention rather than unreachable garbage. I would inspect which objects grow and then find who refers to them. Tracemalloc can compare allocation snapshots and identify the code paths creating memory. Application metrics can also show cache size, active sessions, queued tasks, and container length. Another reason is allocator behavior. After objects are destroyed, Python may keep freed blocks and arenas for later reuse. The operating system can therefore show a large process even when Python has fewer live objects. The exact fix depends on the cause. I would remove the unwanted reference, bound the collection, expire cache entries, close completed tasks, or redesign the object lifecycle. Repeatedly forcing collection does not fix reachable retention.
What exactly does del do, and when is an object actually destroyed?
The del statement removes a binding or container reference. It does not directly destroy the object. If other references still point to the same object, that object remains alive. In normal CPython, removing the final strong reference usually reduces the reference count to zero. CPython can then run finalization and release the object immediately. If the object belongs to a reference cycle, its count may stay above zero even when no application root can reach it. The cyclic collector must identify and clear that unreachable cycle. Weak references behave differently because they do not keep the target alive. When the final strong reference disappears, a weak reference no longer returns the object. The important interview point is to separate a variable name from the object itself. Names and containers hold references. The object lifetime depends on all strong references, not on one particular variable.
73. What are descriptors in Python, and how does property work internally?Language SpecificHard
i Question Details
Explain the descriptor protocol, __get__, __set__, and __delete__, the difference between data and non-data descriptors, and how Python uses descriptors to implement property and bound methods.
Short Interview Answer (30-60 seconds)
Descriptors are objects stored on a class that can control how an attribute is read, assigned, or deleted. They use __get__, __set__, and __delete__. A descriptor that defines __set__ or __delete__ is a data descriptor. A descriptor that defines only __get__ is a non data descriptor. Property is a data descriptor because the property type provides __get__, __set__, and __delete__. Its methods call the getter, setter, or deleter supplied by the class, and raise AttributeError when the required function is missing. Python functions also act as non data descriptors, which is how an instance method becomes bound to an instance.
The practical purpose of a descriptor is to control attribute access while callers continue to use normal dot syntax. A descriptor is an object stored on a class. Python may call its __get__ method when an attribute is read, __set__ when it is assigned, and __delete__ when it is deleted.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
A data descriptor defines __set__ or __delete__. It takes priority over a value with the same name in the instance dictionary. A non data descriptor defines __get__ without defining __set__ or __delete__. An instance value can normally override it.
Property uses this protocol. A property object is stored on the class. Its __get__ calls the getter. Its __set__ calls the setter. Its __delete__ calls the deleter. The property type still has these descriptor methods when a setter or deleter was not supplied, so the missing operation raises AttributeError.
Functions stored on a class are non data descriptors. Their __get__ creates bound methods that automatically pass the instance as the first argument.
Descriptor access is normally constant time for dictionary backed storage, but it adds lookup and function call work. The descriptor object is shared by the class, while each instance usually stores its own managed value.
Example
The example uses one custom data descriptor and one property. PositiveNumber is stored on the Product class. __set_name__ records a private storage name when the class is created. __get__ returns the descriptor during class access and returns the saved instance value during instance access. __set__ validates each new value before storing it in the instance dictionary. The label property validates one specific attribute. Reading label calls its getter, while assigning label calls its setter. The describe function also demonstrates descriptor behavior because Python turns it into a bound method when it is accessed through a Product instance.
Code
classPositiveNumber:
"""Manage a positive numeric value for each instance."""def__set_name__(self, owner, name):
# Save the name used for this descriptor on the owner class.# The actual value will be stored separately in each instance.self.storage_name = "_" + name
def__get__(self, instance, owner=None):
# Access through the class returns the descriptor itself.if instance isNone:
returnself# Access through an instance returns that instance's stored value.return instance.__dict__[self.storage_name]
def__set__(self, instance, value):
# Reject booleans because bool is a subclass of int in Python.ifisinstance(value, bool) ornotisinstance(value, (int, float)):
raise TypeError("The value must be a number")
# Accept only values greater than zero.if value <= 0:
raise ValueError("The value must be greater than zero")
# Store the value in the instance, not in the shared descriptor.
instance.__dict__[self.storage_name] = value
classProduct:
# This object defines __set__, so it is a data descriptor.
price = PositiveNumber()
def__init__(self, name, price):
# This assignment calls the property setter.self.label = name
# This assignment calls PositiveNumber.__set__.self.price = price
@propertydeflabel(self):
# property.__get__ calls this getter.returnself._label
@label.setterdeflabel(self, value):
# property.__set__ calls this setter.ifnotisinstance(value, str) ornot value.strip():
raise ValueError("The label must be a non empty string")
# Use a different backing name to avoid calling the setter again.self._label = value.strip()
defdescribe(self):
# A function stored on a class is a non data descriptor.# Access through an instance creates a bound method.returnf"{self.label}: ${self.price:.2f}"
product = Product("Keyboard", 75)
print(product.label)
print(product.price)
print(product.describe())
product.label = "Mechanical Keyboard"
product.price = 90print(product.describe())
try:
product.price = -10except ValueError as error:
print(error)
Where it is used
Property is useful when one class attribute needs validation, conversion, controlled updates, or a computed value. Custom descriptors are useful when the same rule must be reused across many attributes or classes. Python also uses descriptors for bound methods, classmethod, staticmethod, slots, and many framework managed fields. In production, descriptor logic should remain small and predictable. Slow database calls, network calls, or surprising state changes should usually not happen during ordinary attribute access. A descriptor stored on the class is shared by all instances, so mutable state placed inside the descriptor may also be shared. Per instance values should normally be stored in the instance dictionary, in a slot, or in another storage structure designed for instance specific data.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands Python attribute access below the surface. It checks knowledge of the descriptor protocol, attribute lookup order, property behavior, method binding, reusable validation, and the practical cost of placing logic behind normal attribute access.
Common interview mistakes
A common mistake is saying that every descriptor must define all three protocol methods. A descriptor may define only __get__. Another mistake is calling every descriptor with __get__ a data descriptor. The defining rule is whether its type provides __set__ or __delete__. Candidates may also say that a read only property is a non data descriptor. It is still a data descriptor because the property type provides __set__ and __delete__, even when those operations raise AttributeError. Another mistake is reading or assigning the public property name inside its own getter or setter, which causes endless recursion. Developers may also forget to return the descriptor itself when __get__ receives instance as None. Finally, storing instance values directly on a shared descriptor can accidentally share state across every instance.
Interview tip
Start with the practical result: descriptors control attribute access behind normal dot syntax. Then name __get__, __set__, and __delete__. Explain the lookup difference between data and non data descriptors. Finish by connecting property to data descriptors and bound methods to function descriptors.
Interviewer may ask next
What happens if an instance dictionary contains the same name as a descriptor?
A data descriptor still takes priority over the instance dictionary. A non data descriptor does not, so an instance value with the same name can override it. This matters because a property remains in control even if the instance dictionary contains that public name, while a normal method can be shadowed by assigning an attribute with the method name on that instance.
When should you use a custom descriptor instead of property?
Use a custom descriptor when the same attribute behavior must be reused across several fields or classes. Use property when the logic belongs to one attribute on one class. A descriptor reduces repeated validation or storage code, but it adds indirection and can make attribute behavior harder to trace. It also creates one shared descriptor object, so instance specific state must be stored separately and not as mutable state on the descriptor itself.
74. How does Python's object model perform attribute lookup?Language SpecificHard
i Question Details
Explain lookup through an instance and its class hierarchy, the roles of __dict__, __getattribute__, and __getattr__, and how overriding lookup hooks can cause recursion or surprising behavior.
Short Interview Answer (30-60 seconds)
Python sends every normal attribute read through __getattribute__. The default lookup first checks the class hierarchy for a data descriptor. It then checks the instance __dict__. After that, it checks the class hierarchy for a non data descriptor or a normal class attribute, following the method resolution order. If lookup raises AttributeError, Python calls __getattr__ when it is defined. I normally use __getattr__ for missing values and override __getattribute__ only when every read must be controlled. Inside an override, I delegate to object.__getattribute__ to avoid infinite recursion.
The practical rule is to preserve Python's normal lookup and customize only the part you need. Every expression such as user.name starts with __getattribute__. The default implementation first searches the class and its base classes for a data descriptor. A property is a common example. It then checks the instance __dict__, which stores normal instance attributes when the class allows one. Next, it searches the class hierarchy again for a non data descriptor or a regular class value, following the method resolution order. Normal methods are non data descriptors and become bound methods when read through an instance. If no value is found and AttributeError leaves __getattribute__, Python calls __getattr__ as a fallback. This is useful for lazy values, compatibility names, and proxy objects. A class using __slots__ may not have an instance __dict__. Reading an attribute normally returns a reference to the stored object rather than copying it. Custom __getattribute__ code runs on every read, so it can add noticeable cost. It can also recurse forever if it reads another attribute through self instead of delegating to object.__getattribute__.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
Example
The example uses the same lookup rules described in the answer. name is stored in the instance __dict__. category is inherited from the base class. display_name is a property, so it acts as a data descriptor on the class. The __getattribute__ override records each read and then delegates to object.__getattribute__, which preserves descriptor handling, instance lookup, and class hierarchy lookup. When nickname cannot be found normally, __getattr__ creates a fallback value. Any other missing name raises AttributeError so that Python tools such as hasattr continue to behave correctly.
Code
classBaseUser:
# This value is found through the class hierarchy.
category = "member"classUser(BaseUser):
def__init__(self, name):
# This assignment stores name in the instance __dict__.self.name = name
@propertydefdisplay_name(self):
# A property is a data descriptor on the class.returnself.name.upper()
def__getattribute__(self, attribute_name):
# This hook runs for every normal attribute read.print(f"Reading attribute: {attribute_name}")
# Delegate to the base implementation.# This preserves Python's normal lookup rules and avoids recursion.returnobject.__getattribute__(self, attribute_name)
def__getattr__(self, attribute_name):
# This hook runs only after normal lookup raises AttributeError.if attribute_name == "nickname":
returnself.name[:3]
# Other missing names must still raise AttributeError.raise AttributeError(f"{type(self).__name__} has no attribute {attribute_name!r}")
user = User("Amina")
# Found in the instance __dict__.print(user.name)
# Found as a data descriptor on the class.print(user.display_name)
# Found in the base class through the method resolution order.print(user.category)
# Not found normally, so __getattr__ supplies the value.print(user.nickname)
# Read the instance dictionary without entering the custom hook again.print(object.__getattribute__(user, "__dict__"))
try:
# This name is missing and must raise AttributeError.print(user.age)
except AttributeError as error:
print(error)
Where it is used
This behavior appears in properties, normal method binding, inheritance, proxy objects, lazy loading, compatibility layers, configuration objects, and object relational mapping tools. __getattr__ is usually the safer hook when only missing names need special behavior. __getattribute__ is appropriate when every read must be observed or controlled, such as in a strict proxy or an access logging wrapper. Classes may use __slots__ to restrict allowed attributes and reduce the memory used by a separate instance dictionary. These hooks should remain small because complex lookup logic is harder to test, debug, and understand.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands what Python does when an attribute is read. It tests knowledge of instances, classes, inheritance, descriptors, the method resolution order, and lookup hooks. It also tests whether the candidate can customize lookup without causing recursion, hiding missing attributes, or adding unnecessary cost to every attribute access.
Common interview mistakes
A common mistake is saying that Python always checks the instance dictionary before the class. A data descriptor on the class has higher priority than the instance __dict__. Another mistake is treating __getattr__ and __getattribute__ as the same hook. __getattribute__ handles every normal read, while __getattr__ is a fallback for a missing name. Reading self.some_name inside an unsafe __getattribute__ override can call the same method again until RecursionError occurs. Returning a default for every name in __getattr__ can hide spelling mistakes and make hasattr report that an attribute exists when it should not. It is also incorrect to assume every instance has a __dict__, because a class using __slots__ may not provide one.
Interview tip
State the lookup order clearly. Start with data descriptors, then the instance __dict__, then non data descriptors and class attributes through the method resolution order. Explain that __getattribute__ handles every read and __getattr__ handles only a missing name. Finish by mentioning delegation to object.__getattribute__, recursion risk, and the possible absence of __dict__ when __slots__ is used.
Interviewer may ask next
What happens when an instance dictionary contains the same name as a descriptor on the class?
A data descriptor on the class wins over the matching value in the instance __dict__. A property is normally a data descriptor because it controls attribute assignment or deletion even when no setter is provided. A non data descriptor has lower priority, so a matching instance value can hide it. This distinction matters because it explains why properties keep control of attribute access while normal methods can be shadowed by instance attributes.
What are the production tradeoffs of overriding __getattribute__ instead of using __getattr__?
__getattribute__ provides control over every attribute read, but it adds Python code to every access and creates a greater risk of recursion and surprising behavior. __getattr__ runs only after normal lookup fails, so it has a smaller performance effect and preserves standard behavior for existing attributes. The main tradeoff is complete control versus simpler, safer, and more predictable lookup. Production code should prefer __getattr__ unless successful attribute reads must also be intercepted.
75. How does cooperative multiple inheritance with super() work?Language SpecificHard
i Question Details
Explain zero-argument super(), how calls follow the method resolution order rather than a single parent, signature compatibility requirements, and why every participating class must delegate consistently.
Short Interview Answer (30-60 seconds)
Cooperative multiple inheritance works when each participating class performs its own task and then calls super() with compatible arguments. Zero argument super() does not simply call one direct parent. It continues with the next class in the method resolution order. This allows every class in the chain to run once. The design fails if a class stops the chain too early or passes arguments that the next method cannot accept.
Use cooperative multiple inheritance only when every participating class follows one shared calling contract. Inside an instance method, zero argument super() uses the class where the method was defined and the current instance. Python then searches after that class in the method resolution order, called the MRO. It does not simply choose one direct parent.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
For example, ReportProcessor can inherit from LoggingMixin, ValidationMixin, and BaseProcessor. ReportProcessor calls super(). LoggingMixin logs the request and delegates. ValidationMixin checks the data and delegates. BaseProcessor intentionally ends the chain. Each method runs once in MRO order.
Signatures must be compatible. A useful pattern is for each class to accept the named arguments it owns and pass remaining arguments through **kwargs. If a class does not call super(), later methods are skipped. If it forwards an unsupported argument, Python can raise TypeError.
The runtime cost is one method call and MRO lookup for each participating class. Zero argument super() creates a small proxy object, but it does not copy the instance or its data. The memory cost is normally small. In production, keep the hierarchy shallow, document the argument contract, inspect the MRO, and test the complete call chain.
Example
The example defines ReportProcessor, LoggingMixin, ValidationMixin, and BaseProcessor. Calling process on ReportProcessor starts the cooperative chain. Each zero argument super() call continues with the next class in ReportProcessor's MRO. LoggingMixin consumes request_id. ValidationMixin consumes is_valid. Both pass the remaining named arguments forward. BaseProcessor is the intentional endpoint and rejects any argument that no earlier class consumed. The printed MRO and messages show the exact lookup and execution order.
Code
classBaseProcessor:
defprocess(self, **kwargs):
# This class is the intentional endpoint of the cooperative chain.# Reject arguments that no earlier class consumed.if kwargs:
unexpected = ", ".join(sorted(kwargs))
raise TypeError(f"Unexpected arguments: {unexpected}")
print("Base processing complete")
classValidationMixin:
defprocess(self, *, is_valid, **kwargs):
# Consume the argument owned by this class.ifnot is_valid:
raise ValueError("The report is not valid")
print("Validation complete")
# Continue with the next class in the MRO.super().process(**kwargs)
classLoggingMixin:
defprocess(self, *, request_id, **kwargs):
# Consume the argument owned by this class.print(f"Logging request {request_id}")
# Continue with the next class in the MRO.super().process(**kwargs)
classReportProcessor(LoggingMixin, ValidationMixin, BaseProcessor):
defprocess(self, **kwargs):
print("Report processing started")
# Continue with LoggingMixin, the next class in the MRO.super().process(**kwargs)
if __name__ == "__main__":
processor = ReportProcessor()
# Show the exact order Python uses for method lookup.print([cls.__name__ for cls in ReportProcessor.mro()])
# Each mixin consumes its own named argument.
processor.process(request_id="REQ123", is_valid=True)
Where it is used
This pattern is used when small mixins add separate behavior to one operation. Common examples include validation, logging, access checks, serialization, and framework lifecycle methods. It works best when each class has one clear responsibility and all classes can follow the same method contract. Composition is usually clearer when behaviors need different arguments, independent state, or an explicit execution order.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands Python method resolution order, zero argument super(), and safe method design in a multiple inheritance hierarchy. They also evaluate whether the candidate can recognize fragile inheritance designs and choose composition when it is clearer.
Common interview mistakes
A common mistake is assuming super() always calls one direct parent. It actually continues after the current defining class in the MRO. Another mistake is calling a parent class by name, which can skip another class or cause one implementation to run more than once. The chain also breaks when an intermediate class forgets to call super(), uses an incompatible signature, removes an argument owned by another class, or forwards an unsupported argument. A terminal base may end the chain intentionally, but that endpoint should be clear and documented.
Interview tip
Begin by saying that super() follows the MRO rather than one direct parent. Then explain the two main rules: every intermediate class must delegate, and all participating methods must use compatible signatures. Finish with one small mixin example and mention that composition is often clearer for unrelated behaviors.
Interviewer may ask next
What happens if one intermediate class does not call super()?
The cooperative chain stops at that class. Every later process implementation in the MRO is skipped because Python continues only when the current method delegates with super(). This matters because required validation, logging, cleanup, or base behavior may never run. An intentional terminal base can stop the chain, but an intermediate class should not.
When should composition be preferred over cooperative multiple inheritance?
Composition should be preferred when the behaviors need different method contracts, own independent state, or require an explicit execution order. Cooperative inheritance can reduce repeated wiring for small compatible mixins, but composition usually makes dependencies and control flow easier to understand, replace, and test.
76. How does Python determine whether an object is hashable?Language SpecificHard
i Question Details
Explain the relationship among __hash__, __eq__, immutability expectations, dictionary and set invariants, and why overriding equality can make instances unhashable.
Short Interview Answer (30-60 seconds)
Python considers an object hashable when hash(obj) can call its type’s __hash__ method and receive an integer. The hash must stay stable while the object is in use, and objects that compare equal must have the same hash. Hashable objects can be dictionary keys and set members. When a class defines __eq__ but does not define a matching __hash__, Python normally sets __hash__ to None, so its instances become unhashable.
Detailed Explanation
Python determines hashability by checking the __hash__ behavior provided by the object’s type. When code calls hash(obj), Python calls that hash implementation. If the type sets __hash__ to None, or the operation raises TypeError because hashing is unsupported, the object is unhashable. A valid __hash__ method must return an integer.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
Hashing and equality must follow one important rule. If a == b is true, hash(a) and hash(b) must be equal. Dictionaries and sets use the hash to choose where to search, then use equality to confirm a match. Breaking this rule can cause incorrect lookups.
Immutable built in values such as integers and strings are hashable. A tuple is hashable only when every contained value is hashable. Lists, dictionaries, and sets are unhashable because their contents can change.
Custom class instances use identity based equality and hashing by default. If a class overrides __eq__ without defining __hash__, Python normally sets __hash__ to None. This prevents an equality definition from silently conflicting with the inherited identity hash. A custom hash should use only stable values that also take part in equality.
Where it is used
Hashable objects are used as dictionary keys, set members, cache keys, graph nodes, and unique domain identifiers. In production code, a value object such as an immutable account identifier can safely define equality and hashing from the same stable fields. A mutable profile or configuration object should usually remain unhashable when fields used for equality can change. Computing a hash has a cost based on the object type and its contents. For example, hashing a tuple requires hashing its elements. A custom hash method should avoid unnecessary allocation and expensive repeated work.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands how Python dictionaries and sets identify objects. It checks knowledge of __hash__, __eq__, the equality and hash contract, and the risks of using changing values in hashed collections.
Common interview mistakes
A common mistake is assuming that immutability alone automatically makes an object hashable. Hashability depends on the type’s __hash__ behavior. Another mistake is assuming that every tuple is hashable. A tuple containing a list is unhashable. Developers may also override __eq__ and forget that Python normally disables __hash__. A more serious mistake is calculating the hash from mutable fields. If those fields change after insertion into a dictionary or set, lookup and removal may fail. Equal objects must never return different hashes. Unequal objects may return the same hash, but too many collisions can reduce performance.
Interview tip
Begin with the direct rule: hash(obj) must succeed, the hash must stay stable, and equal objects must have equal hashes. Then explain why dictionaries and sets need that rule. Finish by mentioning that overriding __eq__ without a matching __hash__ normally makes instances unhashable.
Interviewer may ask next
Is every immutable object hashable in Python?
No. Immutability is an important expectation, but Python determines hashability from the type’s __hash__ behavior. A tuple is immutable, yet it is unhashable when it contains an unhashable value such as a list. This matters because the tuple hash depends on the hashes of its elements.
What are the tradeoffs of defining __hash__ for a custom class?
Defining __hash__ allows instances to be used as dictionary keys and set members, but it creates a strict contract with __eq__. The same stable fields should normally be used by both methods. Hash calculation also adds runtime work, especially when it processes many fields. The main tradeoff is convenience in hashed collections versus the risk of broken lookups when equality fields can change.
77. How do metaclasses control class creation?Language SpecificHard
i Question Details
Explain that classes are instances of metaclasses, how the metaclass is selected, the roles of __prepare__, __new__, and __init__, and practical uses and risks of metaclass-based customization.
Short Interview Answer (30-60 seconds)
Metaclasses control the process that creates class objects. Most Python classes are instances of type, but a class can use a custom metaclass. Python selects a metaclass that is compatible with all base classes. It calls __prepare__ to create the namespace for the class body, __new__ to create the class object, and __init__ to finish initializing that object. I would use a metaclass for rules that must apply during class creation, but I would prefer a class decorator or __init_subclass__ when either gives a simpler solution.
Use a metaclass when you must control how Python creates classes, not how those classes create instances. A class is an object, and the object that creates it is its metaclass. Most classes use type.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
When Python reaches a class statement, it determines the metaclass. An explicit metaclass argument is considered first. Python then checks the metaclasses of all base classes and selects the most specific compatible choice. If no compatible metaclass exists, Python raises TypeError.
Python calls __prepare__ to obtain the namespace in which the class body will run. After the body finishes, __new__ receives the class name, bases, namespace, and class options. It validates or changes that data and creates the class object. The metaclass __init__ then performs final initialization.
A metaclass can validate required attributes, register created classes, or change class definitions. Its work happens once each time a class statement executes. The time cost depends on the work performed. A registry also uses memory because it keeps references to registered classes. Metaclasses can cause inheritance conflicts and make code difficult to follow, so simpler tools should be preferred when possible.
Example
This example uses a metaclass to validate and register model classes. __prepare__ returns the namespace used while the class body executes and adds a shared created_by attribute. __new__ checks that every concrete model defines a nonempty table_name before it creates the class object. It also passes the complete namespace to type.__new__, which is important because Python may place internal values such as __classcell__ in that namespace. __init__ runs after the class object exists and stores the concrete model in a registry. The validation and registration happen once when each class statement executes. The registry keeps a strong reference to each registered class, so it also has a small memory cost for every entry.
Code
classModelMeta(type):
# Keep valid model classes under their table names.
registry = {}
@classmethoddef__prepare__(metaclass, class_name, bases, **class_options):
# Create the namespace in which the class body will run.
namespace = {}
# Add a value that every created class can receive.
namespace["created_by"] = "ModelMeta"return namespace
def__new__(metaclass, class_name, bases, namespace, **class_options):
# The shared base class does not represent a database table.if class_name != "BaseModel":
table_name = namespace.get("table_name")
# Stop class creation when the required value is missing or empty.ifnotisinstance(table_name, str) ornot table_name.strip():
raise TypeError("Model classes must define a nonempty table_name")
# Pass the complete namespace to type.__new__.# This preserves internal values that Python may add.
created_class = super().__new__(
metaclass,
class_name,
bases,
namespace,
**class_options,
)
return created_class
def__init__(created_class, class_name, bases, namespace, **class_options):
# Finish normal initialization of the new class object.super().__init__(
class_name,
bases,
namespace,
**class_options,
)
# Register only concrete model classes.if class_name != "BaseModel":
ModelMeta.registry[created_class.table_name] = created_class
classBaseModel(metaclass=ModelMeta):
passclassUser(BaseModel):
table_name = "users"print(User.created_by)
print(ModelMeta.registry["users"].__name__)
Where it is used
Metaclasses are useful in libraries and frameworks that must apply the same creation rules to many classes. Real uses include collecting declared fields, registering plugin classes, validating model definitions, creating mapping metadata, and adding class level behavior before normal application code uses the class. They are most appropriate when the rule must run as part of class creation. A class decorator is often clearer for changing one completed class. __init_subclass__ is often clearer when a base class only needs to validate or register its subclasses.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands that Python creates classes as runtime objects. They also evaluate knowledge of metaclass selection, the class creation sequence, inheritance conflicts, and whether the candidate can choose a simpler design when a metaclass is not necessary.
Common interview mistakes
A common mistake is saying that a metaclass creates normal instances. A metaclass creates a class object, while that class normally creates its instances. Another mistake is confusing metaclass __new__ with the __new__ method used during instance creation. Developers may also forget that the selected metaclass must be compatible with the metaclasses of every base class. This can cause a metaclass conflict during multiple inheritance. Other mistakes include returning an unsuitable object from __prepare__, failing to return the created class from __new__, discarding values from the original namespace, performing expensive input or network work during class creation, and keeping unnecessary class references in a permanent registry. The main design mistake is using a metaclass when a decorator, __init_subclass__, or normal inheritance would be easier to understand.
Interview tip
Explain the sequence in order. Start by saying that classes are objects and most are instances of type. Then explain metaclass selection, __prepare__, execution of the class body, __new__, and __init__. Finish with one practical use, one inheritance risk, and one simpler alternative.
Interviewer may ask next
What happens when base classes have incompatible metaclasses?
Python raises TypeError before creating the new class. The selected metaclass must be a subclass of the metaclass used by every base class. This rule matters because one metaclass must control the complete creation process while remaining compatible with all inherited class behavior. A combined metaclass can sometimes solve the conflict by inheriting from the required metaclasses, but this adds complexity and may still fail when their behaviors do not work together.
When should __init_subclass__ be used instead of a metaclass?
__init_subclass__ should be used when a base class only needs to validate, configure, or register subclasses after Python creates them. It is usually easier to read and avoids custom metaclass selection and many metaclass conflicts. A metaclass remains useful when the namespace must be customized through __prepare__, when data must be checked before the class object is created, or when the class creation process itself must be changed. Both approaches add work when a class is defined, but their actual performance and memory costs depend on the logic they execute and the references they retain.
78. How do slots change Python class instances?Language SpecificHard
i Question Details
Explain how __slots__ restricts declared instance attributes, can remove the normal per-instance __dict__, affects memory and weak references, and interacts with inheritance and dataclasses.
Short Interview Answer (30-60 seconds)
Using __slots__ declares the instance attributes a class expects and can remove the normal instance __dict__. This can reduce memory when an application creates many small objects. It also prevents normal assignment to undeclared attributes when no parent or subclass provides __dict__. Slots do not make an object immutable, and inheritance, weak references, and dataclasses need careful handling.
Use __slots__ when a class creates many instances with a small and stable set of attributes. A normal Python instance usually stores its attributes in an instance __dict__. That dictionary allows new attribute names to be added at runtime. A class that declares __slots__ gets descriptors for the declared names and may avoid the instance dictionary.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
If neither the class nor its parent classes provide __dict__, assigning an undeclared attribute raises AttributeError. Slots do not make existing attributes read only. Their values can still change unless other code prevents it.
The memory benefit matters most when many instances exist. The exact saving depends on the Python implementation, inheritance structure, and declared slots. Attribute access may also be slightly faster, but this is not guaranteed and should be measured.
Weak references require a __weakref__ slot unless a parent already provides weak reference support. A subclass without __slots__ normally gains __dict__ and restores dynamic attributes. A parent with __dict__ also keeps dictionary storage available.
For dataclasses, slots=True creates generated slots. Add weakref_slot=True when weak references are required. Use slots only when reduced flexibility and compatibility limits are acceptable.
Example
The example compares a normal class with a slotted class. The normal instance accepts a new label attribute because it has __dict__. The slotted instance accepts only x and y, so assigning label raises AttributeError. The class includes __weakref__, so weak references work. The subclass does not declare __slots__, so Python gives it __dict__ and dynamic attributes become available again. The dataclass uses slots=True and weakref_slot=True, which creates slotted fields and weak reference support.
Code
from dataclasses import dataclass
import weakref
classNormalPoint:
# Normal instances usually store attributes in __dict__.def__init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
classSlottedPoint:
# These are the allowed instance attribute names.# __weakref__ allows weak references to instances.
__slots__ = ("x", "y", "__weakref__")
def__init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
classFlexiblePoint(SlottedPoint):
# No __slots__ declaration means this subclass gains __dict__.pass@dataclass(slots=True, weakref_slot=True)classUserRecord:
# The dataclass creates slots for these fields.
name: str
score: intdefmain() -> None:
normal = NormalPoint(10, 20)
# A normal instance can receive a new attribute.
normal.label = "start"print("Normal dictionary:", normal.__dict__)
slotted = SlottedPoint(10, 20)
print("Slotted values:", slotted.x, slotted.y)
print("Slotted has dictionary:", hasattr(slotted, "__dict__"))
try:
# label is not declared in SlottedPoint.__slots__.
slotted.label = "start"except AttributeError as error:
print("Undeclared attribute error:", error)
# This works because __weakref__ is declared.
point_reference = weakref.ref(slotted)
print("Point weak reference works:", point_reference() is slotted)
flexible = FlexiblePoint(30, 40)
# The subclass has __dict__, so this assignment works.
flexible.label = "allowed"print("Subclass dictionary:", flexible.__dict__)
user = UserRecord("Ava", 95)
user_reference = weakref.ref(user)
print("Dataclass value:", user)
print("Dataclass has dictionary:", hasattr(user, "__dict__"))
print("Dataclass weak reference works:", user_reference() is user)
if __name__ == "__main__":
main()
Where it is used
Slots are useful in systems that create large numbers of small and predictable objects, such as coordinates, parsed records, syntax tree nodes, game entities, messages, and cached entries. They can also prevent accidental attribute names caused by spelling mistakes. Normal classes are usually better when objects need dynamic attributes, frequent extension, simple inheritance, or compatibility with tools and frameworks that expect __dict__.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands how Python stores instance attributes and how object layout affects memory, flexibility, inheritance, weak references, and dataclasses. It also shows whether the candidate can choose an optimization only when its limits are acceptable.
Common interview mistakes
A common mistake is saying that __slots__ always removes __dict__. A parent class may already provide a dictionary, a subclass without slots normally adds one, and __dict__ can be declared as a slot. Another mistake is saying that slots make objects immutable. Declared attributes can still be changed. Developers may also forget __weakref__, repeat a parent slot name in a subclass, combine incompatible slotted base classes, assume every tool supports slotted objects, or claim memory and speed improvements without measuring the real application.
Interview tip
Start with the practical rule that slots declare expected attributes and can remove the instance dictionary. Then explain the memory benefit, the undeclared attribute error, and the main exceptions involving inheritance, weak references, and dataclasses. Make it clear that slots are an optional optimization, not a default rule.
Interviewer may ask next
What happens when a subclass of a slotted class does not declare __slots__?
The subclass normally receives an instance __dict__. Its instances can then accept dynamic attributes that were not declared by the parent slots. The inherited slot attributes still use their slot storage, but the new dictionary reduces the memory benefit and removes the strict attribute restriction for the subclass.
Should every class with many instances use __slots__?
No. Use __slots__ only when the attribute set is stable and measurement shows a useful memory or access benefit. The main tradeoff is reduced flexibility. Slots can complicate inheritance, weak references, serialization, inspection, and framework integration, so a normal class is often the safer production choice.
79. How do abstract base classes and virtual subclasses work?Language SpecificHard
i Question Details
Explain ABCMeta, @abstractmethod, enforcement at instantiation, register(), subclass and instance checks, and how abstract base classes differ from informal duck typing and static protocols.
Short Interview Answer (30-60 seconds)
Use an abstract base class when related implementations need an explicit runtime contract. ABCMeta provides the machinery, while inheriting from ABC is the usual simpler syntax. A method marked with abstractmethod must be implemented before a normal subclass can be instantiated. register makes an unrelated class a virtual subclass, so isinstance and issubclass recognize it, but registration does not add methods, change its method resolution order, or verify that it follows the interface.
Use an abstract base class when several related classes must follow an explicit runtime contract. ABCMeta is the metaclass that tracks abstract methods and controls subclass and instance checks. Inheriting from ABC is the common shortcut because ABC already uses ABCMeta. A method marked with abstractmethod remains required until a normal subclass overrides it with a nonabstract attribute. Python allows the subclass definition, but creating an instance raises TypeError while any abstract method remains. Python checks abstract status, not the method signature, so a wrong signature can still satisfy the runtime rule.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
The register method makes an unrelated class, and its descendants, virtual subclasses. After registration, issubclass and isinstance return true. The abstract base class is not added to their method resolution order. Its methods are not inherited, and abstract method enforcement does not apply to them.
This is stricter than informal duck typing, which simply calls the needed behavior. A static Protocol lets type checkers accept matching structure without required inheritance. Use abstract base classes for framework extension points and shared runtime rules. Registration adds small runtime bookkeeping and cached checks, but it does not copy objects or allocate per instance data.
Example
MessageSender is an abstract base class because it inherits from ABC. Its send method is marked with abstractmethod. EmailSender implements send, so it is concrete and can be instantiated. IncompleteSender leaves send abstract, so Python raises TypeError when code tries to create an instance. LegacySender does not inherit from MessageSender. Calling register makes it a virtual subclass, so isinstance and issubclass return true. Registration does not add MessageSender to the method resolution order and does not validate or copy the send method. The example works because LegacySender already provides the expected behavior.
Code
from abc import ABC, abstractmethod
classMessageSender(ABC):
# A normal subclass must replace this abstract method. @abstractmethoddefsend(self, message: str) -> str:
raise NotImplementedError
classEmailSender(MessageSender):
# This concrete method removes the abstract requirement.defsend(self, message: str) -> str:
returnf"Email sent: {message}"classIncompleteSender(MessageSender):
# The inherited send method is still abstract.passclassLegacySender:
# This class is unrelated but already has the expected behavior.defsend(self, message: str) -> str:
returnf"Legacy message sent: {message}"# Registration changes subclass and instance checks only.
MessageSender.register(LegacySender)
email_sender = EmailSender()
legacy_sender = LegacySender()
print(email_sender.send("Hello"))
print(legacy_sender.send("Hello"))
print(isinstance(email_sender, MessageSender))
print(isinstance(legacy_sender, MessageSender))
print(issubclass(LegacySender, MessageSender))
print(MessageSender in LegacySender.__mro__)
try:
IncompleteSender()
except TypeError as error:
print(type(error).__name__)
Where it is used
Abstract base classes are useful for plugin interfaces, storage adapters, message senders, serializers, and framework extension points. A direct subclass is appropriate when the project controls the implementation and wants instantiation enforcement or shared methods. Virtual registration is useful when an existing or external class already supports the expected behavior but should not inherit from the abstract base class. Production code should register only classes whose behavior is covered by tests because registration does not inspect required methods or signatures. Repeated isinstance and issubclass checks use the abstract base class machinery and internal caching, but exact timing is an implementation detail. The feature adds class level metadata and registry entries. It does not copy application values or add special memory to every instance.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands runtime contracts, metaclasses, abstract method enforcement, nominal relationships, structural compatibility, and the limits of isinstance and issubclass checks. It also tests whether the candidate can choose an abstract base class, virtual registration, informal duck typing, or a static protocol for a production design.
Common interview mistakes
A common mistake is thinking register adds inheritance. It does not place the abstract base class in the registered class method resolution order, and methods from the abstract base class are not available through super. Another mistake is assuming registration validates required methods or signatures. It performs neither check. A normal subclass can also satisfy the runtime abstract rule with a method that has an incompatible signature, so type checking and tests are still important. Developers may also overuse isinstance instead of calling the required behavior directly. Finally, abstract methods may contain reusable code, but a concrete subclass must still override them before instantiation.
Interview tip
Start with the decision: use an abstract base class for an explicit runtime contract. Then explain ABCMeta, abstractmethod enforcement at instantiation, and register. State clearly that a virtual subclass passes isinstance and issubclass checks but gains no methods. Finish by comparing runtime enforcement with informal duck typing and static Protocol checks.
Interviewer may ask next
Can a subclass be instantiated if it overrides an abstract method with the wrong signature?
Yes, Python can allow the instance because ABCMeta checks whether the abstract attribute was replaced, not whether the new signature is compatible. The exact change is that the subclass provides a nonabstract send attribute, so the abstract requirement is cleared. Calls may still fail or accept the wrong arguments. This matters because abstract base classes enforce method presence at runtime, while type checkers and tests are needed to verify signature compatibility.
When should a static Protocol be used instead of an abstract base class?
Use a static Protocol when code should accept any object with compatible methods and attributes without requiring inheritance or registration. The exact change is from a nominal runtime contract to structural checking by a type checker. This keeps implementations loosely connected and fits normal duck typing. The tradeoff is that type annotations are not enforced by Python at runtime, and runtime checkable protocols only perform limited member presence checks rather than full signature validation.
80. What is Big O notation, and why does it matter when comparing Python solutions?CodingEasy
i Question Details
Define Big O notation as a way to describe how running time or extra-space use grows as input size grows. Explain O(1), O(log n), O(n), O(n log n), and O(n²) using small Python operations, distinguish growth rate from exact execution time, and show how constraints and list, dictionary, set, heap, and sorting operations guide solution choice.
Short Interview Answer (30-60 seconds)
I use Big O to compare how the work or extra memory grows as input size n grows. It is about growth, not exact seconds. For example, list index access is O(1), binary search is O(log n), one full scan is O(n), sorting is generally O(n log n), and two nested loops are O(n²). Python dictionaries and sets give average O(1) lookup or membership checks, while heap push and pop are O(log n). I use the input constraints to choose the simplest approach that scales well.
The question asks how we decide whether one Python solution will scale better than another as the amount of data becomes larger. Big O gives a simple way to describe that growth. It can describe running time or extra memory. It does not predict exact seconds because real speed also depends on the machine, Python version, implementation, and constant factors. The main goal is to compare growth rates, understand common Python operation costs, and use the input limits to choose a solution that will still work when n becomes large.
Useful Questions to Ask the Interviewer
How large can n become?
Should I discuss running time, extra space, or both?
Is average-case behavior for Python dictionaries and sets acceptable for this comparison?
How to Explain It in an Interview
1. Define Big O in simple words
Let n mean the input size. Big O tells us how the amount of work or extra memory changes when n grows. It focuses on the growth pattern. It does not give an exact number of milliseconds.
2. Explain the five growth rates
O(1) is constant growth. The amount of work stays about the same as n grows. The diagram shows list index access, dictionary key access on average, and set insertion on average.
O(log n) grows very slowly. Binary search on a sorted list is the example. Each step removes a large part of the remaining search range.
O(n) is linear growth. If n doubles, the amount of work is roughly doubled. A loop that visits each item once is the main example.
O(n log n) grows faster than O(n) but much slower than O(n²). General-purpose comparison sorting is the main example in the diagram. Python's sorted() and list.sort() have O(n log n) worst-case time.
O(n²) is quadratic growth. Two nested loops that each run n times perform about n × n operations. This can become expensive quickly when n is large.
3. Connect complexity to Python data structures
A list gives O(1) index access, but searching for a value is O(n). A dictionary gives average O(1) get and set by key. A set gives average O(1) add and membership checks. Python's heapq is a min-heap. heappush() and heappop() are O(log n), so a heap is useful for repeated priority operations such as top-k work.
One technical correction is important: heapq.heapify(arr) builds a heap in O(n) time. It is not O(n log n). The diagram places heapify near the O(n log n) examples, but the individual heap push and pop operations shown elsewhere are correctly O(log n).
4. Let constraints guide the solution
For a small n, an O(n²) solution may be acceptable. As n becomes larger, lower growth rates become more important. For many repeated lookups or membership tests, a dictionary or set is often a better fit than repeatedly scanning a list. For repeated smallest-item or priority operations, a heap can be useful. For ordering data, sorting usually costs O(n log n).
5. Separate growth rate from exact execution time
Two programs can have the same Big O but still run at different speeds because they do different constant amounts of work. An O(n) program can even be slower than an O(n log n) program for a small input. Big O becomes most useful when we care about how behavior changes as n becomes large.
6. Finish with the decision rule
First check the input limits. Then identify the operations the solution performs most often. Choose the simplest algorithm and Python data structure whose growth rate fits those limits. Measure real performance when needed, but use Big O to reason about whether the solution will scale.
Key Insight / Why This Solution Works
This is a complexity-comparison question rather than one problem with a single algorithm. The key insight is to classify each possible operation or solution by how its running time or extra memory grows with n. The main invariant is conceptual: for the same growing input, the lower-order growth rate usually becomes more scalable as n gets large. Then match the work you need to a suitable Python structure. Use list indexing for direct access, dictionaries or sets for average constant-time hash operations, heaps for repeated logarithmic priority operations, and sorting when ordered data is needed.
Code
from bisect import bisect_left
import heapq
defconstant_time_examples(arr: list, d: dict, s: set, key, x):
# O(1): access one list item by its index.
first_item = arr[0]
# O(1) on average: get a dictionary value by key.
dictionary_value = d[key]
# O(1) on average: add one value to a set.
s.add(x)
return first_item, dictionary_value
deflogarithmic_search(sorted_arr: list, x) -> int:
# O(log n): bisect_left performs binary search on a sorted list.
index = bisect_left(sorted_arr, x)
return index
deflinear_scan(arr: list) -> None:
# O(n): visit each item once.for x in arr:
_ = x
defn_log_n_sort(arr: list) -> list:
# O(n log n) worst-case time: return a sorted copy.returnsorted(arr)
defbuild_heap(arr: list) -> list:
# O(n): heapify builds a min-heap in place.
heap = arr.copy()
heapq.heapify(heap)
return heap
defheap_operations(heap: list, x):
# O(log n): push one item into Python's min-heap.
heapq.heappush(heap, x)
# O(log n): remove and return the smallest item.return heapq.heappop(heap)
defquadratic_work(n: int) -> None:
# O(n^2): each loop runs n times.for i inrange(n):
for j inrange(n):
_ = (i, j)
if __name__ == "__main__":
# The diagram contains operation patterns, not one concrete# input/output example. Running this file therefore defines# the same examples without inventing a different problem.pass
Time & Space Complexity
There is no single time or space complexity for the whole question because the diagram compares several operations. O(1) means the work stays about the same as n grows. O(log n) grows very slowly. O(n) grows in direct proportion to n. O(n log n) is common for efficient sorting. O(n²) often appears when two loops each run across n items. Extra-space complexity uses the same notation but measures additional memory. Python dictionary and set lookup or insertion are O(1) on average. Heap push and pop are O(log n). heapq.heapify() is O(n). Python sorting is O(n log n) in the worst case.
Where it is used
Big O is used whenever engineers compare ways to solve the same problem. It helps decide whether to scan a list, use a dictionary or set, sort the data, or keep items in a heap. It is especially useful when inputs can become large because it helps rule out solutions whose running time or memory grows too quickly.
Why Interviewers Ask This
Interviewers use this question to see whether you can reason about how code scales instead of only checking whether it works on a small example. They want you to know the difference between growth rate and exact runtime, recognize common Python operation costs, choose a suitable data structure, and use input constraints to reject approaches that grow too quickly. They also expect accurate average-case wording for dictionaries and sets and correct costs for sorting and heap operations.
Common interview mistakes
A common mistake is treating Big O as an exact runtime instead of a growth rate. Another is saying dictionary and set operations are guaranteed O(1); in Python they are O(1) on average. Candidates may also confuse O(1) list index access with O(n) list search. Another mistake is forgetting the O(n log n) sorting cost inside a larger solution. It is also incorrect to call heapq.heapify() O(n log n); heap construction with heapify is O(n), while individual heap push and pop operations are O(log n). Finally, choosing O(n²) without checking how large n can become can lead to a solution that is too slow.
Interview tip
When comparing two solutions, name n first, identify the operation that dominates the work, state its Big O, and then explain whether that growth rate is safe for the given input limits.
Interviewer may ask next
Why can an O(n) solution sometimes run slower than an O(n log n) solution for a small input?
Big O describes how work grows as n becomes large. It hides constant factors and many implementation details. An O(n) solution may do expensive work during every step, while an O(n log n) solution may have small constant costs. For a small n, the second program can therefore be faster. Their growth classes do not change: one is still O(n) and the other is still O(n log n). As n becomes very large, the lower growth rate usually becomes more important.
When should I use a dictionary or set instead of repeatedly searching a list?
Use a dictionary or set when you need many key lookups or membership tests. Searching a list for a value is O(n) each time. Dictionary and set lookup are O(1) on average. Building a dictionary or set from n items normally takes O(n) expected time and O(n) extra space. After that, repeated lookups are fast on average. The tradeoff is extra memory, and hash-table operations do not have a guaranteed worst-case O(1) time.
More questions load as you scroll
Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.
Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.