Loading...
Loading...
Python interview guide
Memory, references, garbage collection and performance. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
53 questionsLast updated
Both. CPython compiles source to bytecode, then its virtual machine interprets the bytecode. The bytecode is not machine code, which is why Python needs an interpreter at run time. The language itself does not say how it must run, so PyPy adds a just-in-time compiler.
Follow-up: Why do syntax errors show before any output? The whole file is compiled first. A syntax error stops that step, so no line has run yet.
Common mistake: Saying only 'Python is interpreted' and ignoring the compile step.
Lesson: Bytecode and the Python InterpreterBytecode is the instruction list that the compiler makes from your source. A .pyc file in __pycache__ stores it for an imported module, so the next import is faster. The main script is compiled every run and not cached.
They are different implementations of the same language. CPython is the standard one written in C. PyPy adds a just-in-time compiler that speeds up long loops. Jython runs on the Java virtual machine. Behaviour such as reference counting is a CPython detail.
Lesson: Bytecode and the Python InterpreterThe default is evaluated once when def runs and stored on the function. A list or dictionary default is then shared by every call that omits the argument, so changes leak between calls. Use None and build the object inside.
Follow-up: When is a shared default useful? As a deliberate cache, for example a memo dictionary. Say so in a comment, because readers will otherwise think it is a bug.
Common mistake: Saying the default 'resets on every call'.
Lesson: Bytecode and the Python InterpreterLocals are stored in numbered slots of the frame and read with LOAD_FAST. A global needs a dictionary lookup with LOAD_GLOBAL. The gain is small, so only use it in very hot loops, for example by binding a function to a local name first.
It shows the bytecode of a function. It helps to understand why two versions of code differ, for example folded constants or global versus local lookups. It is a learning and debugging tool, not something production code depends on, because output changes between versions.
Lesson: Bytecode and the Python Interpretercompile turns text into a code object. exec runs a code object or text as statements. eval evaluates one expression and returns its value. Running text from users with exec or eval is a security risk.
CPython uses reference counting as the main method: each object counts its references and is freed at zero. A cycle detector in the gc module handles objects that refer to each other. Small objects come from a private memory allocator, so Python does not call the operating system for every object.
Follow-up: What are the weaknesses of reference counting? It cannot free cycles on its own, it costs an update on every reference change, and it needs the GIL or atomic operations to stay correct across threads.
Common mistake: Saying Python 'has a garbage collector' without mentioning reference counting.
Lesson: Reference Counting and Garbage CollectionReference counting frees an object the moment its count reaches zero. The garbage collector runs now and then and frees cycles that counting cannot. Both run in CPython; PyPy uses only a tracing collector.
Lesson: Reference Counting and Garbage CollectionA cycle is a group of objects that refer to each other, so their counts never reach zero. The gc module frees them later. To investigate, use gc.collect(), gc.get_objects() or tracemalloc, and break cycles with weak references when you can.
Follow-up: How do weak references help? A weak reference does not raise the count, so a child can point to its parent without keeping it alive. Caches use WeakValueDictionary for the same reason.
The global interpreter lock lets one thread run Python bytecode at a time. It keeps reference counting and other interpreter data safe without fine-grained locks, and it keeps single-thread code fast. The price is that CPU-bound threads do not run in parallel.
Follow-up: How do you get real parallelism? Use multiprocessing or concurrent.futures.ProcessPoolExecutor, C extensions that release the GIL, or the experimental free-threaded build added in Python 3.13.
Common mistake: Saying 'Python cannot do multithreading'. Threads work well for I/O.
Lesson: Reference Counting and Garbage CollectionNo. It protects the interpreter, not your logic. counter += 1 is several bytecode steps, and a thread switch can happen between them, so you still need a threading.Lock or a thread-safe structure such as queue.Queue.
Use threads for I/O-bound work such as network calls and file reads, because waiting releases the GIL. Use processes for CPU-bound work such as number crunching, because each process has its own interpreter and GIL. Processes cost more memory and need data to be sent between them.
Lesson: Reference Counting and Garbage Collectiondel x removes the name x from the namespace, which lowers the reference count of the object. The object is freed only when nothing else refers to it. del can also remove list items, slices and dictionary keys.
Its timing depends on the implementation and on cycles, and exceptions inside it are ignored. Use a with block and a context manager, or try/finally, for files, locks and connections.
is and ==?== compares values by calling __eq__. is compares identity: whether both names refer to the same object. Two equal lists are different objects. Use is for None, True and False, and == for everything else.
Follow-up: Why can a is b be True for small integers? CPython keeps one shared object for small integers and some strings, so equal values can be one object. This is an implementation detail and must never be relied on.
Common mistake: Saying 'is compares memory addresses so it is faster, use it everywhere'.
Lesson: Object Memory and __slots__It fixes the attribute names of a class so instances store them in fixed positions and have no __dict__. That saves memory and slightly speeds up access. Limits: no new attributes, no weak references unless __weakref__ is listed, and subclasses need their own __slots__ to keep the saving.
Every Python value is an object with a header (reference count and type pointer), and containers hold pointers to those objects. A list of a million integers is a million pointers plus a million objects. For large numeric data, use array, NumPy or other compact types.
It measures the shallow size of one object. For the real total of nested data, add the sizes of the children, use tracemalloc to see allocations, or a memory profiler. Never trust getsizeof alone for containers.
Look for objects that live longer than intended: growing lists, dictionaries, caches or global registries. Use tracemalloc snapshots to compare two moments, check gc.get_objects(), and break references with del, weak references or bounded caches such as functools.lru_cache(maxsize=...).
x is None preferred to x == None?A class can define __eq__ to return anything, even True for None. is cannot be overridden and checks the one None object. It is also what PEP 8 recommends.
No. They are immutable: operations create new objects. That is why CPython can share small integers and interned strings safely. Lists, dictionaries and sets are mutable, which is why a shared default or an alias can surprise you.
Lesson: Object Memory and __slots__The source is compiled to a code object of bytecode. The virtual machine then runs the bytecode one instruction at a time on a value stack. Imported modules are cached as .pyc files. Objects are freed by reference counting, with a cycle collector as backup.
Add __slots__ to remove the per-object dictionary. Use tuples or NamedTuple for fixed records, array or NumPy for numbers, generators instead of full lists, and measure with tracemalloc before and after.
Follow-up: What is the downside of __slots__? You cannot add attributes, weak references need __weakref__ in the slots, and subclasses must define their own slots to keep the saving.
is versus == and give a case where they differ.is compares identity, == compares value. Two equal lists are == but not is. A class with a custom __eq__ can be == to None while is None stays false, so use is None.
No. The GIL stops two threads from running bytecode at the same instant, but a thread switch can happen between the steps of count += 1. Protect shared state with a lock or a queue. For CPU-bound speed use processes.
Objects stay alive while anything refers to them: a growing global list, an unbounded cache, a callback registry, or an exception holding a traceback. The collector only frees unreachable objects. Find leaks with tracemalloc and fix them with bounded caches or weak references.
First measure the whole run with a stable benchmark. Then use cProfile to find the functions with the most total time, look at the algorithm inside the top one, change one thing and measure again. Do not optimise code the profile does not point to.
Follow-up: What do you do if the profile is flat? A flat profile means no single hot spot. Look for a better algorithm or data structure, reduce work done, or move to a faster tool such as NumPy, because micro-changes will not matter.
Common mistake: Starting with micro-optimisations such as local variable tricks without any measurement.
Lesson: timeit and cProfiletimeit repeats the code many times, uses the best available clock and switches off the garbage collector, so noise is much smaller. A single time.time() difference includes one-off effects, such as cold caches, and has lower resolution.
Outside noise only ever adds time: other processes, interrupts and cache misses. The smallest reading is the one with the least interference, so it is closest to the true cost of the code.
Lesson: timeit and cProfiletottime is the time spent inside the function itself, excluding calls to other functions. cumtime includes all the functions it calls. A function with high cumtime but low tottime mainly calls something slow.
cProfile hooks every call and return, so counts are exact but it slows the program. A sampling profiler looks at the stack at fixed intervals, costs less and suits long-running production code, but call counts are estimates.
Making code faster or more complex before measuring shows that it matters. It costs readability and often changes nothing. Make it correct first, then measure, then optimise only the proven hot spot.
Lesson: timeit and cProfileUse the same input and machine, run it many times, ignore the first run if it warms a cache, keep the minimum of several repeats and compare versions in the same session. Make sure the code does not skip work, for example because of a cache that stays filled between runs.
Lesson: timeit and cProfileThey are hash tables. Python computes the hash of the key, turns it into a position and jumps there, so lookup takes about the same time for any size. The average cost is constant. The worst case, many keys hashing to the same place, is rare with Python's hashing.
Follow-up: Why must dict keys be hashable? The hash decides where the key is stored. If the key could change, its hash would change and Python could no longer find it, so mutable types such as lists are not allowed as keys.
Common mistake: Saying dict lookup is 'always O(1)' without saying 'average'.
Lesson: Choosing Data Structures for Speedif x in some_list for 100000 items. How do you speed it up?Convert the list to a set once before the loop. Membership then takes constant time on average instead of walking the list. The loop goes from O(n*m) to O(n+m). Check that the items are hashable and that order and duplicates do not matter.
Lesson: Choosing Data Structures for SpeedA list is an array of pointers, so removing the first item shifts all others left, which costs O(n). collections.deque is built for both ends, so popleft is O(1). Use deque for queues and breadth-first search.
When you search often and insert rarely, or when you need range queries such as 'all prices between 10 and 20'. Search is O(log n); insertion is still O(n) because items shift. For constant churn, use a heap or a balanced structure from a library.
Lesson: Choosing Data Structures for SpeedUse heapq.nlargest(k, items). It keeps a heap of size k, so it costs about O(n log k) instead of O(n log n). For k close to n, sorting is simpler and as fast.
Tuples are immutable, a little smaller and faster to create, and can be dictionary keys when their items are hashable. Lists are resizable. Choose by meaning first: fixed record or changing sequence. Speed is rarely the reason.
Lesson: Choosing Data Structures for SpeedMost appends are cheap. Now and then the list must grow, which copies all items, but the list grows by a proportional amount, so the average cost over many appends is constant.
Lesson: Choosing Data Structures for SpeedMeasure first and profile to find the hot spot. Then in order: use a better algorithm or data structure, remove repeated work from loops, use built-ins and comprehensions, cache repeated results with lru_cache, use generators if memory is the problem. If it is still too slow, use NumPy or another compiled library, or processes for CPU work, or threads or asyncio for I/O.
Follow-up: When would you rewrite in another language? Only after the profile shows a small hot spot that algorithm changes and libraries cannot fix. Often just that function moves to a C extension, Cython or Rust.
Common mistake: Jumping to micro-optimisations or a rewrite before profiling.
Lesson: Common Python Speed-UpsIt stores the result of a function for each set of arguments, so repeat calls skip the work. In Python use functools.lru_cache or functools.cache. Arguments must be hashable and the function must be pure, meaning the same inputs always give the same output. Bound the cache in long-running programs.
A comprehension is usually a little faster because it avoids the repeated method lookup and call of append, and it reads clearly. The gain is small, so choose it for clarity, and use a loop when the body is long or has side effects.
A generator when you process items once and the data is large or endless, because memory stays constant. A list when you need the length, indexing, several passes or slicing. A generator is also lazy, so errors in later items appear only when you reach them.
Lesson: Common Python Speed-UpsFor I/O-bound work with a few connections use threads. For thousands of connections use asyncio, which has less overhead per task. For CPU-bound work use processes, since each has its own GIL. Processes cost more to start and must send data between them.
Follow-up: Why do threads not speed up CPU-bound code? The GIL lets only one thread run bytecode at a time, so threads take turns on one core.
Lesson: Common Python Speed-UpsStrings are immutable, so each addition may copy the whole string, and the total cost can grow with the square of the length. CPython sometimes optimises the simple case, but you cannot rely on it. Collect the parts in a list and use "".join(parts), which is linear on every implementation.
A NumPy array stores raw numbers of one type next to each other, and its operations loop in compiled C code. A Python list stores pointers to separate objects, and each operation runs interpreter steps. Vectorised operations avoid the Python loop altogether.
Lesson: Common Python Speed-UpsMeasure the total time, profile with cProfile, and read the top functions by tottime. Fix the algorithm or data structure first, for example a set for membership or a Counter for counting. Remove repeated work from loops, use built-ins and generators, and cache pure results. Measure after each change and stop when the speed is enough.
I/O-bound code mostly waits, so threads or asyncio overlap the waiting. CPU-bound code mostly calculates, so use processes, NumPy or another compiled library. In CPython the GIL stops threads from running bytecode in parallel, which is why the choice matters.
Lesson: Performance CheckpointRun the same inputs through the old and new versions and compare outputs, ideally with tests. Then time both on the same data. An optimisation that changes the answer is a bug, not a speed-up.
Lesson: Performance CheckpointWhen the function has side effects, depends on changing data, takes unhashable arguments, or returns large objects that would fill memory. Use a bounded maxsize, and remember that cached results are shared, so mutating a returned list changes the cached value.
Programs spend most of their time in a small part of the code, and people guess that part wrongly. A profile shows the real hot spot, so effort goes where it pays and code stays simple elsewhere.
Lesson: Performance CheckpointEach lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson