Concurrency in Python, for a round

Two threads are two lists of steps, and the machine may run them in any order that keeps each list going forwards. Every bug on this shelf is an order nobody thought about, and every picture here is one the build reproduced.

Does the GIL make Python thread safe?

No. The global interpreter lock means one thread runs Python bytecode at a time, and the Python FAQ says the interpreter offers to switch between threads between bytecode instructions. A line like counter += 1 is several instructions: it loads the name, adds, then stores the name back. A switch can land in the middle, so two threads can both load the same value and both store the same result, and one increment is gone. The same holds for any check followed by an act. The GIL protects the interpreter’s own internal state, not yours.

Threads, asyncio or multiprocessing: how do I choose in an interview?

Ask what the work is waiting for. If it waits on somebody else (a network call, a disk read, a database), threads or asyncio both work, because a waiting thread is not holding the GIL. Threads need no rewrite of the calling code and suit a few dozen jobs; asyncio scales to thousands of waits in one thread and needs the whole path to be async. If the work is computing rather than waiting, neither helps under a standard build, because only one thread runs bytecode at a time: use multiprocessing, or a library that releases the GIL in C. Say which of the two the work is before you name an API.

Is list.append thread safe?

In CPython today, yes: it is one bytecode instruction and the interpreter does not switch inside one. The Python FAQ lists it with a handful of other single operations. Two things to say alongside that. It is a description of the current implementation, not a language guarantee, and the free-threading documentation says so in as many words while recommending a threading.Lock instead. And it does not help you: append is safe, but “read the list, decide, then append” is not, because the switch lands between the deciding and the appending. So the honest answer in a round is: yes in CPython, and I would still take the lock, because the next reader cannot tell which appends were safe by accident.

Do I need a lock with asyncio, if only one task runs at a time?

Sometimes. A task runs until it hits an await, so any stretch of code with no await in it cannot be interrupted by another task, and a check followed immediately by an act is safe. Put an await between the check and the act, even an innocent one such as writing an audit line, and you have exactly the same race as with threads: another task gets a turn holding the same stale answer. The rule is short: look at your critical section and count the awaits in it. None means you are safe; one or more means you need an asyncio.Lock, or you need to move the await out.

What is a race condition, said in a way an interviewer will accept?

A race condition is when the result depends on the order the machine happens to run two pieces of code in, and at least one of those orders gives a wrong answer. The useful follow-up is the one that shows you can find them: name what is shared, name who writes it, and name the stretch of code that has to happen all at once for the answer to be right. That last stretch is the critical section, and a race is what you get when two threads are inside it together.