[{"data":1,"prerenderedAt":186},["ShallowReactive",2],{"topic-python-concurrency":3},{"framework":4,"topic":15,"subtopics":24},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":8,"stem":12,"tier":13,"__hash__":14},"frameworks\u002Fframeworks\u002Fpython.yml","Python interview questions across language fundamentals, data structures and common gotchas for backend, data and automation roles.","yml","python",{},"Python",3,"frameworks\u002Fpython",1,"QsijsotyAr-3rnhJDWWZmc7hE6HAhylS5t1dKpigMOA",{"id":16,"description":17,"extension":7,"frameworkSlug":8,"meta":18,"name":19,"order":20,"slug":21,"stem":22,"__hash__":23},"topics\u002Ftopics\u002Fpython-concurrency.yml","Threads and the GIL, multiprocessing, asyncio and concurrent.futures — running Python code concurrently and in parallel.",{},"Concurrency & Parallelism",9,"concurrency","topics\u002Fpython-concurrency","5RIdEXxDtKsbX3veVxq6vhE87mv01LWJ2ut_nkPxEQM",[25,69,107,146],{"id":26,"title":27,"body":28,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":8,"meta":37,"navigation":38,"order":13,"path":39,"questions":40,"related":62,"seo":63,"seoDescription":64,"stem":65,"subtopic":66,"topic":19,"topicSlug":21,"updated":67,"__hash__":68},"qa\u002Fpython\u002Fconcurrency\u002Fgil.md","Gil",{"type":29,"value":30,"toc":31},"minimark",[],{"title":32,"searchDepth":33,"depth":33,"links":34},"",2,[],"hard","md",{},true,"\u002Fpython\u002Fconcurrency\u002Fgil",[41,45,50,54,58],{"id":42,"difficulty":35,"q":43,"a":44},"what-is-the-gil","What is the GIL?","The **Global Interpreter Lock** is a mutex in **CPython** that allows **only one\nthread to execute Python bytecode at a time**. Even on a multi-core machine, a\nmultithreaded pure-Python program runs its bytecode on **one core at a time** —\nthreads take turns holding the lock.\n\nIt exists to make CPython's memory management (especially **reference counting**)\nsimple and fast: without it, every refcount update would need its own lock. The\ninterpreter releases the GIL periodically and **around blocking I\u002FO** so other\nthreads can run.\n\n```python\nimport threading\n# both threads exist, but the GIL serializes their bytecode execution\ndef work():\n    total = 0\n    for _ in range(10_000_000):   # CPU-bound — holds the GIL\n        total += 1\n\nt1 = threading.Thread(target=work)\nt2 = threading.Thread(target=work)\nt1.start(); t2.start(); t1.join(); t2.join()   # ~no speedup vs one thread\n```\n\nWhy it matters: the GIL is a **CPython implementation detail** (not in the language\nspec, and absent in Jython\u002Fthe free-threaded 3.13+ build) that shapes when threads\ndo and don't help.\n",{"id":46,"difficulty":47,"q":48,"a":49},"threading-vs-multiprocessing","medium","What is the difference between threading and multiprocessing?","**Threads** share one process and one memory space, so they're cheap to create and\nshare data directly — but in CPython they're serialized by the **GIL**.\n**Processes** each have their own interpreter and memory, so they run on\n**separate cores in true parallel**, bypassing the GIL — at the cost of higher\nstartup overhead and needing to **serialize (pickle) data** to communicate.\n\n```python\nfrom threading import Thread\nfrom multiprocessing import Process\n\nThread(target=fn)    # shared memory, GIL-bound, light\nProcess(target=fn)   # separate memory, real parallelism, heavier\n```\n\nThreads communicate through shared objects (guarded by locks); processes\ncommunicate through `Queue`, `Pipe`, or shared-memory primitives because they don't\nshare state.\n\nRule of thumb: **threads for I\u002FO-bound** concurrency, **processes for CPU-bound**\nparallelism.\n",{"id":51,"difficulty":35,"q":52,"a":53},"cpu-vs-io-bound","Why don't threads speed up CPU-bound work but help I\u002FO-bound work?","For **CPU-bound** work, threads are constantly executing bytecode, so they're\nalways contending for the **GIL** — only one runs at a time, and you get no\nparallel speedup (often a small slowdown from lock contention and context switches).\n\nFor **I\u002FO-bound** work, a thread that's waiting on the network, disk, or a database\nis **blocked outside the interpreter** — and CPython **releases the GIL during\nblocking I\u002FO**. So other threads run while one waits, giving real concurrency.\n\n```python\nimport requests, threading\n\ndef fetch(url):\n    requests.get(url)     # blocks on the network -> GIL released here\n\n# 10 threads overlap their waiting time -> much faster than sequential\nthreads = [threading.Thread(target=fetch, args=(u,)) for u in urls]\n```\n\nRule of thumb: if your bottleneck is **waiting**, use threads (or asyncio); if it's\n**computing**, use **processes** to get past the GIL.\n",{"id":55,"difficulty":35,"q":56,"a":57},"race-conditions-locks","What is a race condition and how do locks prevent it?","A **race condition** occurs when two threads access shared mutable state and the\nresult depends on **timing**. Even `x += 1` is not atomic — it's read, add, write,\nand a thread can be switched out mid-sequence, so updates get lost.\n\nA **lock** (`threading.Lock`) creates a **critical section**: only the thread\nholding it can proceed, the rest wait, so the read-modify-write happens atomically.\nUsing `with lock:` guarantees the lock is always released, even on exceptions.\n\n```python\nimport threading\ncounter = 0\nlock = threading.Lock()\n\ndef increment():\n    global counter\n    for _ in range(100_000):\n        with lock:           # only one thread in here at a time\n            counter += 1     # now safe from lost updates\n```\n\nBeware over-locking: acquiring multiple locks in different orders can cause\n**deadlock**. Rule of thumb: guard **every** access to shared mutable state, keep\ncritical sections small, and prefer thread-safe `queue.Queue` for handoff.\n",{"id":59,"difficulty":47,"q":60,"a":61},"threadpool-vs-processpool","When do you use ThreadPoolExecutor vs ProcessPoolExecutor?","Both come from `concurrent.futures` and share the same API — `submit` \u002F\n`map` returning `Future` objects — so you can swap them with one line. The\ndifference is the **worker type**: `ThreadPoolExecutor` runs tasks in **threads**\n(shared memory, GIL-bound) and `ProcessPoolExecutor` runs them in **separate\nprocesses** (true parallelism, pickled args).\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n\n# I\u002FO-bound: many network\u002Ffile waits -> threads\nwith ThreadPoolExecutor(max_workers=20) as ex:\n    results = list(ex.map(download, urls))\n\n# CPU-bound: heavy computation -> processes (one per core)\nwith ProcessPoolExecutor() as ex:\n    results = list(ex.map(crunch_numbers, datasets))\n```\n\nUse **ThreadPoolExecutor for I\u002FO-bound** tasks (downloads, DB calls) where waiting\ndominates, and **ProcessPoolExecutor for CPU-bound** tasks to use all cores —\nremembering its arguments and return values must be **picklable**.\n\nRule of thumb: pick the executor by the bottleneck (waiting vs computing), and let\n`concurrent.futures` handle the pool lifecycle and result collection.\n",null,{"description":32},"Python interview questions on threading and the GIL — what the GIL is, threads vs multiprocessing, CPU-bound vs I\u002FO-bound work, race conditions and locks, and ThreadPoolExecutor vs ProcessPoolExecutor.","python\u002Fconcurrency\u002Fgil","Threading & the GIL","2026-06-18","xnP8BJ-7TKpKC-39NQwkB1eRxm7A9hp5p5zsqHw6SWE",{"id":70,"title":71,"body":72,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":8,"meta":76,"navigation":38,"order":33,"path":77,"questions":78,"related":62,"seo":103,"seoDescription":104,"stem":105,"subtopic":71,"topic":19,"topicSlug":21,"updated":67,"__hash__":106},"qa\u002Fpython\u002Fconcurrency\u002Fmultiprocessing.md","Multiprocessing",{"type":29,"value":73,"toc":74},[],{"title":32,"searchDepth":33,"depth":33,"links":75},[],{},"\u002Fpython\u002Fconcurrency\u002Fmultiprocessing",[79,83,87,91,95,99],{"id":80,"difficulty":35,"q":81,"a":82},"multiprocessing-vs-threading","How does multiprocessing sidestep the GIL, and how is it different from threading?","**Threading** runs multiple threads inside **one process** that share one\ninterpreter — and therefore one **GIL** (Global Interpreter Lock), which lets\nonly one thread execute Python bytecode at a time. **Multiprocessing** spawns\n**separate OS processes**, each with its **own** interpreter and **own GIL**, so\nthey can run Python code in **true parallel** on multiple CPU cores.\n\n```python\nfrom multiprocessing import Process\nimport os\n\ndef work():\n    print(f\"running in pid {os.getpid()}\")  # a distinct process each time\n\nif __name__ == \"__main__\":            # required guard on Windows\u002Fspawn\n    ps = [Process(target=work) for _ in range(4)]\n    for p in ps: p.start()\n    for p in ps: p.join()\n```\n\nThe tradeoff: processes don't share memory, so passing data costs\n**serialization (pickling) and IPC overhead**, and each process has higher\nstartup cost than a thread. Rule of thumb: reach for multiprocessing when you\nneed real CPU parallelism, not just concurrency.\n",{"id":84,"difficulty":47,"q":85,"a":86},"process-vs-pool","What is the difference between Process and Pool?","A **`Process`** represents a **single** child process you start and join\nmanually — good when you have a fixed, small number of distinct tasks. A\n**`Pool`** manages a **reusable group of worker processes** and hands out work\nto them, which is far more convenient for **many homogeneous tasks** over a\ndataset.\n\n```python\nfrom multiprocessing import Pool\n\ndef square(n):\n    return n * n\n\nif __name__ == \"__main__\":\n    with Pool(processes=4) as pool:\n        results = pool.map(square, range(10))   # distributed across 4 workers\n    print(results)   # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]\n```\n\n`Pool` reuses workers (amortizing startup cost) and offers `map`, `imap`,\n`apply_async`, etc. Use `Process` for a few long-lived distinct jobs; use `Pool`\nwhen you're fanning the same function over many inputs.\n",{"id":88,"difficulty":35,"q":89,"a":90},"ipc-queue-pipe","How do processes communicate (Queue vs Pipe)?","Because processes don't share memory, they communicate through **IPC**\nprimitives. A **`Queue`** is a **multi-producer\u002Fmulti-consumer**, thread- and\nprocess-safe FIFO — the general-purpose choice. A **`Pipe`** is a faster but\nlower-level **two-endpoint** connection, best for communication between exactly\n**two** processes.\n\n```python\nfrom multiprocessing import Process, Queue\n\ndef producer(q):\n    q.put(\"result\")          # values are pickled across the boundary\n\nif __name__ == \"__main__\":\n    q = Queue()\n    p = Process(target=producer, args=(q,))\n    p.start()\n    print(q.get())           # \"result\"\n    p.join()\n```\n\nBoth serialize objects under the hood, so only **picklable** data flows through\nthem. Use `Queue` for fan-in\u002Ffan-out among many workers; use `Pipe` for a tight\none-to-one channel where you want the lower overhead.\n",{"id":92,"difficulty":35,"q":93,"a":94},"pickling-overhead","What is the pickling overhead, and what can't be pickled?","Every argument and return value crossing a process boundary must be\n**serialized with `pickle`**, sent, then **deserialized** on the other side.\nFor large objects this **copying cost can dwarf the parallelism gains**, and\nsome objects simply **can't be pickled**.\n\n```python\nimport pickle\n\npickle.dumps(lambda x: x)   # PicklingError — lambdas aren't picklable\n# Also unpicklable: open file handles, sockets, locks, db connections,\n# local\u002Fnested functions, and generators.\n```\n\nPicklable things include module-level functions and classes, and basic\ncontainers of picklable values. The practical implications: pass **small,\npicklable** payloads, define worker functions at **module top level**, and\navoid shipping huge data structures between processes. Minimizing what crosses\nthe boundary is the key to multiprocessing performance.\n",{"id":96,"difficulty":35,"q":97,"a":98},"shared-state","How do you share state between processes?","Since each process has its own memory, you need explicit shared-state tools.\n**`Value`** and **`Array`** put simple data in **shared memory** (fast, but\nlimited types and you must guard with a lock). A **`Manager`** hosts richer\nshared objects (`dict`, `list`, etc.) via a **server process** — more flexible\nbut slower because access is proxied.\n\n```python\nfrom multiprocessing import Process, Value, Lock\n\ndef inc(counter, lock):\n    for _ in range(1000):\n        with lock:               # protect the shared value\n            counter.value += 1\n\nif __name__ == \"__main__\":\n    counter = Value(\"i\", 0)      # shared int in shared memory\n    lock = Lock()\n    ps = [Process(target=inc, args=(counter, lock)) for _ in range(4)]\n    for p in ps: p.start()\n    for p in ps: p.join()\n    print(counter.value)         # 4000\n```\n\nPrefer **message passing** (Queue\u002FPipe) over shared state when you can — it's\neasier to reason about. Reach for `Value`\u002F`Array` for hot, simple counters and a\n`Manager` only when you genuinely need shared complex objects.\n",{"id":100,"difficulty":47,"q":101,"a":102},"when-multiprocessing","When should you use multiprocessing instead of threads or asyncio?","Use multiprocessing for **CPU-bound** work — number crunching, image\nprocessing, data transforms — where you need to **saturate multiple cores** with\nPython code. Threads and asyncio can't do that because the **GIL** serializes\nbytecode execution; only separate processes get separate GILs.\n\n```python\nfrom multiprocessing import Pool\n\ndef heavy(n):                      # pure-Python CPU work\n    return sum(i * i for i in range(n))\n\nif __name__ == \"__main__\":\n    with Pool() as pool:           # defaults to os.cpu_count() workers\n        print(pool.map(heavy, [10_000_000] * 8))   # runs in parallel\n```\n\nFor **I\u002FO-bound** work (network, disk, DB), threads or asyncio are usually\nbetter — they're cheaper and the GIL is released during I\u002FO anyway. Rule of\nthumb: CPU-bound -> multiprocessing; I\u002FO-bound -> threads\u002Fasyncio.\n",{"description":32},"Python interview questions on the multiprocessing module: how it sidesteps the GIL, Process vs Pool, IPC with Queue\u002FPipe, pickling limits, and shared state.","python\u002Fconcurrency\u002Fmultiprocessing","q6g-NDE__tlhDOdIPbX1WLFlzxD4custMmavX-rPQ3s",{"id":108,"title":109,"body":110,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":8,"meta":114,"navigation":38,"order":11,"path":115,"questions":116,"related":62,"seo":141,"seoDescription":142,"stem":143,"subtopic":144,"topic":19,"topicSlug":21,"updated":67,"__hash__":145},"qa\u002Fpython\u002Fconcurrency\u002Fasyncio.md","Asyncio",{"type":29,"value":111,"toc":112},[],{"title":32,"searchDepth":33,"depth":33,"links":113},[],{},"\u002Fpython\u002Fconcurrency\u002Fasyncio",[117,121,125,129,133,137],{"id":118,"difficulty":35,"q":119,"a":120},"what-is-asyncio","What is asyncio and the event loop?","**asyncio** is Python's framework for **single-threaded concurrency** using an\n**event loop**. The **event loop** is a scheduler that runs many **coroutines**\ncooperatively: when one coroutine **awaits** something (typically I\u002FO), it\n**yields control** back to the loop, which runs another ready coroutine while the\nfirst waits. No thread is blocked sitting idle.\n\n```python\nimport asyncio\n\nasync def main():\n    print(\"hello\")\n    await asyncio.sleep(1)    # yields to the loop instead of blocking\n    print(\"world\")\n\nasyncio.run(main())           # creates the loop, runs main, then closes it\n```\n\nCrucially this is **concurrency, not parallelism** — one thread, one core,\ninterleaving tasks at `await` points. Rule of thumb: asyncio shines when you\nhave **many tasks that spend most of their time waiting** on I\u002FO.\n",{"id":122,"difficulty":47,"q":123,"a":124},"async-def-await","What do async def and await do?","**`async def`** defines a **coroutine function** — calling it doesn't run the\nbody, it returns a **coroutine object** that must be awaited or scheduled.\n**`await`** **suspends** the current coroutine until the awaited **awaitable**\n(another coroutine, a Task, or a Future) completes, handing control back to the\nevent loop in the meantime.\n\n```python\nimport asyncio\n\nasync def fetch():\n    await asyncio.sleep(1)    # suspension point — loop runs others here\n    return \"data\"\n\nasync def main():\n    coro = fetch()            # nothing has run yet\n    result = await coro       # now it runs; main suspends until it finishes\n    print(result)\n\nasyncio.run(main())\n```\n\nYou can only `await` **inside** an `async def`. Forgetting to await a coroutine\nis a common bug — it never runs and you get a \"coroutine was never awaited\"\nwarning. Think of `await` as \"pause me here and let others run until this is\nready.\"\n",{"id":126,"difficulty":35,"q":127,"a":128},"coroutines-vs-threads","How do coroutines differ from threads?","**Threads** use **preemptive** multitasking — the OS can switch threads at\n**any** point, so shared state needs locks and context switches are relatively\nexpensive. **Coroutines** use **cooperative** multitasking on **one thread** —\nswitches happen **only at explicit `await` points**, so the code between awaits\nis effectively atomic and switching is cheap.\n\n```python\nimport asyncio\n\nasync def task(name):\n    print(f\"{name} start\")\n    await asyncio.sleep(1)        # the ONLY place this can yield\n    print(f\"{name} done\")\n\nasync def main():\n    await asyncio.gather(task(\"a\"), task(\"b\"))   # thousands are feasible\n\nasyncio.run(main())\n```\n\nBecause there's no OS thread per task, you can run **tens of thousands** of\ncoroutines cheaply, and most data races disappear. The catch: a coroutine that\nnever awaits **monopolizes** the loop. Threads tolerate blocking code;\ncoroutines do not.\n",{"id":130,"difficulty":47,"q":131,"a":132},"asyncio-gather","How do you run coroutines concurrently with asyncio.gather?","Awaiting coroutines one by one runs them **sequentially**. To run them\n**concurrently**, schedule them together with **`asyncio.gather`** (or wrap each\nin a **Task**), which lets the loop interleave their `await` points.\n\n```python\nimport asyncio\n\nasync def fetch(n):\n    await asyncio.sleep(1)\n    return n * 2\n\nasync def main():\n    # all three overlap -> ~1 second total, not 3\n    results = await asyncio.gather(fetch(1), fetch(2), fetch(3))\n    print(results)        # [2, 4, 6] — order matches the arguments\n\nasyncio.run(main())\n```\n\n`gather` returns results in argument order and, by default, propagates the first\nexception. `asyncio.create_task(coro)` schedules a coroutine to start running\nimmediately so it overlaps with later code. The key idea: concurrency comes from\n**scheduling tasks together**, not from awaiting them in turn.\n",{"id":134,"difficulty":35,"q":135,"a":136},"dont-block-the-loop","What is the \"don't block the event loop\" rule?","Because asyncio runs on **one thread**, any code that **doesn't await** —\nheavy CPU work or **blocking synchronous I\u002FO** like `time.sleep`,\n`requests.get`, or blocking DB drivers — **freezes the entire loop**. Every other\ncoroutine stalls until that call returns.\n\n```python\nimport asyncio, time\n\nasync def bad():\n    time.sleep(5)              # BLOCKS the whole loop for 5s\n\nasync def good():\n    await asyncio.sleep(5)     # yields; other tasks keep running\n\n# offload unavoidable blocking\u002FCPU work to a thread or process pool:\nasync def offloaded():\n    loop = asyncio.get_running_loop()\n    await loop.run_in_executor(None, time.sleep, 5)   # runs in a thread\n```\n\nFixes: use **async-native libraries** (`aiohttp`, `asyncpg`), and push\nCPU-bound or unavoidably-blocking calls into `run_in_executor` (a thread pool, or\na process pool for CPU work). Rule of thumb: inside `async` code, never call\nsomething that blocks without awaiting it.\n",{"id":138,"difficulty":47,"q":139,"a":140},"when-async-helps","When does asyncio actually help?","asyncio wins for **high-concurrency I\u002FO-bound** workloads — thousands of\nnetwork calls, web requests, websocket connections, or database queries that\nspend their time **waiting**. While one request waits, the loop services others,\nso a single thread handles huge concurrency cheaply.\n\n```python\nimport asyncio\n\nasync def call_api(i):\n    await asyncio.sleep(0.5)        # stand-in for a network round trip\n    return i\n\nasync def main():\n    # 1000 \"requests\" overlap on one thread in ~0.5s of wall time\n    results = await asyncio.gather(*(call_api(i) for i in range(1000)))\n    print(len(results))             # 1000\n\nasyncio.run(main())\n```\n\nIt does **not** help **CPU-bound** work — that needs multiprocessing for real\nparallelism. And for a handful of blocking calls, plain threads are often\nsimpler. Reach for asyncio when concurrency is high and the bottleneck is\nwaiting on I\u002FO.\n",{"description":32},"Python interview questions on asyncio and async\u002Fawait: the event loop, coroutines vs threads, asyncio.gather, the don't-block-the-loop rule, and when async helps.","python\u002Fconcurrency\u002Fasyncio","asyncio & async\u002Fawait","Nmpm5dZjqwdFh49NpQFVIUo_4pc2dAboUlrFVlULydE",{"id":147,"title":148,"body":149,"description":32,"difficulty":47,"extension":36,"framework":10,"frameworkSlug":8,"meta":153,"navigation":38,"order":154,"path":155,"questions":156,"related":62,"seo":181,"seoDescription":182,"stem":183,"subtopic":184,"topic":19,"topicSlug":21,"updated":67,"__hash__":185},"qa\u002Fpython\u002Fconcurrency\u002Fconcurrent-futures.md","Concurrent Futures",{"type":29,"value":150,"toc":151},[],{"title":32,"searchDepth":33,"depth":33,"links":152},[],{},4,"\u002Fpython\u002Fconcurrency\u002Fconcurrent-futures",[157,161,164,168,173,177],{"id":158,"difficulty":47,"q":159,"a":160},"executor-abstraction","What is the Executor abstraction in concurrent.futures?","`concurrent.futures` provides a **high-level**, uniform interface for running\ncallables asynchronously. An **`Executor`** manages a **pool of workers** and\nhands you back **`Future`** objects representing pending results — and the **same\nAPI** works whether the workers are threads or processes, so you can swap one for\nthe other with a one-line change.\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor\n\ndef work(x):\n    return x * 2\n\nwith ThreadPoolExecutor(max_workers=4) as ex:   # context manager auto-shuts down\n    future = ex.submit(work, 10)                # schedule the call\n    print(future.result())                      # 20\n\n# swap to processes by changing only the class name:\n# with ProcessPoolExecutor() as ex: ...\n```\n\nThe context manager (`with`) cleanly handles worker shutdown and waits for\npending work on exit. Rule of thumb: prefer `concurrent.futures` over raw\n`threading`\u002F`multiprocessing` when you just want to run a function over a pool\nand collect results.\n",{"id":162,"difficulty":47,"q":60,"a":163},"thread-vs-process-pool","Both share the Executor API but differ in workers. **`ThreadPoolExecutor`** runs\ntasks in **threads** within one process — cheap, shared memory, but bound by the\n**GIL**, so it only helps **I\u002FO-bound** work. **`ProcessPoolExecutor`** runs tasks\nin **separate processes**, each with its own GIL, giving **true parallelism** for\n**CPU-bound** work (at the cost of pickling arguments\u002Fresults).\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n\n# I\u002FO-bound: many network\u002Fdisk waits -> threads\nwith ThreadPoolExecutor() as ex:\n    ex.map(download, urls)\n\n# CPU-bound: number crunching -> processes\nwith ProcessPoolExecutor() as ex:\n    ex.map(crunch, datasets)\n```\n\nBecause the API is identical, you can prototype with threads and switch to\nprocesses if the GIL becomes the bottleneck. Rule of thumb: I\u002FO-bound ->\n`ThreadPoolExecutor`; CPU-bound -> `ProcessPoolExecutor`.\n",{"id":165,"difficulty":47,"q":166,"a":167},"submit-and-futures","What does submit() return, and what is a Future?","**`submit(fn, *args)`** schedules `fn` to run in the pool and **immediately**\nreturns a **`Future`** — a handle to a result that may not exist yet. The Future\nlets you check status (`done()`, `running()`), **block for the result**\n(`result()`), retrieve an exception (`exception()`), `cancel()`, or attach a\ncallback (`add_done_callback`).\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor\n\ndef slow_double(x):\n    return x * 2\n\nwith ThreadPoolExecutor() as ex:\n    fut = ex.submit(slow_double, 21)   # returns instantly\n    print(fut.done())                  # False — probably still running\n    print(fut.result())                # 42 — blocks until ready\n```\n\n`result(timeout=...)` blocks (up to an optional timeout) until the value is\nready. A Future decouples **starting** the work from **collecting** it, which is\nwhat makes overlapping multiple calls possible.\n",{"id":169,"difficulty":170,"q":171,"a":172},"executor-map","easy","How does executor.map work and how does it differ from submit?","**`executor.map(fn, iterable)`** is the convenient bulk form: it applies `fn` to\nevery item concurrently and returns an **iterator of results in input order** —\nanalogous to the built-in `map`, but parallel. **`submit`** is lower level,\ngiving you a Future per call for fine-grained control.\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor\n\ndef fetch(url):\n    return len(url)\n\nurls = [\"a\", \"bb\", \"ccc\"]\nwith ThreadPoolExecutor() as ex:\n    for result in ex.map(fetch, urls):   # results stream back in order\n        print(result)                    # 1, 2, 3\n```\n\n`map` is great when you have a clean iterable and want **ordered** results with\nminimal code. Use `submit` (often with `as_completed`) when you need results\n**as they finish**, per-task error handling, or cancellation. Note `map` raises\nthe first exception when you iterate to that result.\n",{"id":174,"difficulty":47,"q":175,"a":176},"as-completed","What does as_completed do?","**`as_completed(futures)`** yields each Future **as soon as it finishes**,\nregardless of submission order — so you can process results the moment they're\nready instead of waiting for the slowest task to keep its place (as `map`'s\nordered output would).\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\ndef work(n):\n    return n * n\n\nwith ThreadPoolExecutor() as ex:\n    futures = [ex.submit(work, i) for i in range(5)]\n    for fut in as_completed(futures):     # whichever finishes first\n        print(fut.result())               # order is non-deterministic\n```\n\nThis is ideal for **responsiveness** — show progress as tasks complete, or\nhandle failures immediately. Use `map` when you want results in input order; use\n`as_completed` when you want them in **completion** order.\n",{"id":178,"difficulty":47,"q":179,"a":180},"exception-propagation","How are exceptions handled with futures?","An exception raised inside a worker is **captured and stored** in its Future,\nnot raised at submit time. It **re-raises when you call `future.result()`** (or\niterate to that item in `map`). You can also inspect it without raising via\n**`future.exception()`**.\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor\n\ndef boom(x):\n    raise ValueError(f\"bad: {x}\")\n\nwith ThreadPoolExecutor() as ex:\n    fut = ex.submit(boom, 1)\n    err = fut.exception()        # returns the ValueError, doesn't raise\n    print(err)                   # bad: 1\n    fut.result()                 # NOW it re-raises ValueError\n```\n\nThe implication: if you never call `result()` (or check `exception()`), an\nerror can **pass silently**. Always retrieve results — typically in a `try\u002Fexcept`\naround `result()` — so worker failures surface in the main thread.\n",{"description":32},"Python interview questions on concurrent.futures: the Executor abstraction, ThreadPoolExecutor vs ProcessPoolExecutor, submit and Future objects, map, as_completed, and exception handling.","python\u002Fconcurrency\u002Fconcurrent-futures","concurrent.futures","EBXrpKsBJ9HKBfCU5qZ9GBoP7V5poZ4M3DOQAmPFiLQ",1781808675498]