Common Python interview questions with clear answers, examples you can run in your browser, and the follow-ups interviewers like to ask: core language, data structures, functions, OOP, internals, concurrency, tools and coding problems.
- 44 questions
- 17 easy
- 23 medium
- 4 hard
- Free, no sign-up
- Link to this question
An immutable object can't be changed after it's created; "changing" it makes a new object. A mutable object can be changed in place.
Immutable Mutable int,float,bool,str,tuple,frozenset,byteslist,dict,set,bytearray, most custom objectsmain.py · editableOutputPress Run to execute this code.Why it matters:
- Aliasing: two names can point to the same mutable object, so a change through one is visible through the other.
- Dictionary keys and set items must be hashable, which in practice means immutable: a tuple can be a key, a list can't.
- Default arguments that are mutable are a classic bug (see that question).
- Immutable objects are safe to share between threads and functions.
main.py · editableOutputPress Run to execute this code. - Link to this question
==compares values: do the two objects have equal contents? (It calls__eq__.)iscompares identity: are they the same object in memory? (Sameid().)
main.py · editableOutputPress Run to execute this code.Use
isonly for singletons:None,True,False.main.py · editableOutputPress Run to execute this code.Trap: small integers and some strings are cached by CPython, so
iscan seem to work by accident:main.py · editableOutputPress Run to execute this code.Never rely on that: compare numbers and strings with
==. - Link to this question
Default values are evaluated once, when the function is defined, not each time it's called. A mutable default (like
[]or{}) is therefore shared between calls:main.py · editableOutputPress Run to execute this code.The fix: use
Noneas the default and create a new object inside the function:main.py · editableOutputPress Run to execute this code.The same applies to
dict,setand any mutable object, and to defaults likedatetime.now(), which is fixed at definition time.Linters such as Ruff and Pylint flag this pattern automatically.
- Link to this question
They let a function accept a variable number of arguments:
*argscollects extra positional arguments into a tuple.**kwargscollects extra keyword arguments into a dict.
main.py · editableOutputPress Run to execute this code.The same symbols unpack when calling a function:
main.py · editableOutputPress Run to execute this code.Full parameter order:
def f(pos_only, /, normal, *args, kw_only, **kwargs).- Parameters before
/are positional-only. - Parameters after
*or*argsare keyword-only.
A common use is a wrapper or decorator that passes everything through:
return func(*args, **kwargs). - Link to this question
- A shallow copy creates a new outer object, but the items inside are shared with the original.
- A deep copy recursively copies everything, so nothing is shared.
main.py · editableOutputPress Run to execute this code.Ways to make a shallow copy:
lst[:],list(lst),lst.copy(),dict(d),d.copy(),{**d},copy.copy(x).When it matters: copying nested structures such as a list of lists, a dict of lists, or JSON-like data.
A classic bug with the same cause:
main.py · editableOutputPress Run to execute this code.deepcopyis slower and handles cycles. Classes can customise it with__copy__and__deepcopy__. - Link to this question
Neither, exactly. Python passes object references by value, often called pass by assignment or "pass by object reference". The parameter becomes a new name for the same object.
- If the function changes a mutable object in place, the caller sees the change.
- If the function rebinds the parameter to a new object, the caller doesn't see it.
main.py · editableOutputPress Run to execute this code.So a function can't change a caller's variable to point to something else; it can only change mutable objects, or return new values (
x = increment(x)).Good practice: avoid surprising callers by mutating their arguments. Either document it clearly or work on a copy and return the result.
- Link to this question
In an
iforwhile, any object can be tested. These are falsy:NoneandFalse- zero of any number type:
0,0.0,0j,Decimal(0) - empty collections:
"",[],(),{},set(),range(0)
Everything else is truthy, including
"0","False",[0]and-1.main.py · editableOutputPress Run to execute this code.Custom classes decide with
__bool__, or__len__if there's no__bool__.Pythonic checks:
main.py · editableOutputPress Run to execute this code.Trap:
if value:treats0and""the same asNone. When0or an empty string is a valid value, useif value is not None:.andandorreturn one of their operands, not justTrue/False:name = user_input or "Guest". - Link to this question
Python looks up names using the LEGB rule, in this order:
- Local: inside the current function.
- Enclosing: in any outer function (for nested functions).
- Global: at the top level of the module.
- Built-in: names like
lenandprint.
main.py · editableOutputPress Run to execute this code.Assigning to a name inside a function makes it local to that function, unless you declare otherwise:
main.py · editableOutputPress Run to execute this code.Without
globalornonlocal,count += 1raisesUnboundLocalError, because the assignment makescountlocal, and it's read before it's set.In real code, avoid
global: pass values in and return results, or use a class. - Link to this questionmain.py · editableOutputPress Run to execute this code.
Good practices:
- Catch specific exceptions, not bare
except:(which also catchesKeyboardInterrupt). - Keep the
tryblock small, so you don't accidentally catch errors from other lines. Put follow-up code inelse. - Use
with(context managers) instead offinallyfor closing files and locks. - Re-raise with plain
raiseto keep the original traceback; chain withraise NewError(...) from eto add context. - Define your own exceptions by subclassing
Exception.
main.py · editableOutputPress Run to execute this code.EAFP vs LBYL: Python style often prefers "easier to ask forgiveness than permission" (try it and handle the exception) over "look before you leap" (check first), especially where the check could become out of date (a file deleted between the check and the open).
- Catch specific exceptions, not bare
- Link to this question
List [1, 2, 3]Tuple (1, 2, 3)Mutable? yes no Typical use a collection of similar items that grows or changes a fixed record, like (x, y)or(name, age)Hashable (dict key, set item)? no yes, if its items are hashable Memory and speed a bit more memory (room to grow) slightly smaller and faster to create main.py · editableOutputPress Run to execute this code.Follow-ups:
- Single-item tuple:
(5,). The comma makes the tuple, not the parentheses:(5)is just5. - "Is a tuple always immutable?" The tuple is, but it can hold mutable items: in
t = ([1], 2),t[0].append(3)works. - Named tuples (
collections.namedtuple,typing.NamedTuple) give tuple items names:p.xinstead ofp[0].
- Single-item tuple:
- Link to this question
A
dictis a hash table:- Python calls
hash(key)to get an integer. - That number picks a slot in an internal array.
- If the slot is taken by a different key (a collision), it probes other slots.
- To find a key later, it hashes again, goes to the slot, and checks equality with
==.
That's why lookups, inserts and deletes are O(1) on average, regardless of size.
Keys must be hashable: their hash must never change, and equal keys must have equal hashes. Mutable objects like lists aren't hashable, because changing them would change their hash and the dict would look in the wrong slot.
main.py · editableOutputPress Run to execute this code.Other facts interviewers like:
- Since Python 3.7, dicts keep insertion order (guaranteed by the language).
- Custom classes are hashable by identity by default. If you define
__eq__, also define__hash__(or use a frozendataclass). 1,1.0andTrueare equal and have the same hash, so they're the same key:
main.py · editableOutputPress Run to execute this code.Sets use the same hash-table idea, storing keys without values.
- Python calls
- Link to this question
A comprehension builds a collection from an iterable in one readable expression:
[expression for item in iterable if condition].main.py · editableOutputPress Run to execute this code.A generator expression uses parentheses and produces values lazily, without building a list in memory:
main.py · editableOutputPress Run to execute this code.When not to use them:
- When the logic needs several lines, multiple conditions or side effects: a normal
forloop is clearer. - Deeply nested comprehensions are hard to read; two levels is usually the limit.
- Don't use a list comprehension only for side effects (
[print(x) for x in items]).
Comprehensions are usually a bit faster than an equivalent
forloop withappend, because the loop runs in optimised bytecode. - When the logic needs several lines, multiple conditions or side effects: a normal
- Link to this questionmain.py · editableOutputPress Run to execute this code.
Tool Use it for Countercounting, top-N, comparing frequencies ( Counter(a) == Counter(b)checks anagrams)defaultdictgrouping and accumulating without if key not in dchecksdequequeues (BFS), sliding windows, "last N items"; list.pop(0)is O(n),deque.popleft()is O(1)namedtuplesmall immutable records (or typing.NamedTuple/dataclasswith type hints)OrderedDictmostly historical now; still has move_to_end, handy for LRU cachesChainMaplayered lookups, like settings overriding defaults - Link to this question
list.sort()sorted(iterable)Works on lists only any iterable Result sorts in place, returns Nonereturns a new list Both accept
key=(a function giving the value to sort by) andreverse=True.main.py · editableOutputPress Run to execute this code.Key facts:
- The algorithm is Timsort: O(n log n) worst case, and very fast on data that's already partly sorted.
- It's stable: items with equal keys keep their original order. That lets you sort by several keys in passes (secondary key first, then primary).
operator.itemgetter(1)andoperator.attrgetter("age")are fast alternatives to lambdas.- For the top few items of a big list,
heapq.nlargest(k, items, key=...)is faster than sorting everything.
- Link to this question
Operation listdict/setGet by index lst[i]O(1) — Look up a key / x inO(n) O(1) average Append / add O(1) amortised O(1) average Insert or delete at the start O(n) (everything shifts) — Pop from the end O(1) — Delete by key — O(1) average Sort O(n log n) — Copy, iterate O(n) O(n) The most common performance bug is a membership test on a list inside a loop:
main.py · editableOutputPress Run to execute this code.Other things to know:
dequegives O(1) appends and pops at both ends.heapqgives O(log n) push and pop of the smallest item, for priority queues.- String concatenation in a loop can be O(n²); collect pieces in a list and
"".join(parts). - "Amortised O(1)" for
append: lists over-allocate, so occasional resizes are spread across many appends.
- Link to this question
sequence[start:stop:step]returns a new sequence fromstartup to, but not including,stop. Any part can be left out.main.py · editableOutputPress Run to execute this code.Slicing works on lists, tuples, strings,
range, bytes and many libraries' types (NumPy, pandas).On lists, you can also assign to a slice:
main.py · editableOutputPress Run to execute this code.Slices create copies for lists and strings (O(k) for k items). For huge data,
itertools.isliceormemoryviewavoids copying. - Link to this question
A decorator is a function that takes a function and returns a new function that adds behaviour around it, without changing the original code.
@decoratorabove a function is shorthand forfunc = decorator(func).main.py · editableOutputPress Run to execute this code.A decorator with arguments needs one more level: a function that returns the decorator.
main.py · editableOutputPress Run to execute this code.Common uses: logging, timing, caching (
functools.lru_cache), retries, access checks, and frameworks registering routes (@app.get("/")in Flask and FastAPI).Built-in decorators:
@property,@staticmethod,@classmethod,@dataclass,@functools.cache. - Link to this question
A generator is a function that uses
yieldto produce values one at a time, pausing between them. Calling it returns a generator object; nothing runs until you ask for the first value.main.py · editableOutputPress Run to execute this code.Why use them:
- Memory: values are produced on demand, so you can process huge files or infinite streams without loading everything.
- Pipelines: chain small generators, each doing one step.
main.py · editableOutputPress Run to execute this code.Things to know:
- A generator can be iterated only once.
yield from other()delegates to another generator.- Generator expressions:
(x * x for x in data). itertools(islice,chain,groupby,count) is full of generator-based tools.
- Link to this question
- An iterable is anything you can loop over: it has
__iter__(), which returns an iterator. Lists, strings, dicts, files and ranges are iterables. - An iterator is the object that does the looping: it has
__next__(), which returns the next item or raisesStopIteration. It remembers its position, and is used up after one pass.
A
forloop callsiter()once, thennext()untilStopIteration:main.py · editableOutputPress Run to execute this code.Writing your own iterable class:
main.py · editableOutputPress Run to execute this code.Gotcha:
zip,map,filter, file objects and generators are iterators, so they're exhausted after one use. Convert to a list if you need to loop again. - An iterable is anything you can loop over: it has
- Link to this question
A lambda is a small, anonymous function written in one expression:
lambda arguments: expression.main.py · editableOutputPress Run to execute this code.mapapplies a function to every item;filterkeeps items where the function returns true. Both return lazy iterators:main.py · editableOutputPress Run to execute this code.Guidelines:
- Use lambdas for short throwaway functions, mostly as
key=arguments or callbacks. - If you'd assign a lambda to a name, write a
def: it gets a real name in tracebacks. - Prefer comprehensions over
map/filterwith a lambda. functools.reduceexists, butsum,max,any,allor a loop are usually clearer.
- Use lambdas for short throwaway functions, mostly as
- Link to this question
A closure is an inner function that remembers variables from the enclosing function, even after the outer function has returned.
main.py · editableOutputPress Run to execute this code.Uses: function factories, callbacks that carry configuration, decorators (the wrapper is a closure over
func), and simple private state.The classic late-binding trap: closures capture variables, not values. In a loop, all functions see the variable's final value:
main.py · editableOutputPress Run to execute this code.To change a captured variable inside the inner function, declare it
nonlocal. - Link to this question
A context manager sets something up and guarantees cleanup, even if an error happens. It's what the
withstatement uses:main.py · editableOutputPress Run to execute this code.Write one as a class with
__enter__and__exit__:main.py · editableOutputPress Run to execute this code.Or, more simply, with
contextlib.contextmanagerand a generator: code beforeyieldis setup, code after is cleanup.main.py · editableOutputPress Run to execute this code.Common uses: files, locks, database transactions, network connections, temporary directories, and timers.
contextlibalso offerssuppress(SomeError)andExitStackfor handling several resources. - Link to this question
functools.lru_cache(orfunctools.cache, unlimited) memoises a function: it stores results by arguments, so repeated calls with the same arguments return instantly.main.py · editableOutputPress Run to execute this code.Rules for caching:
- Arguments must be hashable (no lists or dicts).
- Only cache pure functions: same input, same output, no side effects.
maxsizelimits memory; least-recently-used entries are dropped first.- On methods, the cache keeps
selfalive. Be careful with many short-lived objects.
functools.partialcreates a new function with some arguments pre-filled:main.py · editableOutputPress Run to execute this code.partialis handy for callbacks andkey=functions that need extra arguments. - Link to this question
Instance method @classmethod@staticmethodFirst argument self(the instance)cls(the class)none Can access instance and class data class data only its arguments Typical use normal behaviour alternative constructors, class-wide settings helper functions that belong with the class main.py · editableOutputPress Run to execute this code.Why
clsmatters: a classmethod called on a subclass creates an instance of that subclass.main.py · editableOutputPress Run to execute this code.Examples from the standard library:
dict.fromkeys(),datetime.fromisoformat()andint.from_bytes()are classmethods. - Link to this question
Dunder ("double underscore") methods like
__init__and__len__let your classes work with Python's built-in syntax and functions. Python calls them for you:len(x)callsx.__len__(),a + bcallsa.__add__(b).main.py · editableOutputPress Run to execute this code.Category Methods Creation and display __init__,__new__,__repr__,__str__,__format__Comparison and hashing __eq__,__lt__(+functools.total_ordering),__hash__Containers __len__,__getitem__,__setitem__,__contains__,__iter__Arithmetic __add__,__sub__,__mul__,__radd__(when the left side doesn't support it)Other __call__(call an object like a function),__enter__/__exit__(with),__bool__Always define a helpful
__repr__. Or let@dataclassgenerate__init__,__repr__and__eq__for you. - Link to this question
A class can inherit from one or more parent classes. When you call a method, Python searches the classes in the Method Resolution Order (MRO), which it computes with the C3 linearization algorithm: each class appears before its parents, and parents keep the order you listed them in.
main.py · editableOutputPress Run to execute this code.Notice that
super()inAcalledB, notBase.super()means "the next class in the MRO of the object", not "my parent". That's what makes cooperative multiple inheritance work: in the diamond above,Base.greetruns exactly once.super()in__init__:main.py · editableOutputPress Run to execute this code.Practical advice:
- Prefer composition (having an object) over deep inheritance trees.
- Use multiple inheritance mainly for small mixins that add one behaviour (
JsonMixin,LoggingMixin). - Always call
super().__init__(...)when overriding__init__, and accept**kwargsin cooperative hierarchies.
- Link to this question
@dataclassgenerates the boilerplate for classes that mainly hold data:__init__,__repr__and__eq__, plus optional ordering, immutability and more.main.py · editableOutputPress Run to execute this code.Useful options:
Option Effect frozen=Trueimmutable and hashable (usable as dict keys) order=Truegenerates <,>and so on, comparing fields in orderslots=True(3.10+)uses __slots__: less memory, faster attribute accesskw_only=True(3.10+)fields must be passed by keyword __post_init__validation or derived fields after __init__Alternatives:
typing.NamedTuple(immutable, tuple-like); Pydantic models when you need validation and parsing of external data (APIs, config);attrsfor more features. - Link to this question
@propertyturns a method into an attribute-like accessor. Callers writeobj.value, but your code runs, so you can compute values or validate assignments without changing the public interface.main.py · editableOutputPress Run to execute this code.Why it's useful: start with a plain public attribute. If you later need validation, switch to a property and no calling code changes. That's why Python doesn't need Java-style
get_x()/set_x()methods.Use
functools.cached_propertyfor an expensive value computed once on first access. - Link to this question
Duck typing: "if it walks like a duck and quacks like a duck, it's a duck". Python cares whether an object has the methods you call, not what class it is.
main.py · editableOutputPress Run to execute this code.Abstract base classes (
abc.ABC) define a required interface and refuse to instantiate subclasses that don't implement it:main.py · editableOutputPress Run to execute this code.Protocols (
typing.Protocol) give duck typing static type checking: any class with matching methods satisfies the protocol, with no inheritance needed. Tools like mypy and Pyright check it.main.py · editableOutputPress Run to execute this code.Use When Plain duck typing small scripts, flexible code ABCa framework or plugin system that should fail early if a method is missing Protocoltype-checked code that accepts "anything with these methods" - Link to this question
By default, each instance stores its attributes in a per-object dictionary,
__dict__. Declaring__slots__replaces that dict with a fixed set of attribute slots:- less memory per instance, which matters with millions of objects;
- slightly faster attribute access;
- no new attributes can be added by accident (typos raise an error).
main.py · editableOutputPress Run to execute this code.Trade-offs:
- No dynamic attributes, and no
__weakref__unless you add it to the slots. - Every class in an inheritance chain needs its own
__slots__, or instances get a__dict__anyway. - Some libraries that rely on
__dict__(certain pickling or copying tricks) need extra care.
Easy way to use it:
@dataclass(slots=True)(Python 3.10+).Use it for small, numerous objects (points, records, graph nodes). For ordinary classes, the default is fine.
- Link to this question
Both, in a way. CPython (the standard implementation):
- Compiles your source code to bytecode, a simpler, lower-level instruction set. It's cached in
__pycache__/*.pycfiles for imported modules. - Interprets that bytecode in a loop in the Python virtual machine.
You can see the bytecode with
dis:main.py · editableOutputPress Run to execute this code.Other implementations:
Implementation How it runs CPython bytecode interpreter (with an experimental JIT from 3.13) PyPy a JIT compiler: often several times faster for pure-Python loops Pyodide CPython compiled to WebAssembly: it's what runs the examples on this site, in your browser Cython, mypyc compile (typed) Python to C extensions Why Python is slower than C or Java for tight loops: dynamic typing (every operation checks types at run time), objects for every value, and interpretation overhead. The usual fixes are to push work into C-backed libraries (NumPy, pandas), use built-ins, or use PyPy, Cython or Numba for hot code.
- Compiles your source code to bytecode, a simpler, lower-level instruction set. It's cached in
- Link to this question
CPython uses two mechanisms:
1. Reference counting (the main one). Every object counts how many references point to it. When the count drops to zero, the object is freed immediately.
main.py · editableOutputPress Run to execute this code.2. A cyclic garbage collector. Reference counting can't free cycles (A refers to B, B refers to A, nothing else refers to either). The
gcmodule periodically finds and frees unreachable cycles. It's generational: new objects are checked often, long-lived ones rarely.main.py · editableOutputPress Run to execute this code.Other points:
- Memory freed by Python is often kept by Python's allocator (pymalloc) for reuse, so a process's memory use may not shrink much.
- Leaks in Python usually mean objects are still referenced: growing global caches, lists that are never cleared, closures or callbacks holding big objects.
- Tools:
tracemalloc(which lines allocate memory),gc.get_objects(),objgraph, memory profilers. weakreflets you refer to an object without keeping it alive, which is useful for caches.
- Link to this question
The Global Interpreter Lock (GIL) is a lock in CPython that allows only one thread to run Python bytecode at a time. It keeps CPython's internals (like reference counts) simple and safe.
The effect:
Workload Threads help? Why I/O-bound (network calls, disk, databases) yes a thread releases the GIL while it waits for I/O, so other threads run CPU-bound pure Python (number crunching in loops) no only one thread executes Python at a time, so there's no speed-up on several cores CPU-bound in C libraries (NumPy, many compression and image libraries) often yes many C extensions release the GIL during heavy work # CPU-bound work: use processes to use several cores from concurrent.futures import ProcessPoolExecutor def count_primes(limit): return sum(1 for n in range(2, limit) if all(n % d for d in range(2, int(n ** 0.5) + 1))) if __name__ == "__main__": with ProcessPoolExecutor() as pool: results = list(pool.map(count_primes, [200_000] * 4)) # 4 processes, 4 cores print(results)Points that impress interviewers:
- The GIL doesn't make your code thread-safe: operations like
counter += 1are several bytecode steps, and a thread switch can happen between them. You still need locks. - Python 3.13 introduced an optional free-threaded build (PEP 703) without the GIL; it's becoming more mature in newer releases.
- Other implementations differ: Jython and IronPython have no GIL; PyPy has one.
- The GIL doesn't make your code thread-safe: operations like
- Link to this question
threadingmultiprocessingasyncioBest for I/O-bound work with blocking libraries CPU-bound work lots of concurrent I/O (thousands of connections) How OS threads sharing memory; the GIL allows one to run Python at a time separate processes, each with its own interpreter and GIL one thread, an event loop switching between tasks at awaitUses several CPU cores no (for Python code) yes no Overhead medium high (process start-up, data must be pickled between processes) very low per task Main risk race conditions memory use, sharing data one blocking call freezes everything import asyncio import httpx # an async HTTP client async def fetch(client, url): response = await client.get(url) # other tasks run while this waits return url, response.status_code async def main(): urls = ["https://example.com"] * 20 async with httpx.AsyncClient() as client: results = await asyncio.gather(*(fetch(client, u) for u in urls)) print(results) asyncio.run(main())How to choose:
- CPU-bound?
multiprocessing/ProcessPoolExecutor(or vectorise with NumPy). - I/O-bound with async libraries available (web servers like FastAPI,
httpx,asyncpg)?asyncio. - I/O-bound with blocking libraries (
requests, most database drivers)?ThreadPoolExecutor.
concurrent.futuresgives threads and processes the same simple interface (submit,map). In async code, run unavoidable blocking calls withawait asyncio.to_thread(func). - CPU-bound?
- Link to this question
async defdefines a coroutine function. Calling it returns a coroutine object; it doesn't run yet.awaitruns another coroutine and pauses the current one until the result is ready. While paused, the event loop runs other tasks.- Everything runs in one thread. Tasks switch only at
await, so there are no surprise thread switches.
import asyncio import time async def brew(name, seconds): print(f"start {name}") await asyncio.sleep(seconds) # non-blocking wait: other tasks run print(f"done {name}") return name async def main(): start = time.perf_counter() results = await asyncio.gather( # run concurrently brew("tea", 2), brew("coffee", 3), brew("toast", 1), ) print(results, f"{time.perf_counter() - start:.1f}s") # about 3s, not 6s asyncio.run(main())Key points:
await asyncio.sleep(1)lets other tasks run;time.sleep(1)blocks the whole loop. The same goes for blocking libraries likerequests. Use async libraries (httpx,aiohttp,asyncpg) orasyncio.to_thread.asyncio.gatherandasyncio.TaskGroup(3.11+) run coroutines concurrently.asyncio.create_taskstarts one in the background.asyncio.timeout(5)(3.11+) orasyncio.wait_foradd timeouts.asyncio.Semaphorelimits concurrency, e.g. at most 10 requests at once.- Async helps with waiting (I/O), not with CPU work.
- Link to this question
Type hints annotate the expected types of variables, parameters and return values. Python doesn't enforce them at run time; they're used by tools: type checkers (mypy, Pyright), editors (autocomplete, error highlighting) and libraries like Pydantic and FastAPI, which read them to validate data.
main.py · editableOutputPress Run to execute this code.A type checker would flag
greet(123)before the code ever runs.Modern syntax (3.9 / 3.10+):
list[int],dict[str, int],int | Noneinstead ofList,Dict,Optional.Useful extras:
TypedDict(dicts with known keys),Literal["red", "green"],Protocol(structural typing),TypeVarand generics,Callable[[int], str].Why use them: catch bugs early, better editor help, and self-documenting code, especially in larger codebases and teams. Many teams run mypy or Pyright in CI.
- Link to this question
match(Python 3.10+) compares a value against patterns that can check structure and extract values at the same time. It's more than aswitchstatement.main.py · editableOutputPress Run to execute this code.It works on dicts and classes too, which suits JSON-like data:
main.py · editableOutputPress Run to execute this code.Gotcha: a bare name in a pattern captures a value; it doesn't compare against a variable.
case RED:matches anything and assigns it toRED. Use dotted names (case Color.RED:) or literals to compare. - Link to this question
A virtual environment is an isolated folder with its own Python interpreter and installed packages. Each project gets its own versions of libraries, so projects don't conflict and you don't touch the system Python.
python -m venv .venv # create source .venv/bin/activate # activate (Windows: .venv\Scripts\activate) pip install requests pandas # installs only into this environment pip freeze > requirements.txt # record exact versions pip install -r requirements.txt # recreate elsewhereModern tools:
Tool What it adds uv very fast all-in-one: creates environments, installs packages, manages Python versions, lock files ( uv add,uv sync)Poetry / PDM / Hatch dependency management with pyproject.tomland lock files, packagingpip-tools turns a short list of direct dependencies into a pinned requirements.txtconda also manages non-Python dependencies (C libraries, CUDA); popular in data science Good practices:
- Keep dependencies in
pyproject.toml(the modern standard), with a lock file for exact, reproducible versions. - Never commit the
.venvfolder; add it to.gitignore. - Pin versions for applications; use version ranges for libraries you publish.
- In Docker, you often skip a venv, or use one in a multi-stage build.
- Keep dependencies in
- Link to this question
pytest is the most popular framework: tests are plain functions with plain
assertstatements.# shop.py def total(prices, discount=0.0): if not 0 <= discount < 1: raise ValueError("discount must be between 0 and 1") return round(sum(prices) * (1 - discount), 2) # test_shop.py import pytest from shop import total def test_total_without_discount(): assert total([10, 20]) == 30 @pytest.mark.parametrize("prices, discount, expected", [ ([100], 0.1, 90.0), ([], 0.5, 0.0), ([19.99, 0.01], 0, 20.0), ]) def test_total_with_discounts(prices, discount, expected): assert total(prices, discount) == expected def test_invalid_discount(): with pytest.raises(ValueError): total([10], discount=1.5)Run with
pytestin the project folder; it findstest_*.pyfiles automatically.Key features to mention:
Feature Use Fixtures ( @pytest.fixture)reusable setup and teardown: a test database, a temporary folder ( tmp_path), an API clientparametrizerun one test with many inputs Mocking ( unittest.mock,monkeypatch)replace slow or external parts: HTTP calls, time, randomness pytest-covmeasure test coverage Good testing habits: test behaviour, not implementation details; keep tests fast and independent; mock at the boundaries (network, clock), not your own logic; and run tests in CI on every pull request.
The standard library's
unittest(class-based,self.assertEqual) also works, and pytest can run those tests too. - Link to this question
Pythonic code uses Python's features and idioms the way they're meant to be used: clear, concise and readable (see
import this, "The Zen of Python", and the PEP 8 style guide).main.py · editableOutputPress Run to execute this code.More idioms:
withfor resources (files, locks).- Unpacking:
first, *rest = items. is None, not== None.- EAFP:
try/except KeyErrorwhere appropriate. - Small functions, descriptive names,
snake_casefor functions and variables.
Tools that enforce style: Ruff (fast linter and formatter), Black (formatter), plus a type checker.
- Link to this question
Brute force checks every pair: O(n²) time.
Better: one pass with a dictionary of numbers already seen. For each number, check whether its complement (
target - n) was seen before: O(n) time, O(n) memory.main.py · editableOutputPress Run to execute this code.What interviewers look for:
- Clarify: can numbers repeat? Is there always exactly one answer? Can you use the same element twice? (No: that's why we check before adding
ntoseen.) - Start with the brute force, state its complexity, then improve it.
- Test edge cases out loud: duplicates (
[3, 3]), no solution, negative numbers.
Variation: if the list is sorted, use two pointers from both ends: O(n) time and O(1) memory.
- Clarify: can numbers repeat? Is there always exactly one answer? Can you use the same element twice? (No: that's why we check before adding
- Link to this question
Normalise the text, count with
Counter, then take the top k:main.py · editableOutputPress Run to execute this code.Complexity: counting is O(n) for n words.
most_common(k)uses a heap, so it's O(m log k) for m distinct words, which is better than sorting all of them when k is small.Without
Counter, to show you know the mechanics:main.py · editableOutputPress Run to execute this code.Follow-ups to discuss:
- Ties: sort by
(-count, word)for a stable, alphabetical tie-break. - Stop words: filter out "the", "a" and so on.
- Huge files: read line by line (a generator), so the whole file never sits in memory.
- Too big for one machine: split the counting across workers and merge the counters (the classic MapReduce word count).
- Ties: sort by
- Link to this question
Clean the string, then compare it with its reverse:
main.py · editableOutputPress Run to execute this code.That's O(n) time and O(n) extra memory. With two pointers, you can use O(1) extra memory by skipping non-alphanumeric characters from both ends:
main.py · editableOutputPress Run to execute this code.Things to say: clarify the rules first (case? spaces? Unicode?), mention both approaches and their complexity, and test the edge cases: an empty string, one character, only punctuation.
- Link to this question
Recursive generator: yield items, and recurse into anything that's a list.
main.py · editableOutputPress Run to execute this code.Iterative version with a stack, which avoids Python's recursion limit (about 1,000 levels) for very deep nesting:
main.py · editableOutputPress Run to execute this code.Discussion points:
- Why not flatten strings: a string is iterable, and each character is also a string, so checking for "any iterable" would recurse forever. Checking
isinstance(item, list)(or excludingstrandbytes) avoids that. - For exactly one level of nesting,
itertools.chain.from_iterable(lists)or[x for sub in lists for x in sub]is simplest. - Generators keep memory low for large inputs.
- Why not flatten strings: a string is iterable, and each character is also a string, so checking for "any iterable" would recurse forever. Checking