Skip to content

Python — Theory

The Global Interpreter Lock is a mutex protecting CPython object memory. Only one thread executes Python bytecode at a time.

  • Why exists: simplifies CPython’s reference-counting GC. Removing it requires per-object locks or alternative GC (PyPy uses generational without GIL).
  • Released during: I/O (network, file), time.sleep, blocking C extensions that explicitly release it (numpy ops). So threads still help I/O-bound work.
  • Hurts CPU-bound multi-threading — threads can’t use multiple cores for pure Python compute.
  • Workarounds:
    • multiprocessing (each process has own GIL/heap; IPC via pipe/queue/shm).
    • C extensions (numpy, cython) that release GIL.
    • Free-threaded Python 3.13 (PEP 703) — experimental, no GIL.
    • asyncio for I/O concurrency on single thread.
  • Single thread, single event loop, cooperative multitasking.
  • await suspends the coroutine; loop schedules others.
  • Event loop never knows what’s blocking — if you call sync I/O inside coroutine, loop freezes.
  • Tasks vs coroutines: create_task schedules immediately; bare coroutine is just an object.
  • Cancellation: task.cancel() raises CancelledError at next await. Must handle for cleanup.
  • Context propagation: contextvars.ContextVar survives across await.
  • Reference counting (primary GC): every object has refcount. When 0 → freed immediately.
  • Cyclic GC (secondary): handles ref cycles (A → B → A). Generational (3 generations).
  • sys.getsizeof(obj) reports shallow size.
  • Memory leaks: usually unintentional global refs, caches, listeners, circular refs in C extensions.
  • Diagnose: tracemalloc, objgraph, memory_profiler, pympler.
  1. What does @property do, and how does it interact with inheritance? Descriptor that turns method into attribute access. Can have getter, setter, deleter.

  2. __slots__ tradeoffs? Memory savings (no per-instance dict). Cost: no dynamic attrs, harder to subclass without slots conflict, can’t use weakref unless added explicitly.

  3. Decorators with args: 3-level nesting.

    def tag(name):
    def deco(fn):
    def wrap(*a, **k): print(name); return fn(*a,**k)
    return wrap
    return deco
  4. Mutable default args bug:

    def f(x=[]): # shared across calls!
    x.append(1); return x
    # use: def f(x=None): x = x or []
  5. Generator vs iterator: every generator is an iterator; not all iterators are generators. Generators are easier to write (yield).

  6. Metaclasses: class of a class. type is the default. Override __init__/__new__ of metaclass to customize class creation. Used by Django ORM, SQLAlchemy declarative base, ABCs.

  7. async def vs def returning Future: coroutine objects are awaitable; sync function returning Future is also awaitable.

  • Request lifecycle: WSGI/ASGI handler → middleware (request) → URL resolver → view → middleware (response).
  • ORM N+1: select_related('fk_field') → SQL JOIN, eager fetch (FK and OneToOne). prefetch_related('m2m') → 2 queries with IN, joined in Python (M2M and reverse FK).
  • Lazy querysets evaluate on iteration, slicing (without step), len, bool, list(), repr (in shell).
  • Migrations: makemigrations writes file, migrate applies. Squash old migrations periodically.
  • Signals: avoid for new code — implicit, hard to test. Prefer explicit calls / domain events.
  • select_for_update: row lock for transaction. Combine with transaction.atomic().
  • Channels for WebSockets/ASGI.
  • Built on Starlette + Pydantic.
  • Dependency injection via Depends(fn) — testable, composable.
  • Background tasks via BackgroundTasks for fire-and-forget after response.
  • Pydantic v2: 5-50× faster than v1 (Rust core). model_validate, model_dump.
  • cProfile + snakeviz to visualize.
  • py-spy — sampling profiler, no code change, attaches to running process.
  • line_profiler for hot functions.
  • Common wins: avoid + string concat in loops (use ''.join), use dict/set for membership, cache with functools.lru_cache, use generators for streaming, use C-backed libs (orjson, ujson, cython).
  • Late binding in closures: [lambda: i for i in range(3)] — all return 2. Fix: [lambda i=i: i ...].
  • Modifying a list while iterating.
  • Catching too broadly (except Exception silently swallowing).
  • Using is for value comparison (x is 1000 may be False; CPython interns small ints only).
  • Forgetting __hash__ when overriding __eq__ → object unusable in dict/set.