Python Senior
What happens at the bytecode level when a Python script is executed?
Select the correct answer
Source is compiled to bytecode that the PVM executes instruction by instruction
Source is translated to assembly that the CPU executes register by register
Source is parsed into a syntax tree that the OS executes node by node
Source is converted to C code that a compiler executes function by function
How does the 'Specializing Adaptive Interpreter' introduced in 3.11/3.12 improve performance?
Select the correct answer
It caches function return values so repeated calls skip executing the bytecode
It compiles the entire module into native machine code ahead of program startup
It rewrites hot bytecodes into type-specialized versions based on observed runtime types
It removes the GIL so specialized threads can run bytecodes fully in parallel
What is string and integer interning in Python, and why does a=256;b=256;a is b return True while a=257;b=257;a is b might return False?
Select the correct answer
Python interns all integers below 1000 by default, so larger numbers like 257 get fresh objects each assignment.
Integers are compared by value with is, so small numbers match but large ones land in distinct slots.
CPython pre-caches integers from -5 to 256, so equal values share one object; 257 lies outside that cached range.
The garbage collector merges duplicate integers up to 256, then stops deduplicating beyond that size threshold.
Beyond mutability, what are the architectural differences between a list and a tuple, and why is a tuple slightly more memory-efficient?
Select the correct answer
Tuples omit pointers and keep values directly, while lists keep both pointers and a separate index cache.
Tuples share a single global instance per length, while lists always allocate a brand-new buffer on creation.
Tuples store elements inline as raw bytes, while lists box every element, doubling their per-item memory cost.
Tuples are fixed-size and allocated once without spare capacity, while lists over-allocate room for future growth.
When would you use __slots__ in a class definition, and what are the trade-offs regarding memory and flexibility?
Select the correct answer
It makes all instance attributes immutable, trading flexibility for thread-safety guarantees.
It automatically generates getters and setters, reducing boilerplate at a small speed cost.
It caches attribute lookups for faster access but increases per-instance memory consumption.
It restricts attributes to a fixed set, saving memory but preventing dynamic attribute addition.
Explain the difference between __init__ and __new__. When would you specifically need to override __new__?
Select the correct answer
__new__ runs after __init__ and is used mainly to validate the constructed object.
__new__ creates and returns the instance; override it to control immutable type creation.
__init__ returns the new instance; override it when subclassing immutable built-in types.
__init__ creates the instance while __new__ only assigns attributes after construction.
Explain the difference between __getattr__ and __getattribute__.
Select the correct answer
__getattribute__ runs on every attribute access, while __getattr__ runs only when lookup fails
Both run only when an attribute is missing, but __getattribute__ is called first
Both run on every access, but __getattr__ takes priority over __getattribute__ each time
__getattr__ runs on every attribute access, while __getattribute__ runs only when lookup fails
How does Python handle multiple inheritance? Explain the C3 Linearization algorithm and how super() determines which class to call next.
Select the correct answer
The MRO is computed by C3 linearization, and super() calls the next class in that order
The MRO is computed left-to-right depth-first, and super() always calls the direct parent
The MRO merges bases by inheritance depth, and super() calls the most derived base class
Each base is searched fully before the next, and super() calls the first listed base class
Explain how the Descriptor protocol works. How do @property, @classmethod, and @staticmethod use descriptors under the hood?
Select the correct answer
Descriptors define __init__ hooks, and property stores values directly in the instance __dict__
Descriptors define __getattr__/__setattr__, and property and classmethod wrap them as closures
Descriptors define __get__/__set__/__delete__, and property, classmethod, staticmethod are descriptors
Descriptors define __call__ only, and staticmethod and classmethod bind via metaclasses
What is a metaclass, and how does it differ from a standard class? When would you use a metaclass instead of a class decorator?
Select the correct answer
A metaclass is a special instance of a class used to cache attributes across many instances
A metaclass is a class decorator written in C that runs after the class is fully built
A metaclass is a base class shared by all classes and controls only instance creation logic
A metaclass is the class of a class and controls class creation, affecting all subclasses too
When would you use __init_subclass__ instead of a metaclass?
Select the correct answer
When you need a lightweight hook to customize subclasses without writing a full metaclass.
When you need to combine multiple metaclasses that would otherwise cause layout conflicts.
When you want to modify the class namespace before the class body has been executed.
When you must alter instance creation behavior each time a new object is instantiated.
Explain the difference between Nominal typing and Structural typing (Protocols) in Python.
Select the correct answer
Nominal matches by method signatures; structural matches by explicit subclass declarations.
Nominal checks types at runtime; structural checks types only during static analysis runs.
Nominal allows duck typing freely; structural forbids any implicit interface matching at all.
Nominal matches by declared class or inheritance; structural matches by the required methods.
What is the difference between typing.Any and object in a type hint?
Select the correct answer
Any restricts values to builtins; object accepts only user classes and their subclasses.
Any disables checking and is compatible both ways; object accepts all but allows few operations.
Any accepts all values but blocks operations; object disables checking and allows any operation.
Any and object behave identically except Any is faster during static analysis runs.
Explain the new Type Parameter Syntax introduced in PEP 695 (Python 3.12).
Select the correct answer
Adds a keyword that imports generics automatically without referencing the typing module at all.
Lets you enforce runtime generics so type arguments are checked whenever functions get called.
Lets you declare generic type parameters inline, like def f[T](x: T) -> T, without TypeVar.
Replaces all existing type hints with a new bracket syntax required for every annotated function.
How does Python's memory management work? Explain the difference between reference counting and the cyclic garbage collector, and how Python handles two objects that reference each other.
Select the correct answer
Reference counting frees objects on a fixed timer, while a cyclic collector reclaims objects whose count never changes at all.
Reference counting frees objects when their count hits zero, while a cyclic collector reclaims objects that reference each other.
Reference counting tracks the call stack depth, while a cyclic collector compacts the heap to remove memory fragmentation gaps.
Reference counting frees objects that reference each other, while a cyclic collector frees objects when their count hits zero.
What is the difference between the stack and the heap in the context of Python's memory management, and who manages the private heap?
Select the correct answer
The stack stores frames and references; the heap stores the objects, managed by Python's private memory manager.
The stack stores global variables; the heap stores local variables, managed jointly by the interpreter and CPU cache.
The stack stores large objects; the heap stores small immutable ones, managed by the cyclic garbage collector internally.
The stack stores all the objects; the heap stores only references, both managed directly by the operating system kernel.
What are 'Immortal Objects' (PEP 683) and how do they help with multi-processing performance?
Select the correct answer
Objects whose reference count is never changed, so their memory stays unchanged and shareable across forked processes.
Objects cached in a shared pool, so multiple processes can mutate them at the same time without acquiring any lock.
Objects that are never garbage collected, so each forked process must keep its own private duplicate copy in memory.
Objects pinned to a fixed memory address, so the operating system can map them faster between separate process spaces.
Why does Python not immediately release memory back to the operating system when an object is deleted?
Select the correct answer
It defers freeing so other running processes can read the old values directly from the abandoned memory blocks.
It keeps freed blocks in internal pools to reuse them, avoiding the overhead of repeated system calls to the OS.
It waits for the cyclic garbage collector to run on a timer before any freed memory can be returned to the OS.
It must keep every object alive until the interpreter exits, since reference counts can never truly reach zero.
How does generational garbage collection work in Python, and why does the collector move objects between 'Generation 0', '1', and '2'?
Select the correct answer
New objects start in generation 0; survivors are promoted to older generations, which are scanned less frequently.
Large objects start in generation 2; as they shrink they move to younger generations, which are scanned more often.
Objects move between generations by size, so generation 0 holds bytes, generation 1 kilobytes, and 2 megabytes.
Objects are sorted by reference count into generations, and only generation 0 with zero references is ever collected.
What is the difference between a Coroutine and a standard Generator?
Select the correct answer
A generator receives values from its caller, while a coroutine only produces values and cannot be paused
A generator produces values to its caller, while a coroutine can also receive values and pause execution
A coroutine and a generator are identical, except coroutines must always be defined using the async keyword
A coroutine runs on multiple threads in parallel, while a generator runs strictly on a single thread only
What do coroutines and generators have in common, and how did generators evolve into async/await?
Select the correct answer
Neither can be paused mid-execution; async/await added pausing as an entirely separate language feature
Both return single values eagerly; async/await emerged from list comprehensions optimized for concurrency
Both can suspend and resume execution; async/await grew from generator coroutines built on yield from
Both run code in parallel threads; async/await replaced generators with new OS-level threading primitives
What do the send(), throw(), and close() methods do on a generator object?
Select the correct answer
send creates a new generator copy, throw logs an exception silently, close flushes its buffered values
send passes a value into the generator, throw raises an exception inside it, close terminates it
send returns the next value out, throw catches exceptions inside it, close pauses it temporarily
send restarts the generator from start, throw skips the current value, close caches its final result
What are positional-only and keyword-only parameters, and how do the / and * markers in a function signature work?
Select the correct answer
Parameters between / and * must always be passed strictly as keywords
Parameters before * are positional-only and those after / are keyword-only
Parameters before / are positional-only and those after * are keyword-only
Parameters after / are positional-only and those before * are keyword-only
How do you implement an asynchronous context manager (async with), and how do __aenter__ and __aexit__ differ from their synchronous counterparts?
Select the correct answer
Define __enter__ and __exit__ with async def so the event loop schedules them automatically
Define __aenter__ and __aexit__ as coroutines that can be awaited during setup and teardown
Define __aenter__ only, since async teardown reuses the synchronous __exit__ method unchanged
Define __aenter__ and __aexit__ as ordinary methods that return awaitable futures via a callback
What is the experimental 'free-threaded' build in Python 3.13 (PEP 703), and what are the trade-offs of removing the GIL for existing C extensions?
Select the correct answer
It replaces the GIL with multiprocessing so all C extensions work unchanged and faster
It adds a second GIL per thread, doubling parallelism without affecting any C extensions
It compiles Python to machine code, removing the need for any C extension changes at all
It removes the GIL so threads run in parallel, but some C extensions need updates
Compare and contrast threading, multiprocessing, and asyncio. When is each appropriate?
Select the correct answer
threading and asyncio suit I/O-bound work; multiprocessing suits CPU-bound parallelism
asyncio suits CPU-bound work while threading and multiprocessing handle I/O-bound tasks
multiprocessing suits I/O-bound work while threading handles all CPU-bound parallel tasks
all three run CPU-bound code in parallel; the choice depends only on code readability
Explain what the GIL is and how it impacts multithreading in Python. If Python has a GIL, when would you still choose to use the threading module over multiprocessing?
Select the correct answer
Threads share memory cheaply, so use them for I/O-bound tasks where the GIL is released
Threads avoid the GIL entirely, so use them whenever fast inter-process messaging is needed
Threads run truly in parallel, so use them for CPU-bound tasks that need multiple cores
Threads spawn faster than processes, so use them only for heavy CPU-bound computation work
What is the difference between a per-interpreter GIL introduced in 3.12 and the traditional GIL?
Select the correct answer
The per-interpreter GIL removes locking entirely so all threads now run in parallel
The per-interpreter GIL replaces processes with threads sharing global state safely
Each subinterpreter has its own GIL, allowing separate interpreters to run in parallel
Each thread shares one GIL but subinterpreters bypass it for parallel execution
How does the asyncio event loop work, and what happens to the loop when you execute a blocking I/O call inside an async function?
Select the correct answer
The loop preempts tasks on a timer; a blocking call is paused and resumed later without blocking.
The loop runs ready tasks until each awaits; a blocking call freezes the whole loop and all tasks.
The loop polls tasks on parallel cores; a blocking call is automatically offloaded to a worker pool.
The loop spawns a thread per task; a blocking call only delays that one task, not the others.
What makes an object awaitable in Python? Explain the relationship between coroutines, Tasks, and Futures.
Select the correct answer
Any object passed to await is awaitable; coroutines, Tasks, and Futures are three names for one class.
An object defining __await__ is awaitable; a Task schedules a coroutine and subclasses Future.
An object defining __next__ is awaitable; a Future schedules a Task that wraps a coroutine.
An object with a callback is awaitable; Tasks are low-level results that Futures schedule on the loop.
How does the await keyword actually work under the hood?
Select the correct answer
It starts a new OS thread for the coroutine and joins it once the work is done.
It invokes the awaitable's __await__ and yields control to the event loop until completion.
It schedules a callback and returns a future immediately without suspending anything.
It blocks the current thread entirely until the awaited coroutine finishes running.
How does the Python import system work, and what happens when you import a module for the second time?
Select the correct answer
The module file is read again and its top-level code is re-executed completely.
The interpreter raises an error since importing the same module twice is forbidden.
A fresh independent module object is created each time leaving the cache unchanged.
The cached module in sys.modules is reused and its code is not executed again.