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.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
71. 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.
72. How does exception chaining work?Language SpecificHard
i Question Details
Explain implicit context through __context__, explicit causes using raise ... from ..., suppression with from None, traceback presentation, and when preserving causal information improves debugging.
Short Interview Answer (30-60 seconds)
Exception chaining connects a new exception to an earlier exception. If I raise a new exception while handling another one, Python automatically stores the earlier exception in __context__. If the earlier exception is the direct cause, I use raise NewError from original_error, which stores it in __cause__. If I use from None, Python hides the automatic context from the normal traceback but does not remove the stored __context__. Explicit chaining is usually best when translating a low level failure into a clearer application error because the traceback keeps the real cause.
Detailed Explanation
The practical rule is to preserve the earlier exception when it explains why the new exception occurred. If a new exception is raised while another exception is being handled, Python automatically assigns the active exception to the new exception's __context__. The traceback normally says that another exception occurred while handling the earlier one.
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?
Use raise NewError from original_error when the earlier exception is the direct cause. Python stores that exception in __cause__, sets context suppression, and presents the cause before the new exception. The traceback says that the new exception was the direct result of the earlier one. This makes error translation clear across abstraction layers.
Use raise NewError from None when the automatic context is not useful to the person reading the traceback. Python sets __cause__ to None and __suppress_context__ to true. The original exception can still remain in __context__, but the standard traceback does not display it.
Chaining adds little work beyond exception creation and traceback handling. It can keep references to earlier exceptions, traceback frames, and local values while the exception chain remains reachable. Production code should preserve useful causes, avoid retaining exception objects unnecessarily, and suppress context only when the hidden details do not help debugging.
Where it is used
Exception chaining is used when application code converts a parsing, file, network, database, or library error into a clearer domain specific error. A service layer may raise a business error from a lower level storage error. A library may expose a stable public exception while preserving its internal cause. API code may return a safe client message while internal logging records the complete chained traceback.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands how Python records relationships between exceptions and presents them in tracebacks. It also tests whether the candidate can translate low level failures into clearer application errors without losing useful debugging information.
Common interview mistakes
A common mistake is raising a replacement exception without from when the original exception is the known direct cause. The automatic context may still exist, but the intended causal relationship is less precise. Another mistake is believing that from None deletes the original exception. It only suppresses implicit context in the standard traceback. Developers may also suppress useful context only to shorten logs, catch Exception too broadly before translating it, or log only the final error message instead of the complete traceback. Another mistake is using chaining when a bare raise is better. A bare raise should be used when the same exception should continue with its existing traceback.
Interview tip
Explain the three cases in order. Start with automatic __context__, then explicit __cause__ through raise from, and finally suppression through from None. State that explicit chaining is useful when translating an error while preserving its real cause. Also mention that from None hides traceback context but does not erase __context__.
Interviewer may ask next
What values are stored after raise NewError from None?
The new exception has __cause__ set to None and __suppress_context__ set to true. If it was raised while another exception was active, that earlier exception can still be stored in __context__. This matters because the standard traceback hides the context, but debugging code can still inspect the stored relationship.
When should a bare raise be used instead of exception chaining?
A bare raise should be used when the current exception should continue unchanged with its existing traceback. Exception chaining should be used when code intentionally creates a different exception and needs to record the relationship. The tradeoff is clarity at the abstraction boundary. A new domain error may be easier for callers to handle, while the chained cause preserves the lower level details needed for production debugging.
73. 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.
74. How are coroutines scheduled by asyncio?Language SpecificHard
i Question Details
Explain coroutine objects, tasks, the event loop, awaiting, suspension points, cooperative scheduling, cancellation, and why blocking calls can stall all tasks on the loop.
Short Interview Answer (30-60 seconds)
Asyncio uses an event loop and cooperative scheduling. Calling an async function creates a coroutine object, but does not run it. Awaiting that coroutine runs it as part of the current task. Creating a task schedules it to run independently when the event loop gets control. Each task runs until it reaches an await that must wait. It then suspends so the event loop can run other ready work. Because the loop does not interrupt normal Python code, one blocking call or long calculation can delay every task on that loop.
Detailed Explanation
Asyncio schedules coroutine work through an event loop. Calling an async function creates a coroutine object. The object holds the future execution state, but does not start by itself.
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?
Awaiting a coroutine runs it as part of the current task. Creating a task registers the coroutine with the event loop so it can make progress independently. The loop selects ready tasks and lets each task run until it finishes, raises an exception, or reaches an await that cannot complete yet.
At that suspension point, the task saves its state and gives control back to the loop. When the awaited operation becomes ready, the loop schedules the task to continue. An await may not suspend when its result is already available.
This is cooperative scheduling. Asyncio provides concurrency, but does not make ordinary Python code run in parallel on one event loop. Blocking input and output, time.sleep, or long calculation can stall every task because the loop cannot run other work until control returns.
Each coroutine and task also uses memory for frames, local values, state, and results. Task switching has overhead, but it is usually small compared with the waiting time saved in input and output heavy programs.
Where it is used
Asyncio is useful in programs that manage many waiting operations at the same time. Examples include web servers, API clients, database connections, web sockets, message consumers, timers, and network services. It is most useful when tasks spend much of their time waiting for input and output. Use asynchronous libraries when possible. Move unavoidable blocking input and output to a worker thread. Move heavy calculation to a separate process or another suitable execution service so the event loop remains responsive.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands how asynchronous Python code actually runs. They are evaluating knowledge of coroutine objects, tasks, the event loop, suspension, cancellation, blocking work, and the difference between concurrency and parallel execution.
Common interview mistakes
Common mistakes include believing that calling an async function starts it, creating coroutine objects without awaiting or scheduling them, and assuming that every await always gives control to another task. Another mistake is treating concurrency as parallel execution. Developers may also call time.sleep, use a blocking library, or perform long calculation directly on the event loop. This prevents other tasks from running. It is also incorrect to assume that cancel stops a task immediately. Cancellation is a request and cleanup may still need to run. Fire and forget tasks should be stored and their exceptions should be observed.
Interview tip
Explain the runtime flow in this order: coroutine object, current task or new task, event loop, await, suspension, and resumption. Then state the main limitation clearly. Asyncio is cooperative, so code must return control to the event loop and must not perform blocking work there.
Interviewer may ask next
Does every await allow another task to run?
No. An await suspends the current task only when the awaited operation is not ready. When the result is already available, execution may continue without giving another task a chance to run. This matters because placing await in code does not guarantee fairness or prevent a long section of work from delaying other tasks.
How should cancellation and blocking work be handled in production asyncio code?
Cancellation should be treated as a request, and blocking work should be kept off the event loop. A cancelled task normally receives CancelledError when it next runs, so cleanup should use try and finally and should usually allow cancellation to continue. Blocking input and output can run in a worker thread, while heavy calculation may need a separate process. These choices keep the loop responsive, but add scheduling, memory, and coordination cost.
75. 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.
76. How do __new__ and __init__ differ?Language SpecificHard
i Question Details
Explain object allocation versus initialization, the order in which they run, the return requirements of __new__, and why immutable subclasses or singleton-like designs may override __new__.
Short Interview Answer (30-60 seconds)
__new__ creates and returns an object, while __init__ initializes an object that has already been created. Python calls __new__ first. It then calls initialization only when the returned object is an instance of the requested class or one of its subclasses. I normally use __init__ for regular setup and override __new__ only when creation itself must be controlled, such as for an immutable subclass or a singleton like design.
Detailed Explanation
__new__ handles object creation. It receives the class as its first argument and must return an object. In a normal custom class, it usually calls super().__new__(cls) and returns the new instance.
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 then performs initialization when the returned object is an instance of the requested class or one of its subclasses. __init__ receives that existing object. It can validate input, assign attributes, and prepare state. It must return None. Returning another value raises TypeError.
The order is therefore creation first and initialization second. If __new__ returns an unrelated object, Python skips initialization for the requested construction call.
Most classes should only define __init__. Immutable subclasses may define __new__ because values such as an int, str, or tuple value must be chosen while the object is being created. A singleton like design may also define __new__ to return a cached instance. However, __init__ can still run after each successful class call, so repeated initialization must be safe or guarded.
Overriding __new__ has no fixed performance or memory cost by itself. The cost depends on extra allocation, caching, locking, validation, or lookup logic added by the implementation.
Where it is used
__init__ is used in ordinary application classes to validate constructor input, assign instance attributes, and prepare dependencies. __new__ is used when creation must be controlled before initialization begins. Common examples include subclasses of immutable built in types, instance caching, and singleton like designs. In production, __new__ should remain small and predictable. Cached instances can reduce repeated allocation, but the cache also keeps objects in memory and may require synchronization when several threads can create the object at the same time.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands the two stages of Python object construction. It tests knowledge of instance allocation, initialization order, return rules, immutable types, and the risks of controlling object creation in production code.
Common interview mistakes
A common mistake is saying that __init__ creates the object. The object already exists when __init__ starts. Another mistake is forgetting that __new__ must return an object. If it returns None or an unrelated object, normal initialization does not occur. Developers may also return a value from __init__, but Python requires an implicit or explicit None return. In singleton like designs, it is incorrect to assume that __init__ runs only once. It can run after every class call that returns a valid instance. Another mistake is overriding __new__ for normal attribute assignment when __init__ is simpler and clearer.
Interview tip
Begin with the direct rule: __new__ creates and returns the object, while __init__ prepares the returned object. Then explain the call order, the return requirements, and the case where initialization is skipped. Finish with one practical example involving an immutable subclass.
Interviewer may ask next
What happens if __new__ returns an object that is not an instance of the requested class?
Python returns that object and skips the normal initialization step for the requested construction call. This matters because __new__ can change the final result of calling a class. Returning an unrelated object should therefore be rare, intentional, and clearly documented.
What tradeoff comes with using __new__ to return a cached singleton instance?
The design can avoid repeated allocation and provide one shared object, but it also creates shared state and can keep the object in memory for a long time. __init__ may still run after later class calls, so initialization must be guarded or safe to repeat. Threaded code may also need synchronization around first creation, which adds complexity and possible contention.
77. How do async iterators and async context managers work?Language SpecificHard
i Question Details
Explain the __aiter__ and __anext__ protocols, StopAsyncIteration, async for, the __aenter__ and __aexit__ protocols, async with, and appropriate resource-management use cases.
Short Interview Answer (30-60 seconds)
Use an async iterator when getting the next value may require waiting. It implements __aiter__ and __anext__. Async for calls __aiter__, awaits each __anext__ result, and stops when __anext__ raises StopAsyncIteration. Use an async context manager when resource setup or cleanup may require waiting. It implements __aenter__ and __aexit__, and async with awaits both methods. These protocols are common for network streams, database sessions, locks, and other input or output resources.
Use an async iterator for values that become available over time. The __aiter__ method must return an async iterator directly. Its __anext__ method returns an awaitable that produces one value. When no value remains, it raises StopAsyncIteration. Async for performs these calls and awaits automatically. Any other exception leaves the loop and continues through normal exception handling.
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?
Use an async context manager when entering or leaving a resource may require waiting. Async with awaits __aenter__, runs the block, and then awaits __aexit__. The __aexit__ method receives information about any exception. A truthy return value suppresses that exception. A false value lets it continue. If __aenter__ fails, the block is never entered and __aexit__ is not called.
Each iteration adds an awaited method call, so it has more overhead than a normal loop. It is useful when waiting time is much larger than that overhead. It does not speed up CPU heavy work. Async iteration can process one value at a time, so it can avoid storing the full result in memory. Cancellation still requires care because cleanup code can also be interrupted while it awaits.
Example
The example uses AsyncNumbers as a single use async iterator. Its __aiter__ method returns the same iterator object. Its __anext__ method waits briefly, returns the next number, and raises StopAsyncIteration after three values. AsyncResource is an async context manager. Its __aenter__ method waits and opens the resource. Its __aexit__ method waits and closes the resource, then returns false so exceptions are not hidden. Main enters the resource with async with and consumes the values with async for. The output is Resource opened, the numbers 1, 2, and 3, Resource closed, and Finished.
Code
import asyncio
classAsyncNumbers:
def__init__(self, limit: int):
# Save the final value and current positionself.limit = limit
self.current = 0def__aiter__(self):
# Return the async iterator directlyreturnselfasyncdef__anext__(self):
# End the async for loop after the final valueifself.current >= self.limit:
raise StopAsyncIteration
# Simulate waiting for a value from an external sourceawait asyncio.sleep(0.1)
self.current += 1returnself.current
classAsyncResource:
asyncdef__aenter__(self):
# Simulate asynchronous resource setupawait asyncio.sleep(0.1)
print("Resource opened")
returnselfasyncdef__aexit__(self, exception_type, exception_value, traceback):
# Simulate asynchronous resource cleanupawait asyncio.sleep(0.1)
print("Resource closed")
# Do not suppress an exception from the blockreturnFalseasyncdefmain():
# Await resource setup before entering the blockasyncwith AsyncResource():
# Await each value from the async iteratorasyncfor number in AsyncNumbers(3):
print(number)
# Resource cleanup finishes before this line runsprint("Finished")
if __name__ == "__main__":
asyncio.run(main())
Where it is used
Async iterators are used for streamed network messages, paginated service responses, database rows, log events, and other values that arrive gradually. Async context managers are used for database sessions, transactions, HTTP connections, asynchronous locks, and temporary service connections. In production, they help keep the event loop available while operations wait. They also place setup and cleanup in one reusable object. A developer must still handle timeouts, cancellation, partial setup, and cleanup failures.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands Python asynchronous protocols, awaited runtime behavior, exception flow, cancellation risks, and safe resource management. It also tests whether the candidate can choose the correct tool for values that arrive over time and resources whose setup or cleanup must wait for input or output.
Common interview mistakes
A common mistake is defining __aiter__ with async def in modern Python. That returns a coroutine instead of returning the async iterator directly. Another mistake is returning a plain value from __anext__ instead of returning an awaitable, or raising StopIteration instead of StopAsyncIteration. Developers may also use for instead of async for, or with instead of async with. Returning true from __aexit__ by accident can hide an important exception. Breaking out of async for does not automatically call a custom cleanup method on every async iterator. Resource cleanup should therefore use an explicit async context manager when cleanup is required. Cancellation and failures inside cleanup must also be considered.
Interview tip
Explain the two protocols separately. First say that async for gets the iterator from __aiter__, awaits __anext__, and stops on StopAsyncIteration. Then say that async with awaits __aenter__ and __aexit__ for resource setup and cleanup. Mention that these tools help with waiting operations, not CPU heavy work. Finish with a database or network example and one cancellation warning.
Interviewer may ask next
What happens if __aenter__ raises an exception?
The async with block is not entered, and __aexit__ is not called for that failed entry. This matters because any resource acquired before the failure must be released inside __aenter__ or by another protected cleanup step. The main tradeoff is that setup code becomes more careful because __aexit__ only protects resources after entry succeeds.
When is an async iterator better than returning a complete list?
An async iterator is better when values arrive gradually, may require waiting, or may be too large to store together. It can produce one value at a time and keep memory use close to the iterator state instead of the full result size. The tradeoff is one awaited protocol call per item, more complex error handling, and a result that may be consumed only once when the iterator keeps mutable position like the example.
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?NEWCodingEasy
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.