Python object-oriented programming interviews should reveal whether a candidate can model state, explain how attribute access and method calls work, and keep an objectโs behavior predictable as a system grows. Recruiters can distinguish memorized terminology from practical skill by asking candidates to trace a small class, identify shared-state bugs, and defend an ownership or interface decision.
These 120 questions progress from classes and instances to the data model, reusable interfaces, lifecycle management, and production design. The focus is Python OOP rather than a general Python syntax quiz or a generic design-principles checklist. Beginner questions establish the mechanics, intermediate questions test precise implementation choices, and experienced scenarios examine compatibility, concurrency, debugging, and testability.
How can recruiters tell whether a Python OOP candidate understands object behavior rather than repeating a rehearsed design explanation?
Ask the candidate to predict an attribute lookup, repair a shared mutable default, or explain what happens when a collaborator fails midway through an operation. Change one assumption and ask them to update their design and tests.
Sherlock AI helps recruiters assess signals of potential AI cheating, deepfakes, impersonation, and unauthorized assistance during interviews.
Consider these signals alongside practical work and follow-up discussion rather than treating them as conclusive proof of misconduct. Clear reasoning about state and failure is more informative than an elaborate class hierarchy without a concrete purpose.
Python OOP Interview Questions
Use this complete question bank to choose a balanced interview before consulting the matching answers. Ask for concrete examples and make ownership, runtime version, and concurrency assumptions explicit when they affect the result.
Python OOP Interview Questions for Beginners
Start with classes, methods, attributes, inheritance, and the everyday object protocols. These prompts test whether a candidate can reason about simple stateful code.
What does object-oriented programming mean in Python?
How does a class differ from an instance?
How do you define a class and create an instance?
Is __init__ the method that creates a Python object?
What is self, and why must instance methods declare it?
What are instance attributes, and where should you initialize them?
When should you use a class attribute rather than an instance attribute?
Why is a mutable class attribute a common source of bugs?
How does basic attribute lookup find instance and class values?
What is a bound method in Python?
What is a class method, and when is it useful?
How does a static method differ from an instance method and a class method?
How does Python indicate public and nonpublic attributes?
What does name mangling do for attributes such as __balance?
How can a property help encapsulate an object's state?
What does inheritance provide in Python?
What happens when a subclass overrides a method?
What is duck typing, and how does it enable polymorphism?
How does composition differ from inheritance?
When should you use isinstance instead of comparing type directly?
What is the difference between object identity and equality?
What happens when two variables refer to the same mutable object?
Should a constructor copy a mutable argument before storing it?
Why should a constructor avoid a mutable default argument?
How do __repr__ and __str__ differ?
How do you make a custom object work with len()?
How is an object's truth value determined?
What makes an instance callable?
What is the difference between an iterable and an iterator?
What does the context-manager protocol provide for objects?
How do you define a custom exception class?
What does the dataclass decorator generate?
Why do dataclasses use default_factory for mutable fields?
Do type annotations enforce an object's attribute types at runtime?
What role does the built-in object class play?
Does del immediately destroy an object?
Can you add attributes to an instance after construction?
Where should a class validate the state established by its constructor?
When is returning self from a method appropriate?
When is a function or simple data structure better than a custom class?
Python OOP Interview Questions for Intermediate
Move into method resolution, descriptors, abstract interfaces, generated methods, copying, and lifecycle management. These questions expose implementation assumptions that often survive basic tests.
When should a class implement __new__ instead of relying only on __init__?
How does cooperative super() differ from calling a parent class explicitly?
How does Python resolve a diamond-shaped inheritance hierarchy?
How can composition provide delegation without exposing every implementation detail?
What does an abstract base class enforce at runtime?
When should you choose a Protocol rather than an abstract base class?
What are the limits of runtime-checkable protocols, especially in Python 3.12?
What distinguishes a data descriptor from a non-data descriptor?
How do __getattribute__ and __getattr__ differ?
How can a property preserve an invariant without duplicating validation?
How do you override __setattr__ without causing infinite recursion?
What does __slots__ save, and what does it not guarantee?
Why should __eq__ return NotImplemented for unsupported operands?
What contract must a custom __hash__ satisfy?
What does functools.total_ordering generate, and what must you still design?
How do arithmetic and reflected special methods cooperate?
How is __iadd__ different from __add__ in an augmented assignment?
How do __contains__, __iter__, and __getitem__ support container behavior?
Why should a reusable iterable usually return a fresh iterator?
How should a context manager handle exceptions in __exit__?
How do dataclasses generate equality and ordering methods?
Why is a frozen dataclass not necessarily deeply immutable?
How do dataclass inheritance and __post_init__ affect initialization?
What is the practical difference between shallow and deep copying an object?
Why must a custom __deepcopy__ implementation use the memo dictionary?
What should you explain before using pickle to serialize an object?
How should an object be converted to JSON without blindly exposing __dict__?
When is a weak reference useful, and what limitations matter?
What can a class decorator change, and when does it run?
How does __init_subclass__ support a plugin-style base class?
What problem does a descriptor's __set_name__ method solve?
What is a metaclass, and when is type enough?
How do generic class annotations help without enforcing runtime types?
How do ClassVar and InitVar change dataclass field handling?
How does cached_property affect instance state and concurrency?
Why can introspecting an object accidentally execute code?
How does constructor-based dependency injection improve object testability?
What do mock spec, spec_set, and autospec protect against?
How should a class document ownership of mutable inputs and returned values?
How does an asynchronous context manager differ from a synchronous one?
Python OOP Interview Questions for Experienced
These scenarios assess framework design, API evolution, concurrency, resource ownership, object graphs, and evidence-based debugging. Strong responses describe both a decision and how to validate it.
How would you diagnose an initializer skipped in a cooperative multiple-inheritance hierarchy?
When should an integration subclass a framework type rather than wrap it?
Why can assigning an instance attribute fail to override a class attribute?
Why does assigning a special method to an instance often not change an operator's behavior?
How would you resolve a metaclass conflict without adding unnecessary complexity?
Why must a custom metaclass preserve the class namespace's __classcell__ entry?
What extra work is required when attaching a descriptor after class creation?
How would you make subclass-based plugin registration predictable?
Why can an ABC virtual subclass pass isinstance checks while missing required behavior?
How would you introduce a Protocol into a mature object API without breaking clients?
Why is unsafe_hash=True dangerous for a mutable dataclass used as a dictionary key?
How would you design an immutable value object that normalizes its input?
Can a frozen object safely contain a cache?
Why can multiple inheritance fail when several bases define nonempty __slots__?
What lifetime pitfalls arise when replacing a normal cache with a weak-reference cache?
Why should resource cleanup not depend on __del__ or immediate garbage collection?
How does ExitStack help an object manage a variable number of owned resources?
How should an asynchronous context manager handle cancellation during cleanup?
Why does the GIL not make a mutable object's methods thread-safe?
How would you choose lock boundaries for an object that collaborates with other objects?
How would you prevent concurrent construction of a shared service instance?
How can an async method violate an object's invariant even on one event-loop thread?
What should an object expose when it crosses a multiprocessing boundary?
How would you evolve a persisted object format without tying it to a class's internal layout?
What should deepcopy do for an object that owns a lock or external resource?
Why is a general-purpose object proxy difficult to make fully transparent?
What tooling problems can a dynamic __getattr__ API create?
How would you construct domain objects from untrusted serialized input safely?
What compatibility risks arise when changing a class that users are expected to subclass?
How can changing dataclass fields break structural pattern-matching clients?
Why can an immutable built-in subclass lose its domain invariant after an operation?
How does Self improve annotations for fluent APIs and alternate constructors?
How would you assess whether a subclass preserves a base class's behavioral contract?
How would you make a service object's varying behavior testable without a large subclass hierarchy?
How would you investigate an object graph that appears to retain memory unexpectedly?
How would you decide whether __slots__ is a worthwhile optimization?
When should request-specific object state use ContextVar instead of threading.local?
How would you test an object's lifecycle without relying on garbage-collection timing?
What should contract tests for a custom Python protocol cover beyond method presence?
What would you review before approving a stateful Python service object for production?
Python OOP Interview Questions and Answers
The answers follow the bank in exactly the same order. Each explanation connects the language mechanism to a practical consequence or design decision. The baseline is Python 3.12 and its standard library. Later behavior is labeled explicitly, including optional free-threaded CPython builds introduced in 3.13. Language guarantees are distinguished from CPython implementation details; type-checker results depend on the checker and configuration. Code examples are intentionally small and do not perform network operations or load untrusted pickle data.
Python OOP Interview Questions and Answers for Beginners
The beginner answers explain the mechanism first, then show where it matters. Look for accurate distinctions between an object, a reference to it, and the class that defines its behavior.
1. What does object-oriented programming mean in Python?
Object-oriented programming organizes behavior and state around objects. In Python, an object has an identity, a type, and a value; a class describes a kind of object and supplies behavior its instances can use. For example, an inventory item can hold a quantity and expose a method that rejects withdrawals larger than the available stock. The useful part is keeping related rules and data together, not merely wrapping every function in a class. Python also supports procedural and functional styles, so an application can combine them. A strong design makes ownership and valid state easier to understand without introducing unnecessary inheritance or layers of tiny classes.
2. How does a class differ from an instance?
A class is itself an object that defines a type and provides attributes and behavior; an instance is a particular object created from that class. Two instances can share the same method implementation while carrying different instance attributes. For example, two Account objects may have different balances even though both use Account.deposit. Changing one instance's balance does not automatically change the other's balance. Shared class attributes are an important exception to that simple mental model, especially when they refer to mutable values. In an interview, explain both the blueprint analogy and its limitation: Python classes are runtime objects that can be assigned to variables, passed to functions, and inspected.
3. How do you define a class and create an instance?
Use a class statement to define the class, then call the class to construct an instance. The class body defines attributes such as methods; initialization commonly stores per-instance state. In this example, the two calls create distinct objects, and each greeting reads the receiving object's name rather than a shared variable.
Calling a class can do more than allocate storage: initialization can validate inputs or establish invariants. Keep construction predictable and avoid surprising external side effects unless the API clearly documents them.
4. Is __init__ the method that creates a Python object?
No. __init__ initializes an already created instance; __new__ is responsible for creating and returning the object during the usual construction process. Most everyday classes only need __init__, because the inherited allocation behavior is suitable. Its return value must be None, normally achieved by not writing a return statement. Returning an account balance or another object from __init__ raises a TypeError rather than changing the construction result. For a simple User(name) call, think of allocation followed by initialization, while recognizing that custom construction can alter that sequence. Put ordinary attribute assignment and validation in __init__; do not override __new__ just to make a normal constructor look more explicit.
5. What is self, and why must instance methods declare it?
self is the conventional name for the instance receiving an instance-method call. In item.rename("new"), ordinary method binding supplies item as the first argument, so the definition commonly begins def rename(self, name):. It is not a reserved keyword, but changing the name makes familiar code harder to read. self.name accesses an attribute on the receiver, while a bare name refers to a local or another name resolved by Python's scope rules. Forgetting the first parameter usually produces an argument-count error or assigns arguments to the wrong parameters. The key idea is explicit access to the receiver inside the method, not a magical variable that exists in every function.
6. What are instance attributes, and where should you initialize them?
Instance attributes hold state associated with one instance, commonly assigned through self.attribute in __init__. For example, self.status = "pending" gives each newly initialized order its own status binding. Establishing expected attributes during initialization makes the object's valid shape clear and reduces failures caused by calling methods in the wrong order. Attributes can also be introduced later on ordinary instances, but that flexibility is not a reason to leave essential fields undefined. If a field is optional, initialize it explicitly, perhaps to None, and document what that means. A separate binding per instance does not guarantee separate underlying objects: two attributes may still reference the same mutable value supplied by a caller.
7. When should you use a class attribute rather than an instance attribute?
Use a class attribute for a value intentionally associated with the class, such as a default label, configuration constant, or shared registry. Use an instance attribute for state that belongs to one object, such as an individual order's status. Reading obj.label can find a class attribute when the instance does not provide its own value. Assigning obj.label = "custom" ordinarily creates or updates an instance attribute instead of rewriting the class's value. That distinction makes class-level defaults convenient, but shared mutable class attributes require special care. If a counter is intended to count every instance across subclasses, define exactly which class owns and updates it rather than assuming all assignment forms affect one shared counter.
8. Why is a mutable class attribute a common source of bugs?
A mutable class attribute is shared by instances that resolve that attribute through the class. Mutating the shared object is different from assigning an instance attribute. A list intended to represent each student's courses therefore belongs in initialization, not as a single class-level list.
A shared list is not inherently wrong: it can implement a deliberate registry. The bug is an ownership mismatch. Explain whether every instance should see the same changes before choosing where to create the mutable object.
9. How does basic attribute lookup find instance and class values?
For ordinary attributes without descriptor customizations, Python checks an instance's own attributes and then searches its class and base classes according to the method resolution order. This allows an instance-specific value to shadow a class default. For example, a class may define theme = "light", while one instance sets self.theme = "dark"; other instances can still read the class default. This simplified model is useful but not the full lookup algorithm: properties and other data descriptors can take precedence over an instance dictionary, and classes can customize access. When diagnosing an unexpected value, first check whether the name was assigned on the instance or on the class instead of assuming that both assignments modify the same place.
10. What is a bound method in Python?
A bound method combines a method implementation with the instance that will receive the call. Retrieving an ordinary instance method as obj.method creates a callable whose receiver is already supplied; you can save it and call it later. Retrieving the underlying function through the class instead normally requires supplying the instance explicitly.
This explains why callbacks such as button.on_click = controller.handle_click retain access to the controller's state. It also means a saved bound method can keep its receiver alive while the method remains referenced.
11. What is a class method, and when is it useful?
A class method, declared with @classmethod, receives the class as its first argument, conventionally named cls. It is useful for behavior tied to the class rather than a particular existing instance, especially alternative constructors. For example, Date.from_text(text) can parse input and return cls(year, month, day). Using cls rather than hard-coding Date allows an inherited constructor to construct the receiving subclass, provided its constructor has a compatible interface. A class method does not automatically make a function thread-safe or restrict it to class attributes. It simply changes binding. Choose it when the operation genuinely needs the receiving class; otherwise, a module function or static method may express the responsibility more clearly.
12. How does a static method differ from an instance method and a class method?
A static method receives neither an instance nor a class implicitly. @staticmethod places a function in the class namespace without ordinary instance-method binding. An instance method receives self and is suitable for instance state; a class method receives cls and is suitable for class-aware operations. For example, a parser class might expose a small syntax-checking helper that needs no parser state as a static method. However, a module-level function is often equally clear and easier to reuse. Static methods are not automatically faster, purer, or private. Choose among these forms by the dependencies the operation actually needs, rather than decorating a method merely because its current implementation happens not to use self.
13. How does Python indicate public and nonpublic attributes?
Python normally uses naming conventions rather than enforced public and private access modifiers. A name without a leading underscore is typically part of the public interface; a single leading underscore, such as _cache, signals an implementation detail that callers should not depend on. The interpreter generally still permits direct access. This convention matters when maintaining compatibility: changing a documented public attribute can break callers, while internal names should remain implementation choices. Names with both leading and trailing double underscores, such as __len__, are reserved conventions for Python's protocols, not a way to make an ordinary field more private. Encapsulation therefore depends on a clear interface and disciplined callers, not on treating underscore names as a security boundary.
14. What does name mangling do for attributes such as __balance?
Within a class definition, a name with at least two leading underscores and no more than one trailing underscore is transformed to include the class name, subject to Python's mangling rules. For example, __balance in Account becomes _Account__balance. The main purpose is to reduce accidental name collisions with subclasses, not to prevent access by determined callers. A subclass's own __balance is mangled with its own class name and is therefore a different attribute. This can be useful for internal implementation details in extensible classes, but it can also complicate testing and introspection. Prefer a single underscore unless collision avoidance is genuinely needed, and never store secrets on the assumption that name mangling protects them.
15. How can a property help encapsulate an object's state?
A property exposes a computed or managed value through attribute syntax, letting callers use a stable interface without depending on how a value is stored. A read-only computed property is useful when a value follows from existing state and storing another copy would risk inconsistency.
Here, area always reflects the current dimensions and has no setter. Read-only access to one property does not make the whole object immutable: width and height remain writable. Keep property access unsurprising rather than hiding expensive external work behind a field-like interface.
16. What does inheritance provide in Python?
Inheritance lets a subclass reuse and extend behavior from a base class. If SavingsAccount inherits from Account, attribute lookup can find methods defined on Account without copying their source. The subclass may add behavior or override inherited methods. Inheritance is most useful when callers can reasonably treat the subclass as the base abstraction, not simply when two classes happen to share a few lines of code. A subclass can become tightly coupled to its base class's initialization and internal assumptions. For a component that merely uses another object's services, composition is often clearer. Explain the relationship and behavioral expectations before choosing inheritance as a code-reuse mechanism.
17. What happens when a subclass overrides a method?
An override defines an attribute with the same method name on the subclass, so normal lookup on a subclass instance finds that implementation before the inherited one. Calls through a base-class interface can therefore execute subclass-specific behavior. An override should preserve the caller's meaningful expectations, such as accepted inputs and the kind of result returned. It can call super().method(...) when it needs to cooperate with the next implementation rather than replace everything. An inherited method that calls self.other_method() also uses the actual instance's lookup behavior, so it may reach an override. That is powerful, but it means subclass behavior can affect base-class code in places a reader might not initially expect.
18. What is duck typing, and how does it enable polymorphism?
Duck typing means code relies on the operations an object supports rather than requiring one specific concrete class. A function that calls sender.send(message) can work with an email sender, a test fake, or another compatible object without making them share a custom base class. This is a form of polymorphism: one calling pattern works with different implementations. Compatibility still involves behavior, not just the presence of a method name. Callers need to know arguments, return values, failures, and side effects. Type hints and protocols can document these expectations for static checking, while runtime tests establish behavior. Avoid replacing every useful capability check with broad exception catching that can hide unrelated implementation errors.
19. How does composition differ from inheritance?
Composition builds an object from collaborating objects; inheritance defines a subtype relationship through a base class. For example, an InvoiceService can hold a TaxCalculator and delegate tax computation to it rather than inheriting from the calculator. Composition makes it easy to replace a collaborator in a test or configure different behavior for different instances. It also makes ownership and delegation explicit, although forwarding methods can add some code. Inheritance remains useful when there is a real substitutable abstraction and shared lifecycle. A practical interview answer should identify the relationship: an invoice service uses a tax calculator, but it is not a tax calculator. Reusing implementation alone does not establish a sound inheritance hierarchy.
20. When should you use isinstance instead of comparing type directly?
Use isinstance(obj, Base) when subclasses should count as instances of the abstraction. Use type(obj) is Base only when the exact concrete type is deliberately required. For example, isinstance(True, int) is true because bool is a subclass of int; this matters if a validation rule intends to accept integers but reject Boolean values. Neither check proves that an object satisfies every behavioral expectation of an API. Duck-typed code may need no nominal type check at all, while input boundaries may need precise validation. Explain why a restriction exists before choosing the expression. Exact-type checks can unnecessarily reject useful subclasses, and broad instance checks can accept values that a domain rule should reject.
21. What is the difference between object identity and equality?
Identity asks whether two references point to the same object; equality asks whether the objects compare as equal according to their types. Use is for identity and == for equality. Distinct lists can hold equal contents, while an alias is another reference to the same list.
Custom classes can define equality, but identity is not customized through __eq__. Use value is None for the singleton None. Do not use is to compare strings or integers by value: implementation reuse of some immutable objects can make an incorrect comparison appear to work in a small test.
22. What happens when two variables refer to the same mutable object?
Both variables can observe changes to that object because assignment binds references; it does not clone the object. If backup = original and original is a list, appending through either name changes the same list. Rebinding original to a different list is different: backup still refers to the old one. This distinction also applies to attributes, parameters, and entries inside containers. For an object-oriented API, decide whether a collaborator or collection is deliberately shared or should be copied at the boundary. Copying an outer list only creates a shallow copy, so nested mutable elements may remain shared. Ownership needs to be explicit when independent-looking objects can alter one another through shared state.
23. Should a constructor copy a mutable argument before storing it?
It depends on the ownership contract. Storing self.items = items shares the caller's object; storing self.items = list(items) creates a new outer list but still shares its elements. Sharing can be desirable when the object is a view or a coordinated collaborator. Copying can be safer when the instance should own an independent collection. Neither policy should be accidental. For example, a shopping cart might copy a supplied collection of immutable item identifiers so later changes to the caller's list do not alter the cart. Document whether returned collections expose live state too: copying on input does not prevent callers from mutating a list that a public attribute later reveals.
24. Why should a constructor avoid a mutable default argument?
Default argument values are evaluated when the function definition executes, not separately for each call. A constructor parameter such as items=[] therefore reuses one list across calls that omit the argument. Use a sentinel such as None when omission should create fresh state, while defining how explicitly supplied values are handled.
This version also copies the outer collection supplied by a caller. Do not use items or [] automatically: truth-value fallback treats an explicitly supplied empty collection like omission and can obscure the intended ownership policy.
25. How do __repr__ and __str__ differ?
__repr__ supplies an information-rich representation useful for debugging, while __str__ supplies a readable representation intended for people. repr(obj) requests the former; str(obj) and ordinary printing request the latter. If __str__ is not provided, the inherited behavior can use __repr__. A useful representation might show Point(x=2, y=3), while a user-facing string might be (2, 3). A representation that could recreate the object is desirable when practical, but it is not a universal requirement. Both methods must return strings. Avoid exposing passwords, tokens, or excessive personal data in either representation, because debugging output often reaches logs that have broader access and longer retention than application state.
26. How do you make a custom object work with len()?
Define __len__ on the class and return a nonnegative integer representing its size. The meaning should be clear for the abstraction: a playlist's length might be the number of tracks, not its duration in seconds. Callers then use len(playlist) instead of reaching into an internal collection. A negative result is invalid, and Python does not accept an arbitrary floating-point size. Special-method lookup is generally performed on the type, so assigning __len__ only to one ordinary instance is not the normal way to support the protocol. Also consider truth testing: if a class lacks __bool__, its length can determine whether the object is treated as false or true.
27. How is an object's truth value determined?
Python first uses __bool__ when the type provides it; otherwise, it can use __len__, with zero meaning false. If neither is provided, an ordinary object is truthy. __bool__ must return an actual Boolean. This lets a container-like object naturally be false when empty, but surprising definitions can make APIs difficult to read. For example, an account with a zero balance may still be a perfectly valid account, so defining falsehood by balance might confuse absence checks. When testing whether an optional object exists, use obj is not None if that is the intended distinction. A valid, present object can be false because of its content or custom truth-value behavior.
28. What makes an instance callable?
An instance becomes callable when its class implements __call__. Calling the instance then invokes that method, which can read or update the instance's state. This is useful for configurable operations and stateful callbacks that would otherwise require a closure plus additional management.
callable(obj) indicates that an object appears callable, not that every possible call will succeed. Arguments can still be invalid or the implementation can raise an exception. Use a regular function when there is no useful state or configuration to associate with the callable object.
29. What is the difference between an iterable and an iterator?
An iterable can supply an iterator, usually through __iter__; an iterator provides successive values through __next__ and returns itself from __iter__. Exhaustion is signaled by StopIteration. A collection can produce a fresh iterator for each traversal, allowing independent loops. An iterator usually represents one traversal in progress, so consuming it changes what the next call will return. For a custom collection, a beginner-friendly implementation can return iter(self._items) from __iter__ instead of manually implementing iterator state on the collection itself. This keeps nested loops independent. Do not assume that every iterable can be restarted or indexed: generators are iterable iterators and are normally consumed only once.
30. What does the context-manager protocol provide for objects?
The context-manager protocol organizes setup and cleanup around a with block. After the context expression is evaluated, __enter__ runs and its return value becomes the optional as target; __exit__ handles normal completion or an exception from the block. It can release a resource even when the block fails. For example, a file object manages closing through this protocol. If __exit__ returns a truthy value after an exception, that exception is suppressed, so cleanup methods should not accidentally return truthy values. If __enter__ itself raises, that manager's __exit__ is not called. Use context management for deterministic resource handling instead of relying on when garbage collection happens to destroy an object.
31. How do you define a custom exception class?
Create a subclass of Exception or an appropriate existing exception type. A dedicated exception communicates a failure category that callers can catch without treating every programming error as the same operational problem. For example, class InsufficientStockError(Exception): pass can identify an inventory rule failure. Inherit from Exception, not directly from BaseException, for ordinary application errors; the latter also includes control-flow exceptions such as KeyboardInterrupt. A simple exception class often needs no custom methods because the base already stores arguments and supplies a string representation. Add structured attributes only when callers need them, and avoid filling error messages with sensitive data. Raise the exception at the point where the object's operation cannot honor its documented contract.
32. What does the dataclass decorator generate?
@dataclass uses annotated fields to generate methods such as an initializer, representation, and equality comparison according to its options. It reduces repetitive code for classes whose main role is holding structured data. It does not make all behavior disappear: domain operations, validation, ownership, and compatibility still need design.
The generated equality normally compares field values between instances of the identical type. Annotation syntax does not enforce those types at runtime. A dataclass is an ordinary class with generated methods, not a replacement for understanding Python's object model.
33. Why do dataclasses use default_factory for mutable fields?
A default factory is called when an instance needs a default field value, so each such instance can receive a fresh mutable object. Use field(default_factory=list) rather than trying to share a list default unintentionally. Python 3.12 dataclasses reject unhashable field defaults as an approximation for detecting problematic mutable defaults, but that check is not a general proof of immutability.
The factory is supplied as a callable, not called while defining the field. An explicitly supplied mutable argument is still passed into the generated initializer; a default factory does not automatically copy values that callers provide.
34. Do type annotations enforce an object's attribute types at runtime?
No. Ordinary Python annotations describe intended types for readers and tooling, but the interpreter does not generally reject incompatible assignments because an annotation exists. For example, an attribute annotated as int can still receive a string unless code or a separate validation mechanism prevents it. Static type checkers can catch many mistakes before execution when the code is analyzed, but they do not establish every business invariant. A positive account balance needs a value rule, not merely a numeric annotation. Use annotations to communicate the interface, runtime checks at appropriate trust boundaries, and tests for behavior. Do not duplicate every type check throughout internal code without a clear reason or mistake an annotated dataclass for automatic input validation.
35. What role does the built-in object class play?
object is the ultimate base class of Python's ordinary class hierarchy. A definition such as class Customer: implicitly derives from it, so explicitly writing class Customer(object): is normally unnecessary in modern Python. Its inherited behavior supplies basic facilities such as default identity-based equality and a general representation, unless a class changes them. A plain instance of object does not support arbitrary new attributes, so object() is not a drop-in replacement for an instance of an ordinary user-defined class with an instance dictionary. The important interview distinction is between sharing a universal base and sharing a useful application interface: deriving from object alone does not promise methods such as save, send, or close.
36. Does del immediately destroy an object?
No. del removes a binding or requests deletion of an attribute or item; it does not universally mean immediate object destruction. If another reference exists, the object remains accessible through that reference. For example, after second = first, deleting the name first does not invalidate second. CPython often reclaims an object promptly when its reference count reaches zero, but cycles and implementation differences mean code should not depend on that timing. Similarly, del obj.field removes that attribute through the applicable deletion behavior, not the entire object. Use explicit cleanup or context managers for files, locks, and other resources whose release time matters, rather than treating del as a deterministic resource-management API.
37. Can you add attributes to an instance after construction?
Usually yes for ordinary user-defined instances with an instance dictionary: an assignment such as record.note = "checked" can add an attribute after construction. That flexibility is useful for some dynamic integrations, but it can also hide misspellings and make required state unclear. Not every object supports arbitrary attributes; built-in types and classes using restrictive __slots__, for example, may reject them. Custom attribute behavior can also change what an assignment does. Prefer initializing the class's expected state in __init__ and keeping a documented interface. If user-defined extension data is a real requirement, an explicit metadata mapping may be easier to validate and inspect than an unlimited collection of newly invented attribute names.
38. Where should a class validate the state established by its constructor?
Validate inputs before the constructor exposes an instance that violates the class's invariant. For example, a reservation can reject an end time earlier than its start time with ValueError, rather than storing impossible state and hoping every later method notices. Validate relationships as well as individual values: two acceptable timestamps can form an unacceptable interval. Do not silently substitute a default for an invalid explicit input unless that behavior is part of the contract. If later methods or setters can change the same state, they must preserve the invariant too. Constructor validation is only the initial boundary, not permanent protection when mutable attributes remain exposed or callers can mutate shared objects stored inside the instance.
39. When is returning self from a method appropriate?
Returning self can support fluent method chaining when each call intentionally modifies and returns the same object. For example, a builder might implement set_title(...).add_section(...). The interface must make mutation clear, because chaining can otherwise look as though each call creates an independent value. Python's built-in mutable collections often return None from mutating methods, such as list.append, which discourages that ambiguity. Neither convention is universal; consistency and caller expectations matter more than clever syntax. If an operation returns a new immutable value, return that value instead of self. In tests, check both the resulting state and whether identity is preserved when identity is part of the documented API.
40. When is a function or simple data structure better than a custom class?
A function is often better when an operation has no persistent state or meaningful lifecycle, and a simple tuple, mapping, or existing library type may be enough for uncomplicated data. A class earns its complexity when it clarifies ownership, enforces invariants, represents a stable domain concept, or provides behavior over state. For example, wrapping a single stateless formatting function in a class with one method usually adds ceremony without improving the interface. Conversely, an order with controlled state transitions can benefit from an object that keeps those rules together. Evaluate how callers will use and test the abstraction. Object-oriented design is not measured by class count; a smaller, clearer public API is often the stronger result.
Python OOP Interview Questions and Answers for Intermediate
The intermediate answers emphasize lookup order, behavioral contracts, and explicit ownership. Examples illustrate language behavior without suggesting that a convenience feature replaces validation or testing.
41. When should a class implement __new__ instead of relying only on __init__?
__new__ creates or selects the instance, whereas __init__ initializes an instance already returned by __new__. Override __new__ when constructing an immutable subclass or controlling which object is returned, not merely to assign ordinary attributes. If it returns an instance of the requested class, initialization normally follows; returning an unrelated object skips that class's initializer. Immutable values must be established during creation because assigning attributes later does not change their underlying value. Keep signatures compatible and delegate allocation to the appropriate base. Instance caching in __new__ also needs care because a reused instance can be initialized again. This example accepts integer-convertible input and validates the converted value, so a positive fraction that truncates to zero is rejected.
42. How does cooperative super() differ from calling a parent class explicitly?
super() delegates to the next implementation in the method resolution order, not necessarily to the class written immediately above the current class in a diagram. This enables multiple classes to cooperate without hard-coding every participant. Each implementation should call super() consistently, accept the arguments needed by the chain, and consume or forward them under a documented convention. A direct call such as Base.save(self) bypasses this ordering and can skip a mixin or invoke a shared ancestor twice. Cooperative initialization often uses keyword arguments, but forwarding arbitrary leftovers to object.__init__ is an error. Design a clear stopping point rather than assuming every superclass accepts every argument.
43. How does Python resolve a diamond-shaped inheritance hierarchy?
Python uses the C3 method resolution order to create a consistent linear ordering that respects local base order and preserves parent ordering. A shared ancestor appears once in that linearization. Method lookup and cooperative super() follow this order; they do not recursively traverse each parent independently. Inspect Class.__mro__ when diagnosing an unexpected method selection. Some base combinations cannot satisfy the ordering constraints and are rejected when the class is created. The example shows that B precedes C, but C still precedes their shared ancestor A; replacing cooperative calls with explicit ancestor calls would undermine that arrangement.
44. How can composition provide delegation without exposing every implementation detail?
Composition stores a collaborator and forwards selected operations to it. The containing class should expose a deliberate interface instead of automatically making every collaborator attribute public. For example, a report service can delegate data retrieval to a repository while retaining responsibility for formatting and access policy. Explicit forwarding methods make the supported contract visible to readers, type checkers, and tests. A blanket __getattr__ proxy is shorter but can leak methods, obscure exceptions, and fail to forward implicitly invoked special methods such as len. Decide whether callers receive copied results or shared mutable objects. Delegation is an ownership and interface decision, not simply a way to avoid inheritance syntax.
45. What does an abstract base class enforce at runtime?
A class using ABC and @abstractmethod cannot normally be instantiated while required abstract methods or properties remain unimplemented. This enforces the presence of concrete overrides, not their signatures, correctness, return types, or business semantics. An abstract method may provide a reusable implementation that subclasses invoke through super(). When combining decorators, abstractmethod should generally be the innermost decorator, as with an abstract class method. Registering a virtual subclass changes certain isinstance and issubclass results but does not insert methods into its hierarchy or enforce implementation of abstract methods. Use an ABC for a meaningful runtime extension contract and tests for the behavior that the contract promises.
46. When should you choose a Protocol rather than an abstract base class?
A Protocol expresses structural compatibility for static type checking: a class can satisfy it by providing the required members without inheriting from it. An ABC commonly expresses a nominal extension relationship and can supply implementation or prevent incomplete subclasses from being instantiated. A small repository protocol is useful when production and test implementations should remain independent of a shared base class. Neither option proves semantic correctness; two methods can share a signature while disagreeing about transactions or ownership. Choose based on how implementations are discovered and validated, not on a belief that protocols replace all inheritance. A protocol's type annotations also do not automatically validate arguments at runtime.
47. What are the limits of runtime-checkable protocols, especially in Python 3.12?
@runtime_checkable permits limited isinstance checks against a protocol, but those checks establish attribute presence rather than full method signatures or annotated types. An object with a wrongly typed method can therefore pass. Python 3.12 changed these checks to use inspect.getattr_static, so dynamically supplied attributes can produce different results than in earlier versions. The protocol's member set is also frozen for these checks when the protocol class is created; later monkey-patching does not redefine that required set. Runtime protocol checks can be slower than ordinary class checks. Use explicit validation or behavior tests where correctness matters, and remember that protocols with non-method members restrict issubclass checks.
48. What distinguishes a data descriptor from a non-data descriptor?
A descriptor implements __get__, __set__, or __delete__ to participate in attribute access. Defining __set__ or __delete__ makes it a data descriptor; a descriptor with only __get__ is non-data. For ordinary instance lookup, data descriptors take precedence over the instance dictionary, while instance attributes can shadow non-data descriptors. This explains why properties control writes and why a normal method can often be shadowed by an instance attribute. Descriptor methods are found on the descriptor's type. A reusable descriptor must also avoid accidentally storing one shared value for all owning instances: store per-instance state or explicitly document shared behavior. Class access may pass None as the instance to __get__.
49. How do __getattribute__ and __getattr__ differ?
__getattribute__ participates in ordinary attribute access for every name, while __getattr__ is a fallback invoked when normal lookup fails with AttributeError. Use __getattr__ for a narrow dynamic namespace or a useful missing-attribute behavior; override __getattribute__ only when every access needs mediation. An override should usually call object.__getattribute__(self, name) or super().__getattribute__(name) to avoid recursive access. Raising an unrelated exception does not trigger the normal missing-attribute fallback. Implicit special-method operations can bypass instance-level lookup, so these hooks are not a universal proxy mechanism. Preserve AttributeError for truly missing names, or tools such as hasattr and serializers may misunderstand the object.
50. How can a property preserve an invariant without duplicating validation?
Put the invariant in one setter or validation helper, and route construction and later public updates through that path. A property allows callers to keep attribute-style syntax while the class validates, computes, or controls access. Store the accepted value under a separate internal name to avoid recursively assigning the property. Do not imply that an underscore makes the storage inaccessible; it is a convention, and hostile code in the same process can bypass ordinary APIs. Validation should also complete before committing a new value, so a rejected update does not leave the object partially changed.
51. How do you override __setattr__ without causing infinite recursion?
An override must delegate actual storage rather than assigning through its own interception path again. For example, self.name = value inside __setattr__ calls the override recursively, while object.__setattr__(self, name, value) performs the normal assignment machinery, including applicable descriptor setters. Use a narrow property instead when only one field requires validation. A broad hook affects initialization, later updates, and framework-generated assignments, making invariants harder to reason about. It also does not intercept mutations inside a stored list or dictionary: obj.items.append(x) changes the list, not the attribute binding. Validate before delegating and preserve appropriate exceptions rather than silently ignoring unsupported assignments.
52. What does __slots__ save, and what does it not guarantee?
__slots__ declares storage descriptors and can avoid a per-instance dictionary, reducing memory for many small instances. It also limits arbitrary new attributes when no base class supplies __dict__. It does not make objects immutable, hide attributes, or guarantee that subclasses lack dictionaries: an unslotted subclass normally gains one. Weak references require suitable support, often a __weakref__ slot. Multiple inheritance with incompatible nonempty slot layouts can fail, and redeclaring inherited slot names is problematic. Measure actual memory and workload performance rather than assuming slots are always worthwhile. Slots are a layout choice that must remain compatible with inheritance, inspection, and serialization requirements, not an access-control mechanism.
53. Why should __eq__ return NotImplemented for unsupported operands?
NotImplemented tells Python that this operand combination is unsupported and allows the comparison protocol to try the other operand's implementation where appropriate. It is a return value, not the exception NotImplementedError. Returning False immediately can prevent a compatible other type from supplying a meaningful comparison. If neither operand supports equality, equality ultimately falls back to identity-based behavior. When implementing value equality, explicitly decide whether subclasses are comparable or whether equality requires the same concrete type; this decision affects symmetry and transitivity. Never treat NotImplemented as a normal truth value. A clear comparison contract is more important than making every vaguely similar object compare equal.
54. What contract must a custom __hash__ satisfy?
Objects that compare equal must have equal hashes, and an object's hash must remain stable while it is used in a dictionary or set. Different objects may share a hash, so hashing cannot replace equality checks. Base the hash on immutable components that also define equality, commonly hash((self.x, self.y)). Defining __eq__ without an appropriate __hash__ normally makes instances unhashable, protecting against an accidental identity hash paired with value equality. A class can be mutable in unrelated fields while remaining safely hashable if its equality and hash identity never change. Avoid persisting Python hash values as stable identifiers; some built-in hashes are randomized across interpreter processes.
55. What does functools.total_ordering generate, and what must you still design?
total_ordering fills in missing ordering operations from __eq__ and at least one of __lt__, __le__, __gt__, or __ge__. It reduces boilerplate but cannot repair an inconsistent comparison relation. Choose a stable ordering key, define how equal keys relate to equality, and return NotImplemented for unsupported operands. For example, ordering jobs only by priority while equality includes a unique ID may not provide the intuitive total ordering callers expect. Generated methods can add execution overhead and less direct stack traces compared with explicit implementations. Use tuple keys when appropriate, and test symmetry, transitivity, ties, and mixed-type comparisons rather than checking only that sorting a short list succeeds.
56. How do arithmetic and reflected special methods cooperate?
For an expression such as a + b, Python uses the operand types' numeric methods, including __add__ and the other operand's __radd__. Returning NotImplemented permits another supported route instead of immediately raising or producing a misleading result. A strict subclass on the right can receive reflected-method priority when it supplies a different implementation. If the operation remains unsupported, Python raises TypeError. Implement only operations meaningful for the domain: adding two money amounts can require matching currencies rather than silently converting either operand to a float. Reflected methods are especially useful when supporting a built-in left operand, but they should preserve the same validation and result-type contract as the ordinary method.
57. How is __iadd__ different from __add__ in an augmented assignment?
__iadd__ implements the in-place opportunity for +=, but it may mutate and return the original object or return a different object. If it is absent or returns NotImplemented, Python falls back to the ordinary addition protocol and rebinds the assignment target. Consequently, += does not guarantee mutation, and aliases can observe very different behavior for lists and immutable values. A custom mutable container should document whether in-place operations preserve identity and what happens if validation fails halfway through. For attribute or subscript targets, augmented assignment also writes the result back, which can trigger setters or fail after a mutable object has already changed.
58. How do __contains__, __iter__, and __getitem__ support container behavior?
__contains__ directly supports membership tests and can make them more efficient or semantically precise than iteration. Without it, Python can try iteration, and legacy indexed access through __getitem__ can also support iteration when it starts at zero and raises IndexError at the end. Implement __getitem__ deliberately for integer indexes, slices, or mapping keys rather than conflating those contracts. A mapping's iteration typically yields keys, so membership usually tests keys as well. For a custom range-like object, arithmetic membership can avoid traversing every element. Ensure missing keys and out-of-range indexes raise the expected exception types; otherwise callers may misinterpret failures or never terminate a fallback iteration.
59. Why should a reusable iterable usually return a fresh iterator?
An iterable represents something that can be traversed, while an iterator represents the state of one traversal. An iterator's __iter__ returns itself, and __next__ advances until StopIteration; a reusable collection generally creates independent iterators. Returning the collection itself while keeping one shared cursor makes nested loops interfere and later loops unexpectedly start at the end. A generator-producing __iter__ is often a compact solution. Decide separately whether iteration sees a snapshot, live mutations, or a fail-fast policy, because a fresh iterator alone does not provide isolation from concurrent changes.
60. How should a context manager handle exceptions in __exit__?
__exit__ receives the exception type, exception value, and traceback when the managed suite raises; all three are None on normal completion. Returning a truthy value suppresses the active exception, while a false value allows it to propagate. A resource manager should normally clean up and return false unless suppressing a particular exception is an explicit part of its contract. Cleanup errors can replace the original failure, so preserve useful diagnostic context and keep cleanup reliable. If __enter__ itself fails, that manager's __exit__ is not called; acquisition code must therefore clean up its own partial work. Context management makes lifetime explicit but does not inherently implement transaction rollback.
61. How do dataclasses generate equality and ordering methods?
With the default eq=True, a dataclass generates field-based equality that requires the compared instances to have the identical concrete type. With order=True, it generates ordering methods using the comparison-enabled fields in declaration order, again for identical types. A field marked compare=False does not participate. Ordering requires equality support; inconsistent decorator settings or conflicting explicitly defined ordering methods can raise errors at class creation. Do not enable ordering simply because a record has several sortable fields: declaration order then becomes part of the public comparison contract. Dataclass hash generation also depends on eq, frozen, and explicit hash choices; inspect those rules instead of assuming every generated record is hashable.
62. Why is a frozen dataclass not necessarily deeply immutable?
frozen=True blocks ordinary reassignment and deletion of its fields through generated methods, but it does not freeze objects referenced by those fields. A stored list remains mutable. Freezing is also not a security boundary: low-level assignment can bypass the generated restriction. Use immutable nested representations and controlled ownership if callers need a stable value object. Hashability is a separate concern: a generated hash fails if it includes an unhashable list, and a hashable but mutable nested object can violate collection invariants. Prefer tuples of genuinely stable values when equality and hashing must remain reliable.
63. How do dataclass inheritance and __post_init__ affect initialization?
Dataclass field processing incorporates dataclass base fields and then subclass fields, subject to rules such as non-default fields not following default fields in the generated initializer. A generated dataclass initializer does not automatically call a non-dataclass base's __init__; call it deliberately, often from __post_init__, when that base requires setup. The generated initializer calls __post_init__ when appropriate, but a custom initializer must arrange equivalent work itself. If a subclass overrides __post_init__, it should explicitly cooperate with a base hook when the base invariant matters. Treat inherited initialization as a contract: check argument ordering, keyword-only fields, required base state, and validation rather than assuming the decorator wires every constructor together.
64. What is the practical difference between shallow and deep copying an object?
A shallow copy creates a new outer object while reusing references to its contents. A deep copy recursively copies supported contents and uses a memo to preserve shared references and avoid infinite recursion through cycles. Deep copying does not necessarily duplicate everything: classes, functions, external resources, and custom copying implementations have special behavior. For an object holding a list of mutable records, a shallow copy isolates replacing the outer list but not editing one shared record. Choose the intended ownership boundary first. Deep copying a large graph can be expensive and can incorrectly duplicate identity-bearing domain objects; an explicit snapshot or reconstruction method may communicate the required semantics more clearly.
65. Why must a custom __deepcopy__ implementation use the memo dictionary?
The memo maps already encountered objects to their copies so recursive copying preserves cycles and intentional sharing. A custom implementation should return an existing memoized copy when appropriate, allocate its replacement, record it before recursively copying child objects, and pass the same memo to recursive deepcopy calls. Recording only afterward can recurse forever when a child points back to the parent. Decide which attributes are owned values and which represent shared services or resources that should not be duplicated. For example, copying a tree node may copy children while preserving a reference to an immutable configuration. A custom copy hook is a semantic contract, so test cycles, repeated references, and independent mutation afterward.
66. What should you explain before using pickle to serialize an object?
Pickle can preserve Python object graphs, including shared references, but loading a pickle can execute arbitrary code. Never unpickle untrusted or tampered data. It is not a safe interchange format merely because the payload looks binary or was stored internally. Compatibility also depends on importable class definitions, module paths, protocols, and the application's state schema; moving a class can break restoration. __getstate__ and __setstate__ can control saved state and migrations, but they do not make unsafe input safe. Exclude live connections, locks, and secrets where appropriate, then reconstruct resources explicitly. For long-lived or cross-language records, prefer a validated, versioned data format with a clear schema.
67. How should an object be converted to JSON without blindly exposing __dict__?
Create an explicit data-transfer representation containing only supported fields and JSON-compatible values. This avoids accidentally exposing private tokens, caches, collaborator objects, or implementation details from __dict__, and works for classes that use slots. Define how enums, datetimes, decimals, missing values, and identifiers are encoded rather than silently converting everything to strings. Reconstruction should validate types and domain invariants before creating a trusted object; valid JSON syntax is not valid application data. Add a schema version when persisted records must survive evolution. An explicit to_record method or separate serializer often keeps the domain class cleaner than making every internal attribute part of a permanent wire contract.
68. When is a weak reference useful, and what limitations matter?
A weak reference refers to an object without keeping it alive solely through that reference. This is useful for caches, observer registries, and indexes whose entries should not determine lifetime. Dereferencing can return None, so obtain the object once and check the result before use rather than making two separate weak-reference calls. Not every object supports weak references; slotted classes may need a __weakref__ slot or corresponding dataclass support. Weak references do not replace explicit resource cleanup, and callback timing should not be treated as a portability guarantee. Be careful with bound-method callbacks: retaining a bound method strongly can retain its instance, defeating the intended weak ownership.
69. What can a class decorator change, and when does it run?
A class decorator receives the class after its body has executed and the class object has been created. It can inspect or modify that class, register it, or return a replacement object that is then bound to the class name. This is useful for transformations that do not need to control namespace preparation or metaclass construction. Stacked decorators apply from the closest decorator outward, so order can affect generated methods and registration. Returning a replacement class can also affect identity, inheritance assumptions, and tools that refer to the original class. Keep import-time side effects explicit, and prefer a decorator over a metaclass when post-creation transformation is sufficient for the requirement.
70. How does __init_subclass__ support a plugin-style base class?
__init_subclass__ runs when a subclass is created, allowing a base to validate subclass declarations, configure metadata, or register implementations without requiring a custom metaclass. The hook is implicitly a class method and receives the new subclass as cls. In cooperative hierarchies, consume recognized keyword options and forward the rest through super().__init_subclass__. Registration occurs when the class definition executes, so it does not discover modules that were never imported. Avoid silently overwriting duplicate plugin names, and consider abstract intermediate subclasses that should not be registered as usable implementations. This hook helps enforce class-definition conventions, but runtime behavior and method signatures still need separate validation or tests.
71. What problem does a descriptor's __set_name__ method solve?
__set_name__(owner, name) tells a descriptor the class and attribute name under which it was installed during class creation. The descriptor can derive a private storage name, improve validation messages, or record metadata without requiring callers to repeat the attribute name. The callback is not automatically invoked when assigning a descriptor to an already created class, so that case requires explicit setup. Reusing the same descriptor instance under several names also needs a deliberate design because one stored name can overwrite another. The hook assists setup; attribute access still depends on the descriptor protocol.
72. What is a metaclass, and when is type enough?
A metaclass is the class of a class object; ordinary Python classes generally use type. Calling type(name, bases, namespace) can construct a class dynamically, while a custom metaclass can participate in namespace preparation, class creation, or class instantiation behavior. This is stronger machinery than most application customization requires. First consider a class decorator, __init_subclass__, a descriptor, or an ordinary factory because these tend to compose more easily and are easier to inspect. Multiple inheritance can produce metaclass conflicts when the bases' metaclasses are incompatible. Use a metaclass only when the class-creation contract itself requires it, and explain that requirement rather than presenting metaclasses as an automatic sign of advanced design.
73. How do generic class annotations help without enforcing runtime types?
A generic class relates the types of its operations, allowing a type checker to infer that storing a User yields a User later. The annotations do not ordinarily validate assignments, allocate a separate implementation for each type argument, or guarantee that runtime elements match the declared parameter. Python 3.12 introduced type-parameter syntax such as class Box[T]:; the older TypeVar and Generic form remains useful for compatible codebases. Select syntax according to supported interpreter and tooling versions. Runtime validation belongs in explicit checks or a validation layer. A useful generic abstraction preserves meaningful type relationships; adding unconstrained parameters that do not connect inputs and outputs offers little benefit.
74. How do ClassVar and InitVar change dataclass field handling?
ClassVar marks a class-level attribute that dataclasses exclude from normal instance fields. InitVar describes an initialization-only input passed to the generated initializer and, when present, __post_init__, but not stored as a dataclass field. Use InitVar for information needed to derive validated state, not as a promise that a sensitive value will disappear from every possible reference. Neither annotation independently enforces runtime types. Both are excluded by dataclasses.fields, which is why blindly treating every annotation as a persisted field is incorrect. An InitVar default can still exist as a class attribute, so distinguish absence from instance storage from ordinary attribute lookup.
75. How does cached_property affect instance state and concurrency?
cached_property computes a value on first normal access and stores it in the instance dictionary under the property name. Later lookup can use that stored value directly, and deleting the attribute permits recomputation. It therefore generally requires a mutable instance __dict__ and is not suitable for a slots-only instance without one. It can also change memory behavior, so measure large populations of cached objects. Since Python 3.12, cached_property no longer includes its former undocumented per-property lock; concurrent first access can run the getter more than once. Make duplicate computation harmless or provide appropriate synchronization, and define invalidation when the inputs used by the cached result change.
76. Why can introspecting an object accidentally execute code?
getattr performs ordinary attribute access, which can invoke properties, descriptors, __getattribute__, and __getattr__. hasattr also performs lookup and suppresses AttributeError, so it is not a side-effect-free dictionary membership check. A diagnostic tool that reads every advertised attribute can therefore trigger database calls, mutate caches, or raise unrelated errors. inspect.getattr_static can retrieve attributes without normal dynamic lookup, but may return descriptor objects instead of their computed values. vars and __dict__ expose only particular storage and do not fully describe slots or class attributes. Choose introspection tools according to whether you need raw definitions, stored state, or actual runtime behavior, and avoid blindly exposing sensitive attributes.
77. How does constructor-based dependency injection improve object testability?
Passing collaborators into a constructor makes an object's dependencies visible and lets tests supply controlled implementations. A billing service can accept a repository and clock instead of creating a database client and reading global time internally. This separates the behavior under test from infrastructure and makes failure cases reproducible. The collaborator interface should remain small and meaningful; injecting dozens of unrelated dependencies can reveal that the class has too many responsibilities. Decide who owns and closes each dependency so injected resources are not unexpectedly destroyed. Dependency injection does not require a framework: ordinary parameters and a composition function are often sufficient, with production wiring kept separate from the domain logic.
78. What do mock spec, spec_set, and autospec protect against?
A mock with spec restricts access to attributes derived from its specification, while spec_set also restricts setting unsupported attributes. Autospeccing additionally creates mocks with relevant call signatures, helping catch misspelled parameters or wrong argument counts. These features reduce drift between tests and real collaborators but do not verify business semantics, ordering guarantees, or actual integration behavior. Dynamic attributes created only in an initializer may need an instance specification or another explicit interface, and introspection during autospeccing can interact with descriptors. Patch the name where the tested code looks it up, not merely where it was originally defined. Combine focused mocks with small real or fake implementations and integration tests.
79. How should a class document ownership of mutable inputs and returned values?
State whether the class borrows a reference, takes exclusive ownership, copies the outer container, or creates a deeper snapshot. These choices determine whether caller mutations can change internal state and whether returned values expose invariants. Returning a new list protects the outer container but not mutable elements inside it; a read-only mapping view similarly need not be a historical snapshot. For a class storing immutable strings, copying the supplied list may be sufficient. For nested domain objects, copying everything may destroy intentional identity, so define the boundary rather than reflexively calling deepcopy.
80. How does an asynchronous context manager differ from a synchronous one?
An asynchronous context manager implements awaitable __aenter__ and __aexit__ methods and is used with async with. Acquisition and cleanup can therefore await asynchronous operations without substituting a blocking synchronous call. As with synchronous managers, a truthy exit result can suppress an exception, and failure during entry requires cleanup of partially acquired resources within that entry path. Cancellation makes cleanup design especially important: document whether release can be interrupted and use the asynchronous library's appropriate lifetime primitives. An async context manager does not automatically make shared object state safe, nor does it turn a synchronous resource into an asynchronous one. Test success, exceptions, failed acquisition, and cancellation separately.
Python OOP Interview Questions and Answers for Experienced
The experienced answers favor bounded designs and reproducible evidence over universal patterns. Distinguish what the language guarantees from implementation behavior, and connect each recommendation to the stated failure mode.
81. How would you diagnose an initializer skipped in a cooperative multiple-inheritance hierarchy?
Inspect the actual class's __mro__, then trace each initializer's arguments and delegation. In a cooperative hierarchy, super() calls the next implementation in the MRO, not necessarily the class's visually apparent parent. One class that directly invokes a named base, omits super(), or consumes another class's keyword can break the chain. Establish a shared argument convention: each participant consumes its own arguments, forwards the remainder, and calls super() once. Ensure no unexpected arguments reach object.__init__. Test the concrete combinations the framework supports rather than assuming every mixin permutation is valid. If the classes cannot share that contract, composition is safer than forcing unrelated constructors into one inheritance chain.
82. When should an integration subclass a framework type rather than wrap it?
Subclass when the framework explicitly defines extension hooks and the new object genuinely preserves the base type's contract. Wrap when you need an adapter, a narrower API, or independent lifecycle management. For example, a documented serializer hook may be a stable subclassing point, while inheriting a large client solely to reuse one request method creates unnecessary coupling. Compare constructor assumptions, protected hooks, exception behavior, resource ownership, and version compatibility. Composition does not automatically make an integration transparent: the wrapper must explicitly delegate required operations and may not satisfy runtime type checks. Choose the smallest supported extension surface, and write compatibility tests against the framework versions the application actually supports.
83. Why can assigning an instance attribute fail to override a class attribute?
A data descriptor on the class takes precedence over the instance dictionary during ordinary instance lookup. Properties are data descriptors, even when they have no setter; a read-only property rejects ordinary assignment rather than allowing a shadowing instance attribute. By contrast, a non-data descriptor can be shadowed by an instance attribute. When debugging, inspect the class dictionary and the descriptor's type, not only vars(instance). Do not bypass the descriptor merely to suppress an error: it may be enforcing an invariant.
Here the dictionary contains a value that normal lookup deliberately does not return.
84. Why does assigning a special method to an instance often not change an operator's behavior?
Implicit special-method operations generally look up the method on the type rather than through the instance's ordinary attribute lookup. Thus assigning obj.__len__ does not redefine what len(obj) means. This supports consistent interpreter-level behavior and prevents many accidental instance-level overrides. An explicit call such as obj.__len__() can still find the instance attribute, so the two expressions may differ. Implement the protocol on the class, or use an appropriate wrapper type, instead of assuming __getattr__ intercepts every operation.
This distinction is especially important when designing proxies and instrumentation.
85. How would you resolve a metaclass conflict without adding unnecessary complexity?
A class's metaclass must be compatible with the metaclasses of its bases; ordinarily it must be a subclass of each candidate metaclass. A conflict means Python cannot select such a metaclass automatically. First ask whether both base classes are necessary or whether one relationship can become composition. If combined metaclass behavior is genuinely required, a derived metaclass can sometimes combine them, but its creation hooks must cooperate and its ordering must be tested. Merely declaring class Combined(MetaA, MetaB) does not prove that their transformations are semantically compatible. Prefer __init_subclass__, class decorators, or descriptors for simpler customization, and document the supported inheritance combinations rather than exporting an unrestricted mixin promise.
86. Why must a custom metaclass preserve the class namespace's __classcell__ entry?
In CPython, when a class body needs an implicit __class__ reference, including for zero-argument super(), the compiler can place a __classcell__ entry in the namespace passed to the metaclass. A metaclass that delegates to type.__new__ must propagate that entry so Python can populate the cell correctly. Filtering it out while rebuilding the namespace can make class creation fail with a runtime error. This is one reason metaclasses should transform only the entries they actually own rather than aggressively sanitizing the namespace. Test methods using zero-argument super(), not just methods that read ordinary attributes. The namespace-cell mechanism is a documented CPython implementation detail, not a request to manually manufacture closure cells in ordinary application code.
87. What extra work is required when attaching a descriptor after class creation?
Automatic __set_name__ notification happens during class creation for descriptors present in the class namespace. Assigning a descriptor to the class later does not automatically repeat that notification. If the descriptor depends on its owner or attribute name, the installer must arrange the notification explicitly and account for replacing or reusing descriptors. Prefer defining descriptors in the class body when possible, because dynamic installation adds lifecycle and tooling complexity.
A reusable descriptor may need per-owner state rather than one mutable name field.
88. How would you make subclass-based plugin registration predictable?
Treat class definition and plugin discovery as separate steps. An __init_subclass__ hook can register a class when its module executes, but it cannot discover modules that were never imported. Establish an explicit discovery phase, deterministic identifiers, duplicate handling, and clear eligibility rules so abstract helper classes do not become accidental plugins. Avoid relying on incidental import order or silently replacing an existing registration. Tests should isolate registry state and exercise duplicate names, failed imports, and repeated discovery. For production systems, an explicit registry assembled at startup is often easier to reason about than a process-wide mutable class attribute. Registration also does not establish that a plugin is trusted or safe to execute.
89. Why can an ABC virtual subclass pass isinstance checks while missing required behavior?
Registering a virtual subclass with an abstract base class changes relevant issubclass and isinstance results, but it does not insert the ABC into that class's MRO or copy its implementations. It also does not enforce the ABC's abstract-method requirements on the registered class. Consequently, registration is a declaration of compatibility, not a substitute for implementing or testing the contract. A class can pass the check while lacking a method that a caller expects. Use registration only when the existing class truly satisfies the interface, and test behavior separately. When you control the implementation and want abstract-method enforcement at instantiation, ordinary inheritance from the ABC provides a different, stronger mechanism.
90. How would you introduce a Protocol into a mature object API without breaking clients?
Start by describing the smallest behavior existing consumers actually need, then type-check current implementations and adapters against that protocol. Structural typing can reduce dependence on one concrete base class without forcing external implementations to inherit from a new type. Pay attention to parameter names, mutability of attributes, return types, and variance rather than merely listing method names. Introduce the protocol at boundaries incrementally, preserving runtime behavior while improving static checks. If runtime validation is necessary, use explicit validation or contract tests; runtime_checkable protocols do not verify full signatures or semantics. Keep a compatibility suite of representative third-party implementations so a seemingly harmless annotation change does not silently tighten the supported contract.
91. Why is unsafe_hash=True dangerous for a mutable dataclass used as a dictionary key?
A dictionary key's hash must remain stable while it is stored, and equal objects must have equal hashes. unsafe_hash=True asks a dataclass to generate a hash; it does not freeze fields or prove that using the object as a key is safe. If a participating field changes, lookup can fail because the object now belongs in a different hash bucket. Nested mutable fields can also make the generated hash fail outright. Prefer a separate immutable identifier or a genuinely stable value object. Review the interaction of eq, frozen, field-level compare, and field-level hash; exclude fields only when doing so preserves the intended equality and hash contract, not simply to silence an exception.
92. How would you design an immutable value object that normalizes its input?
Normalize and validate at construction, store only the canonical representation, and base equality and hashing on that representation. For example, convert an iterable of coordinates to an immutable tuple rather than retaining a caller-owned list. A frozen dataclass can prevent ordinary field reassignment, but it does not recursively freeze referenced objects or provide a security boundary. Keep normalization deterministic and avoid unexpected external work in the constructor.
Runtime validation of element types is a separate decision; annotations alone do not perform it.
93. Can a frozen object safely contain a cache?
Yes, if the cache is observationally irrelevant to the object's value and cannot invalidate equality, hashing, or promised concurrency behavior. Frozen dataclasses prevent ordinary field reassignment; they do not make all reachable state immutable. A mutable cache can therefore exist inside one, but it should be excluded from value comparison and hashing and should not leak mutable results that alter the object's meaning. Decide how copying, serialization, and concurrent access handle that cache. A cached result is also only valid if its dependencies cannot change. In some designs an external cache keyed by a stable value is clearer. Document logical immutability explicitly rather than implying that frozen=True proves deep immutability or thread safety.
94. Why can multiple inheritance fail when several bases define nonempty __slots__?
Slots influence instance storage layout, not merely attribute naming. Multiple inheritance generally cannot combine independent nonempty layouts from multiple slotted bases, and Python can reject the class with an instance layout conflict. Design one storage-owning base and keep behavioral mixins layout-free, often using __slots__ = () where appropriate. Also inspect the full hierarchy: an unslotted base can provide an instance dictionary, while a subclass without its own slots can reintroduce one. Slots do not automatically make objects immutable or prevent all dynamic behavior. Test the intended hierarchy directly instead of assuming that individually valid slotted classes can be combined safely in arbitrary orders.
95. What lifetime pitfalls arise when replacing a normal cache with a weak-reference cache?
Weak references do not keep their referents alive, so a weak-value cache can lose entries as soon as no strong reference remains elsewhere. That is useful for avoiding ownership, but it changes cache semantics: a lookup can miss even without explicit eviction. Keep a strong local reference after a successful lookup, and do not depend on exact collection timing across Python implementations. Not every object supports weak references; slotted classes may need a __weakref__ slot unless support is inherited. Weak keys also require careful equality reasoning because weak-key mappings do not simply mean identity-only keys. Treat weak caches as optional reuse mechanisms, not as the authoritative store for application objects or resources.
96. Why should resource cleanup not depend on __del__ or immediate garbage collection?
Finalization timing is not a portable resource-management guarantee. CPython commonly uses reference counting alongside cyclic garbage collection, but other implementations can collect objects at different times, and cycles or lingering references can delay cleanup. Shutdown conditions and partially initialized objects make __del__ especially awkward; exceptions raised there are not a normal control-flow mechanism. Modern Python can collect many cycles involving finalizers, so the outdated claim that every such cycle is permanently uncollectable is also wrong. Use context managers or explicit close methods for files, locks, and connections, with idempotent cleanup where appropriate. A finalizer may provide a best-effort fallback, but correctness should not depend on when it runs.
97. How does ExitStack help an object manage a variable number of owned resources?
ExitStack collects cleanup callbacks and context managers dynamically, then unwinds them in reverse registration order. It is useful when the number of resources is determined at runtime or construction can fail partway through acquiring them. Register cleanup immediately after each acquisition so a later failure does not leak earlier resources. Ownership still needs to be explicit: borrowed resources should not be closed merely because the object used them.
For asynchronous resources, use the corresponding asynchronous lifecycle facilities rather than treating an async close operation as an ordinary synchronous callback.
98. How should an asynchronous context manager handle cancellation during cleanup?
Place cleanup in a reliable exit path and design it to tolerate partial initialization and repeated calls. Cancellation can arrive at an await, including one inside cleanup, so merely writing finally: await close() does not prove that cleanup completes under every cancellation pattern. Understand the surrounding task-management contract and keep cleanup bounded. Shielding can be appropriate for a specific cleanup operation, but it requires retaining and managing the underlying task and does not make the caller immune to cancellation. Avoid swallowing cancellation indiscriminately or turning shutdown into an unbounded wait. Test cancellation before acquisition finishes, during the managed operation, and during release, verifying both resource state and the exception observed by the caller.
99. Why does the GIL not make a mutable object's methods thread-safe?
Thread safety depends on preserving invariants across the whole operation, not on whether one interpreter executes Python bytecode at a time. A check followed by an update can interleave with another thread, and extension calls or blocking operations may release the GIL. Protect related state with synchronization or transfer ownership rather than assuming a particular bytecode sequence is atomic. Starting with Python 3.13, CPython also offers an optional free-threaded build, making GIL-based assumptions even less appropriate; this does not mean every CPython installation runs without a GIL. Test the supported runtime configurations and document which methods may be called concurrently. Built-in container protections do not automatically protect an application's multi-step business invariant.
100. How would you choose lock boundaries for an object that collaborates with other objects?
Protect the smallest coherent state transition that preserves the invariant, not necessarily the shortest individual assignment. Document which lock guards which fields, and establish a consistent acquisition order if operations need more than one lock. Avoid invoking arbitrary callbacks or performing slow external work while holding a lock where possible; these can introduce reentrancy, deadlocks, or excessive contention. A reentrant lock solves some same-thread reacquisition cases but does not fix inconsistent lock ordering across threads. Consider snapshotting state under a lock and doing independent work afterward, then validating before committing if needed. Test realistic interleavings and timeouts, and prefer simpler ownership boundaries when the locking protocol becomes harder to understand than the object itself.
101. How would you prevent concurrent construction of a shared service instance?
Prefer explicit startup construction and dependency injection when possible; they make ownership and lifetime clearer than a hidden singleton. If lazy shared construction is required, synchronize the entire transition from absent to fully initialized and published. A check outside the lock followed by construction inside an incomplete protocol can create duplicates or expose partially initialized state. Specify what happens after initialization fails, whether retries are allowed, and whether recursive access during construction is an error. Do not assume that a singleton is global across processes, subinterpreters, or separate class definitions after reload. Tests should cover concurrent first access and failure recovery, not just repeated access in one thread. Sharing one instance also does not make that instance's subsequent methods thread-safe.
102. How can an async method violate an object's invariant even on one event-loop thread?
An await can suspend the current task and let another task operate on the same object. If a method reads state, awaits, then writes based on the old value, another task may have changed that state in between. Keep the invariant-preserving portion synchronous where feasible, serialize access with an asyncio.Lock, or use explicit version checks and retry rules. A thread lock is not a substitute for an async coordination design, and holding one across an await can block the event-loop thread. Define cancellation behavior while waiting for a lock and after partial work. The absence of simultaneous machine-level execution does not eliminate logical races between independently scheduled tasks.
103. What should an object expose when it crosses a multiprocessing boundary?
Expose a serializable description of the work or state rather than assuming its live resources can move to another process. Locks, open connections, thread-bound objects, and local closures often require special handling or cannot be transferred meaningfully. Reconstruct process-local resources in the receiving process and define ownership explicitly. With spawn-style startup, importability of functions and classes and a guarded program entry point matter. Do not rely on one platform's default process start method: Python 3.14 changed the POSIX default to forkserver where supported, while Windows and macOS use spawn defaults. Choose and test an explicit supported context when behavior matters. Serialization compatibility and process isolation remain separate from thread-safety guarantees.
104. How would you evolve a persisted object format without tying it to a class's internal layout?
Persist an explicit schema with a version identifier, stable field names, and well-defined migrations rather than serializing the instance dictionary as the public format. Separate loading, validation, and construction so old data is normalized before entering the current domain model. Define policies for missing fields, unknown fields, invalid values, and forward incompatibility. Keep migrations deterministic and test them against representative historical fixtures, including failures. Renaming a Python class or changing a private attribute should not automatically invalidate years of stored data. Prefer simple data formats where practical, and do not treat a schema version as a security control. Long-lived persistence needs an independently maintained compatibility contract, not just a working round trip in the latest release.
105. What should deepcopy do for an object that owns a lock or external resource?
There is no universal correct copy. Decide whether copying is forbidden, creates a fresh independent resource, shares a deliberately borrowed resource, or produces a detached state-only object. A duplicated Python wrapper does not necessarily duplicate the underlying operating-system resource or ownership rights. Implement the policy explicitly, potentially by raising TypeError from copying hooks when copying cannot preserve invariants. If implementing __deepcopy__, use the memo appropriately to preserve aliases and break recursive copying, but do not blindly deep-copy every field. Explain how the copied object's lifecycle differs from the original. Tests should verify independence, intentional sharing, and cleanup behavior, especially that closing one object cannot unexpectedly invalidate another supposedly independent copy.
106. Why is a general-purpose object proxy difficult to make fully transparent?
Forwarding unknown attributes with __getattr__ handles only part of Python's object protocol. Operators and other implicit special-method operations generally consult the proxy's type, so length, iteration, arithmetic, context management, and similar behaviors may require explicit implementations. Identity, exact type checks, descriptor behavior, weak references, and serialization can expose additional differences. A proxy can also accidentally return bound methods of the target, allowing calls to bypass the proxy's instrumentation. Define which interface is supported instead of promising universal transparency. For many applications, a narrow adapter with an explicit contract is easier to test and maintain than a catch-all proxy. Include operator and lifecycle behavior in tests, not just ordinary method calls.
107. What tooling problems can a dynamic __getattr__ API create?
An API that invents attributes on demand can obscure available operations from static type checkers, autocomplete, documentation tools, and ordinary inspection. It can also make typos look valid or cause hasattr to perform unintended work. Raise AttributeError for unsupported names, keep lookup side effects minimal, and expose discoverable behavior through an explicit interface where practical. __dir__ can improve interactive discovery but is not a substitute for static declarations or type stubs. Be careful when proxying exceptions: an internal AttributeError should not casually be reinterpreted as a missing public attribute. Dynamic access is useful for genuinely dynamic schemas, but a stable application API usually benefits from explicit properties or methods and predictable failure modes.
108. How would you construct domain objects from untrusted serialized input safely?
Parse a data-only format with explicit size and depth limits appropriate to the application, validate the schema, and map approved fields into approved constructors. Do not deserialize untrusted pickle data: loading it can execute arbitrary code, and checking the resulting object's type happens too late. Avoid accepting an arbitrary class or module name from input for dynamic import and construction. Validate business constraints as well as superficial types, and keep constructors free of surprising external side effects. Data-only formats are not automatically harmless; oversized collections, deeply nested structures, and expensive validation can still exhaust resources. Test malformed inputs and reject unsupported schema versions with clear errors rather than falling back to unsafe object reconstruction.
109. What compatibility risks arise when changing a class that users are expected to subclass?
A subclassable class exposes more than its public methods: constructor sequencing, overridable hooks, argument conventions, and calls between methods can become de facto extension contracts. Adding a call to an overridable method during initialization may invoke subclass code before its state exists. Renaming a protected hook or changing how often it is called can break clients without changing the public signature. Document supported extension points and avoid promising every internal method as a hook. Maintain representative subclass compatibility tests and use deprecation paths for intentional contract changes. Where arbitrary subclassing creates an unmanageable surface, offer composition-based collaborators or explicitly restricted extension hooks rather than relying on undocumented assumptions.
110. How can changing dataclass fields break structural pattern-matching clients?
Dataclasses normally generate __match_args__ from non-keyword-only initializer parameters, enabling positional class patterns. Reordering or inserting such fields can therefore change what a pattern like Point(x, y) extracts, even if ordinary attribute access remains unchanged. Keyword patterns such as Point(x=x, y=y) make the dependency clearer. For evolving public APIs, consider keyword-only fields or an explicitly controlled matching contract rather than accidentally exposing field order.
Review constructor compatibility, equality, and serialization separately; protecting pattern matching alone does not make a field change backward compatible.
111. Why can an immutable built-in subclass lose its domain invariant after an operation?
Validating construction does not change the result contract of every inherited operation. An int subclass can reject nonpositive inputs in __new__, yet inherited arithmetic can return an ordinary int, including a negative value. Decide whether that is intentional, whether selected operations should construct and validate a new domain value, or whether a composed value object would expose a safer, smaller API. Overriding one operator does not cover reflected operations, conversions, or all other inherited methods.
Test operations through the interface callers actually use. Constructor validation alone is not evidence that a subclass is closed under its inherited operations.
112. How does Self improve annotations for fluent APIs and alternate constructors?
typing.Self expresses that a returned object has the receiver's concrete type, helping inherited fluent methods retain useful subtype information. A method that returns self can use it naturally, and a classmethod returning cls(...) may also use it when the construction contract supports subclasses. It is not runtime enforcement and should not annotate a method that always constructs a particular base-class instance. For alternate constructors, consider whether subclasses can have incompatible initializer requirements; the annotation does not repair that design. Self is available in the standard library from Python 3.11, so it fits a 3.12 baseline. Test both static expectations and runtime behavior with a meaningful subclass rather than only the base class.
113. How would you assess whether a subclass preserves a base class's behavioral contract?
Test it through the base interface using the same valid inputs and expectations as other implementations. A subclass should not unexpectedly demand stricter input conditions, weaken promised results, or introduce incompatible exception and lifecycle behavior. For example, a read-only implementation should not inherit a mutable collection API merely to reject every mutation if clients are entitled to perform those operations. That is a concrete contract mismatch, not just a naming debate. Separate interfaces or composition may represent the relationship more accurately. Include invariants across method sequences, not only isolated outputs, and document intentional differences. Python allowing inheritance syntactically does not establish that substituting the subclass is safe for existing callers.
114. How would you make a service object's varying behavior testable without a large subclass hierarchy?
Inject the varying collaborator through a small explicit interface, leaving the service responsible for orchestration and its own invariants. For example, a pricing service can accept a discount policy rather than define a subclass for every customer and promotion combination. Tests can supply a simple deterministic implementation and verify resulting business behavior, while separate contract tests exercise real policies. Avoid introducing an interface for every line of code; the abstraction should represent a meaningful source of variation or an external dependency. Prefer assertions about observable outcomes over fragile expectations about every internal call. The design succeeds when adding a new policy does not require changing unrelated service logic or bypassing its validation.
115. How would you investigate an object graph that appears to retain memory unexpectedly?
First distinguish a real retention trend from allocator behavior and a high-water memory measurement. Reproduce the workload, measure live allocations with suitable tooling such as tracemalloc, and identify long-lived owners: registries, callbacks, caches, closures, and task references are common suspects. A reference cycle alone is not proof of a leak because Python's cyclic collector can reclaim many unreachable cycles. Inspect relevant references carefully; debugging tools can themselves retain objects, and collection details vary by implementation. Release references at the correct ownership boundary rather than sprinkling manual collection calls through business methods. Verify the change under repeated workloads and confirm that it has not shortened the lifetime of objects still legitimately in use.
116. How would you decide whether __slots__ is a worthwhile optimization?
Measure representative object counts, attribute access patterns, and end-to-end memory use before changing the model. Slots can reduce per-instance storage by avoiding a normal instance dictionary in suitable hierarchies, but the benefit depends on the runtime and class shape. sys.getsizeof alone does not account for an entire referenced object graph, so it can produce misleading comparisons. Include construction cost, compatibility with weak references, serialization, debugging, inheritance, and libraries that expect __dict__. Slots are not a universal speed improvement, and a subclass or base may reintroduce dictionary storage. Prefer the simpler design unless the measured benefit is meaningful for the actual workload and the resulting API restrictions are acceptable.
117. When should request-specific object state use ContextVar instead of threading.local?
Use context variables when state should follow an execution context, particularly across asynchronous tasks that share one thread. Thread-local storage isolates threads, not independent async tasks on the same event loop. A ContextVar can provide task-appropriate context propagation, but it does not recursively copy a mutable value or make a shared object safe. Set and reset values at an explicit request boundary so state does not leak into later work.
Pass dependencies explicitly when practical; context-local state should not become an invisible replacement for every parameter.
118. How would you test an object's lifecycle without relying on garbage-collection timing?
Exercise explicit acquisition and release paths and assert the externally meaningful state transitions. Use small fake resources that record close calls, controlled failures during initialization, and context-manager tests for both success and exceptions. Verify the documented idempotence policy and whether borrowed resources remain open. For async objects, include cancellation and partial acquisition cases with deterministic coordination rather than arbitrary sleeps. Avoid assertions that depend on a destructor running immediately after del, since del removes a reference rather than guaranteeing finalization. Where a finalizer is intentionally a fallback, test its narrow behavior separately from the core lifecycle contract. A deterministic cleanup API makes both production recovery and testing more reliable.
119. What should contract tests for a custom Python protocol cover beyond method presence?
Test behavioral laws, error cases, and state transitions shared by every implementation. For a reusable iterable, verify independent iterations; for a value object, verify equality symmetry and equal-hash consistency; for a context manager, verify release after both normal and exceptional exits. Make the shared suite accept an implementation factory so it does not accidentally depend on one concrete class's private fields. Include edge cases and documented concurrency or ownership guarantees where relevant. Static typing can check structural compatibility, while runtime checks may inspect attribute availability, but neither proves these semantics. Keep the contract focused enough that legitimate implementations can satisfy it without imitating an arbitrary reference implementation's internal design.
120. What would you review before approving a stateful Python service object for production?
Start with the object's responsibility, invariants, and ownership boundary: what state it owns, who may mutate it, and how its lifetime ends. Review construction failures, exception behavior, resource cleanup, concurrency assumptions, serialization needs, and compatibility promises. Check whether class-level state unintentionally crosses instances or requests and whether collaborators can be replaced in tests without altering core behavior. Require evidence from representative tests and measurements rather than choosing inheritance, slots, or a metaclass because they appear sophisticated. Clarify operational visibility without exposing sensitive state in representations or logs. A strong design makes invalid transitions difficult, failure recovery explicit, and the supported interface smaller than the collection of implementation details behind it.
Python OOP Documentation and Further Reading
These primary references cover the Python 3.12 language and library baseline. The free-threading guide is explicitly from Python 3.14 and describes the optional CPython builds introduced in 3.13. Consult the documentation for the interpreter and library version used in an interview or production system.
Similar Posts
