Let’s slow down and do this properly and clearly.
No images, no fluff — deep internals, step-by-step.
async / await Works Internally (Python – Deep Dive)This explanation is how Python actually executes async code, not just how to use it.
async/awaitis NOT magic, NOT threads, NOT parallelism.
It is:
State machines
Generators
Event loop scheduling
Cooperative multitasking
Python rewrites your async code into a controlled pause–resume machine.
async def REALLY CREATESasync def foo():
return 42
This is a function.
This creates a coroutine object, NOT a function call.
coro = foo()
print(type(coro))
# <class 'coroutine'>
⚠️ Nothing runs yet.
Internally, a coroutine behaves like a generator.
def gen():
yield 1
yield 2
async def coro():
await something()
Behind the scenes:
await ≈ yield control back to event loop
Python converts async def into a state machine
Each await = checkpoint
await (CRITICAL)await asyncio.sleep(1)
Coroutine pauses execution
It yields:
“I’m waiting for this future”
Event loop:
Registers callback
Suspends coroutine
Event loop switches to another task
👉 No blocking
👉 No sleeping
👉 No thread
You can only await things that are awaitable.
Coroutine
Task
Future
All of them implement:
__await__()
Example:
future.__await__()
This returns an iterator that the event loop drives.
The event loop runs something like this:
while True:
ready_tasks = get_ready_tasks()
for task in ready_tasks:
run_until_next_await(task)
Resume coroutine
Execute until:
It finishes OR
Hits await
asyncio.Task)When you do:
task = asyncio.create_task(coro())
Internally:
Task wraps coroutine
Registers it with event loop
Tracks state
PENDING
RUNNING
SUSPENDED (awaiting)
DONE
Task = coroutine + scheduler metadata
A Future represents:
“A value that will exist later.”
future = loop.create_future()
Coroutine waits on Future
Event loop sets result later
Coroutine resumes
This is how:
Network I/O
Timers
File descriptors
work internally.
asyncio.sleep() Internals (Example)await asyncio.sleep(1)
Internally:
Create a Future
Register timer with event loop
Suspend coroutine
After 1 second:
Event loop sets future result
Coroutine resumes
👉 No sleeping thread
👉 No CPU waste
async def bad():
time.sleep(2) # ❌
What happens:
Event loop cannot switch tasks
Whole program freezes
Because:
Blocking code never yields control.
Async only works if you cooperate.
await asyncio.gather(a(), b(), c())
Execution flow:
a starts → awaits
b starts → awaits
c starts → awaits
event loop resumes whichever is ready
⚠️ Only one coroutine runs at a time
⚠️ Switching happens only at await
Python compiles async functions into:
GET_AWAITABLE
SEND
YIELD_VALUE
Each await becomes:
Save state
Yield to loop
Resume later
This is why async is fast and lightweight.
Think of async as:
A single-threaded CPU running many paused programs, switching only when they agree to pause.
| Aspect | async/await | Threads |
|---|---|---|
| Context switch | Explicit (await) | OS-driven |
| Overhead | Very low | High |
| Safety | Deterministic | Race conditions |
| CPU-bound | ❌ | ⚠️ |
| I/O-bound | ✅ Excellent | ⚠️ |
async/awaitis a structured way to pause execution, hand control to an event loop, and resume later without blocking.
Internally, async/await works by transforming coroutines into state machines that cooperatively yield control to an event loop, which schedules and resumes them based on I/O readiness.