Week 13 of 16

Read & Experiment: Async in Jupyter

Try async code interactively — including parallel Claude API calls.

Day 62 60 minutes Experiment

Day 62 of 80

Read

Anthropic Docs — Async Usage

Read Anthropic SDK documentation and look for the async client section. It shows how AsyncAnthropic works and when to use it.

Experiment in Jupyter

Jupyter supports await directly in cells — you don't need asyncio.run() in notebooks. In .py files you'll always use asyncio.run().

Cell 1: Your first async function Python
import asyncio

async def make_coffee(name):
    print(f"  Starting {name}...")
    # asyncio.sleep() pauses THIS coroutine without blocking others.
    # Regular time.sleep() would freeze everything.
    await asyncio.sleep(2)
    print(f"  {name} ready!")
    return f"{name} coffee"

# In Jupyter you can await directly. Each runs in sequence — 6 seconds total.
result1 = await make_coffee("Espresso")
result2 = await make_coffee("Latte")
result3 = await make_coffee("Cappuccino")
print(f"Done: {[result1, result2, result3]}")

Running them in sequence like this — await one, await two, await three — still takes 6 seconds. You're not getting any parallelism yet. The next cell fixes that.

Cell 2: Running in parallel with gather() Python
import time

async def make_coffee(name, seconds):
    print(f"  Starting {name}...")
    await asyncio.sleep(seconds)
    print(f"  {name} ready!")
    return f"{name} coffee"

start = time.time()

# asyncio.gather() runs all three AT THE SAME TIME.
# Total time = time of the SLOWEST task, not sum of all tasks.
results = await asyncio.gather(
    make_coffee("Espresso", 2),
    make_coffee("Latte", 3),
    make_coffee("Cappuccino", 1),
)

elapsed = time.time() - start
print(f"\nAll done in {elapsed:.1f}s: {results}")
# Should print: All done in ~3.0s (the slowest task)
# NOT 6.0s (the sum of all tasks)

The key insight: All three started simultaneously. The total time is determined by the slowest task (3 seconds), not the sum of all tasks (6 seconds). That's the performance gain.

Cell 3: Parallel Claude API calls — the real thing Python
import anthropic
from dotenv import load_dotenv
import time

load_dotenv()

# AsyncAnthropic is the async version of the client.
# Same API, but methods return coroutines you need to await.
client = anthropic.AsyncAnthropic()

async def generate_for_platform(shot, platform):
    """Generate a prompt for one platform — async version."""
    message = await client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=150,
        messages=[{
            "role": "user",
            "content": f"Write a {platform} AI video prompt for: {shot}. Under 50 words. Only the prompt."
        }]
    )
    return {"platform": platform, "prompt": message.content[0].text}

shot = "Slow motion golf ball flight through morning mist"
start = time.time()

# Generate for all 3 platforms at once!
results = await asyncio.gather(
    generate_for_platform(shot, "Kling"),
    generate_for_platform(shot, "Runway"),
    generate_for_platform(shot, "Veo"),
)

elapsed = time.time() - start
print(f"Generated in {elapsed:.1f}s\n")

for r in results:
    print(f"[{r['platform']}]")
    print(f"  {r['prompt']}\n")

Run this and note the elapsed time. Compare it to running the same generation three times sequentially. The parallel version should take roughly the same time as one individual call.

Cell 4: Spreading gather() over a dynamic list Python
# When you have many tasks, build the list first, then unpack with *
# The * before the list "unpacks" it into positional arguments.
platforms = ["Kling", "Runway", "Veo"]
shots = [
    "Wide aerial over foggy mountains",
    "Close-up of hands gripping steering wheel",
]

# Every shot × every platform = 6 tasks, all running in parallel
tasks = [
    generate_for_platform(shot, platform)
    for shot in shots
    for platform in platforms
]

print(f"Running {len(tasks)} tasks in parallel...")
start = time.time()

results = await asyncio.gather(*tasks)

elapsed = time.time() - start
print(f"All {len(results)} prompts in {elapsed:.1f}s")

*tasks unpacks the list. asyncio.gather(*tasks) is equivalent to asyncio.gather(tasks[0], tasks[1], tasks[2], ...). You'll use this pattern constantly when the number of tasks isn't known at write time.

End of Day Checklist

Tomorrow

Days 63–64 build the batch_generator.py — a real tool that takes a list of shots and generates prompts for all platforms in one parallel burst.