Core Python for interviews on one page: the data model, idioms, OOP, the standard library and concurrency, current to Python 3.14.
Objects, identity & copying
Type
Mutable
Hashable
int, float, bool (an int subclass), str, bytes, frozenset, None
no
yes
tuple
no
only if all items are; (1,) needs the comma
list, dict, set, bytearray
yes
no
Variables are names bound to objects; assignment never copies. Arguments are passed by object reference: mutating a passed list is visible to the caller, rebinding the parameter is not.
== compares values (__eq__); is compares identity (id()). Use is only for singletons such as None. CPython caches ints -5 to 256 and interns some strings, so is on them can “work” by accident.
Shallow copy (copy.copy(x), xs[:], list(xs), d.copy()) is a new container sharing the nested objects; copy.deepcopy(x) copies recursively, cycles included.
import copygrid = [[0] * 2] * 2 # two references to ONE rowgrid[0][0] = 1 # [[1, 0], [1, 0]]grid = [[0] * 2 for _ in range(2)] # independent rowsa = [[1], [2]]b = copy.copy(a); b[0].append(9) # a is now [[1, 9], [2]]c = copy.deepcopy(a); c[0].append(7) # a unchanged
python
Memory: an object is freed when its reference count hits zero; a generational cycle collector (gc) reclaims cycles. del x removes a name, not the object. Find leaks with tracemalloc.
set: a | b, a & b, a - b, a ^ b, a <= b. {} is an empty dict; use set().
xs.sort() sorts in place and returns None; sorted(it) returns a new list. Both are stable (Timsort) and take key= and reverse=.
Queues: deque with append + popleft, never list.pop(0).
Comprehensions, slicing & unpacking
evens = [x * x for x in range(10) if x % 2 == 0] # [0, 4, 16, 36, 64]index = {ch: i for i, ch in enumerate("ab")} # {'a': 0, 'b': 1}flat = [x for row in [[1, 2], [3]] for x in row] # [1, 2, 3]: outer loop firstlabels = ["even" if n % 2 == 0 else "odd" for n in range(3)]total = sum(x * x for x in range(10**6)) # generator: O(1) memoryif (n := len(evens)) > 3: print(n) # walrus (3.8): prints 5
python
Dict {k: v ...} and set {...} comprehensions work too; (...) is a lazy, single-pass generator expression. Loop variables don’t leak out.
Slicing s[start:stop:step] excludes stop, counts negatives from the end and never raises on out-of-range bounds: s[::-1] reverses, s[-3:] is the last three, s[:] copies.
No return means None. Sort with key=: sorted(users, key=lambda u: (-u.score, u.name)).
Scope, closures & decorators
Lookup order is LEGB: Local, Enclosing, Global (module), Built-in.
Assigning to a name anywhere in a function makes it local there, so reading it first raises UnboundLocalError. Rebind with global x (module) or nonlocal x (enclosing function).
Closures capture variables, not values (late binding):
fs = [lambda: i for i in range(3)][f() for f in fs] # [2, 2, 2]: i is read at call timefs = [lambda i=i: i for i in range(3)][f() for f in fs] # [0, 1, 2]: the default binds now
python
A decorator takes a function and returns a replacement: @deco means f = deco(f); @a over @b means a(b(f)). functools.wraps keeps __name__ and __doc__; without it every function is named wrapper.
def repeat(n): # with arguments: @repeat(3) def deco(fn): # the factory returns the decorator @functools.wraps(fn) def wrapper(*args, **kwargs): return [fn(*args, **kwargs) for _ in range(n)] return wrapper return deco
python
Iterators, generators & context managers
An iterable has __iter__; an iterator also has __next__ and raises StopIteration when done. for calls iter() then next() repeatedly.
A function with yield returns a generator: lazy, O(1) memory, keeps its state between next() calls, consumable once.
def chunks(xs, size): for i in range(0, len(xs), size): yield xs[i:i + size]list(chunks([1, 2, 3, 4, 5], 2)) # [[1, 2], [3, 4], [5]]def flatten(nested): for x in nested: if isinstance(x, list): yield from flatten(x) # delegate else: yield xlist(flatten([1, [2, [3]]])) # [1, 2, 3]
python
with calls __enter__, then __exit__(exc_type, exc, tb) even if the body raises; a truthy return from __exit__ suppresses the exception.
from contextlib import contextmanagerimport time@contextmanagerdef timer(label): start = time.perf_counter() # __enter__ part try: yield start # value bound by `as` finally: # __exit__ part, runs even on error print(f"{label}: {time.perf_counter() - start:.3f}s")
python
contextlib also has suppress(FileNotFoundError) and ExitStack.
Classes & OOP
Kind
Receives
Use for
instance method
self
behavior using instance state
@classmethod
cls
alternate constructors (cls is the subclass when inherited)
@staticmethod
nothing
a helper that belongs with the class
@property
self
computed or validated attribute; @x.setter for writes
The MRO (C3 linearization, Cls.__mro__) orders lookup: a class before its parents, parents in the order listed. super() calls the next class in the MRO, not necessarily the parent:
@dataclass writes __init__, __repr__, __eq__; slots=True and kw_only=True came in 3.10. A bare [] default raises ValueError: use default_factory.
__slots__ = ("x", "y") drops the per-instance __dict__: less memory, and unlisted attributes raise AttributeError.
Exceptions
try: value = int(raw)except (ValueError, TypeError) as e: # parens optional without `as` (3.14) raise ConfigError(f"bad value {raw!r}") from e # sets __cause__else: print("parsed", value) # only if nothing was raisedfinally: print("cleanup") # always, even after return
python
KeyboardInterrupt and SystemExit derive from BaseException, not Exception, so a bare except: swallows Ctrl-C. Custom exceptions subclass Exception (one base per library).
Order except clauses specific first. raise alone re-raises; raise X from None hides the original.
3.11: ExceptionGroup, except*, e.add_note(). 3.14 warns when return, break or continue exits finally (it discards the exception).
EAFP (try/except) is idiomatic Python; LBYL (if k in d) is the alternative.
Standard library toolkit
collections
Use
Counter(xs)
frequencies; .most_common(k)
defaultdict(list)
grouping; missing keys auto-created
deque(maxlen=n)
O(1) at both ends; BFS, last-n buffers
OrderedDict
move_to_end, popitem(last=False): LRU cache
namedtuple("P", "x y")
lightweight immutable records
itertools
Gives
chain.from_iterable(xss), islice(it, n)
flatten one level, take n
groupby(xs, key)
runs of consecutive equal keys: sort first
product, permutations, combinations
Cartesian product, orderings, subsets
accumulate, pairwise (3.10), batched (3.12)
prefix sums, neighbors, chunks
functools
Use
cache (3.9), lru_cache(maxsize=128)
memoization; hashable arguments only
partial, reduce
pre-fill arguments, fold to one value
cached_property, total_ordering, singledispatch
lazy attribute, comparisons, dispatch on type
Also: heapq (min-heap on a list), bisect (binary search on sorted lists).
Type hints
from typing import Protocol, TypedDict, Literaldef first[T](xs: list[T]) -> T | None: # generics syntax (3.12) return xs[0] if xs else Nonetype Json = dict[str, Json] | list[Json] | str | int | float | bool | Noneclass Closer(Protocol): # any class with close() matches def close(self) -> None: ...class User(TypedDict): id: int role: Literal["admin", "user"]
python
Hints are not enforced at runtime; mypy, pyright and IDEs check them. list[int] (3.9), int | None (3.10).
Optional[X] means X | None, not “optional argument”. Any disables checking. Also Callable, Final, Self (3.11).
3.14 evaluates annotations lazily (PEP 649): forward references need no quotes.
Concurrency & asyncio
threading
multiprocessing
asyncio
Unit
OS thread
OS process
task on one thread
CPU parallelism
no (GIL)
yes
no
Best for
blocking I/O
CPU-bound work
many concurrent network calls
Switching
preemptive
preemptive (OS)
cooperative, at await
Sharing data
shared memory + locks
pickled via Queue/Pipe
shared, single thread
The GIL lets one thread at a time run Python bytecode in standard CPython. Blocking I/O and many C extensions release it, so threads still help I/O-bound code.
It doesn’t make code thread-safe: count += 1 is several bytecodes. Use threading.Lock or queue.Queue.
Pools: concurrent.futures.ThreadPoolExecutor / ProcessPoolExecutor. In 3.14 Linux’s default start method became forkserver (was fork); macOS and Windows use spawn, so guard with if __name__ == "__main__":.
Note
Free-threaded CPython (PEP 703) drops the GIL: experimental in 3.13, officially supported in 3.14 as an optional separate build (often python3.14t; check sys._is_gil_enabled()). Single-threaded code runs somewhat slower, and some C extensions don’t support it yet.
import asyncioasync def fetch(i): await asyncio.sleep(0.1) # yields to the event loop return i * 2async def main(): async with asyncio.TaskGroup() as tg: # 3.11; cancels siblings on error tasks = [tg.create_task(fetch(i)) for i in range(3)] print([t.result() for t in tasks]) # [0, 2, 4] in ~0.1 s, not 0.3 print(await asyncio.gather(fetch(5), fetch(6))) # [10, 12]asyncio.run(main())
python
Calling an async def function only creates a coroutine; it runs when awaited or made a task.
Never block the loop with time.sleep, sync HTTP or heavy CPU: use await asyncio.sleep, async libraries, or asyncio.to_thread(fn). Timeouts: async with asyncio.timeout(5): (3.11).
Modules, packaging & pytest
A module is a .py file; a package is a directory of them (__init__.py optional for namespace packages). Top-level code runs once, on first import; then sys.modules caches it.
if __name__ == "__main__": runs only when the file is executed, not imported.
Imports search sys.path (script directory, PYTHONPATH, stdlib, site-packages). Fix circular imports by moving shared code or importing inside functions.
python3 -m venv .venv && source .venv/bin/activatepython -m pip install -r requirements.txt # pip of THIS interpreterpython -m pip freeze > requirements.txt # pin exact versionspython -m pytest -x -k login # stop at first failure, filter
Terminal
pyproject.toml holds project metadata, dependencies and tool settings.
import pytest@pytest.fixturedef numbers(): # `yield` instead of return adds teardown return [1, 2, 3]def test_sum(numbers): # fixture injected by parameter name assert sum(numbers) == 6 # plain assert, detailed diff on failure@pytest.mark.parametrize("a, b, total", [(1, 2, 3), (-1, 1, 0)])def test_add(a, b, total): assert a + b == totaldef test_div_zero(): with pytest.raises(ZeroDivisionError, match="division by zero"): 1 / 0
python
pytest collects test_*.py or *_test.py files and test-prefixed functions; share fixtures via conftest.py. Built-ins: tmp_path, monkeypatch, capsys.
unittest.mock.patch("app.service.requests"): patch where the name is looked up, not where it’s defined.
Quick answers
List vs tuple? Mutable vs immutable; a tuple of hashables can be a dict key.
Pass by value or reference? Call by sharing: the function gets a reference to the same object.
What is the GIL? A CPython lock letting one thread run bytecode at a time: threads for I/O, processes for CPU.
Generator vs list? Lazy, single-pass, O(1) memory vs materialized, reusable, indexable.
*args / **kwargs? Extra positional arguments as a tuple, extra keyword arguments as a dict.
What is a decorator? A callable wrapping a function to add behavior; use functools.wraps.
@staticmethod vs @classmethod? No implicit argument vs receives cls.
__str__ vs __repr__? Readable for users vs unambiguous for developers.
__new__ vs __init__? Creates the instance vs initializes it.
Shallow vs deep copy? New container sharing children vs a fully independent copy.
How is memory managed? Reference counting, plus a cyclic GC for reference cycles.
Why must dict keys be hashable? The hash locates the slot, so it must never change.
Threads, processes or asyncio? Blocking I/O, CPU-bound work, many sockets.
Gotchas & traps
Mutable defaults, late-binding closures and [[0] * n] * m all share one object (see above).
Resizing a dict or set while iterating raises RuntimeError; for lists, iterate a copy: for x in xs[:].
-7 // 2 is -4 (floors) and -7 % 2 is 1, but int(-3.5) is -3.
round(2.5) is 2 (round half to even). 0.1 + 0.2 == 0.3 is False: use math.isclose or Decimal.
True + True is 2, and {1: "a", True: "b"} is {1: 'b'}.
t = ([1],); t[0] += [2] raises TypeErrorand still mutates the list.
xs = xs.sort() sets xs to None. A second list(gen) is [].
except Exception as e: deletes e after the block.
x = float("nan"): x == x is False, yet [x] == [x] is True (identity is checked first).
Interview tip
Asked to optimize? Name the data structure: set/dict for O(1) lookups, deque for queues, heapq for top-k, Counter for frequencies, a generator for big streams.