ai newsMi., 21. Okt.
EilmeldungI Read All 3 Physical AI Toolchain Launches So You Don't Have To

The path to AI FDE #03: Python types you can trust, and async waiting done well

Issue #02 was the quick TypeScript-to-Python map. This issue goes one level deeper on the two ideas every AI backend leans on: types (what data is allowed in) and async (how one process waits on many slow network calls). Twelve short points, each with the trap I’d fall into first. Part A · Types yo

dev.toMittwoch 19:16 · 21.10.4 Min. Lesezeit
Bild: dev.to

Posted on Oct 21 • Originally published at dhanushkumarsivaji.substack.com

Issue #02 was the quick TypeScript-to-Python map. This issue goes one level deeper on the two ideas every AI backend leans on: types (what data is allowed in) and async (how one process waits on many slow network calls). Twelve short points, each with the trap I’d fall into first.

1. Hints describe intent. def count_words(text: str) -> int tells readers, editors and type checkers what goes in and what comes out. Collections take their item types in brackets: list[str], dict[str, int].

2. “Maybe missing” is spelled X | None. Since Python 3.10 (PEP 604) you write str | None instead of Optional[str]. Trap: in Pydantic v2, subtitle: "str | None with no default is still a required field. Add = None if callers may leave it out."

3. Checked before running, not while running. As covered in #02, Python ignores hints at runtime, so count_words(42) runs anyway and only mypy or Pyright flags it. Run one of them in CI.

4. Pydantic models check data when it arrives. A model is a class that inherits from BaseModel, with typed fields, and creating an instance runs validation. Field() adds constraints such as gt=0 or max_length=200. If no error is raised, the fields are guaranteed to match their types.

5. Lax by default: know what it lets through. Pydantic coerces when it can ( pages="412" becomes the integer 412 ) and silently ignores extra keys. That’s handy for query strings and environment variables, but risky for data from a model or another service. For those, opt into strict=True and extra='forbid'.

6. One error lists every problem, and a model can describe itself. A ValidationError reports all failing fields with their locations (such as tags.2 ), not just the first. Book.model_validate_json(raw) validates a JSON string directly, and Book.model_json_schema() exports a JSON Schema, which is how structured outputs get their contract (coming later in the series).

7. AI backends mostly wait. An LLM call spends most of its time waiting on someone else’s server, not computing on yours. Async lets one process overlap those waits instead of standing idle through each one.

8. The event loop runs one task at a time. asyncio uses cooperative scheduling: whenever the running task hits an await on something not yet ready, the loop switches to another. Nothing is truly simultaneous; the overlap comes only from the waiting.

9. async def makes a coroutine, and asyncio.run() starts the loop. asyncio.run(main()) is the usual entry point in a script. Calling main() on its own only creates a coroutine object, which does nothing until awaited (#02, point 6). In FastAPI, the framework runs the loop for you.

10. Back-to-back awaits are still sequential. This is the trap I’d hit first. In the asyncio docs’ example, await say_after(1,...) then await say_after(2,...) takes 3 seconds: async syntax alone doesn’t make anything concurrent.

11. Tasks are how work overlaps. asyncio.create_task(coro) schedules the coroutine to run soon, alongside the current one, which brings the same example down to about 2 seconds. Keep a reference to every task: the loop holds tasks only weakly, so an unreferenced task can be garbage-collected before it finishes. TaskGroup handles that for you (next issue).

12. One blocking call stalls everyone. time.sleep(1) or a synchronous HTTP client inside async def freezes the whole loop, while await asyncio.sleep(1) always suspends just the current task. For blocking I/O you can’t avoid, await asyncio.to_thread(fn) moves it off the loop.

Write async def nap(n) that does await asyncio.sleep(n). Time await nap(1); await nap(2), then the same two calls wrapped in create_task. Then swap in time.sleep and watch the overlap vanish. Separately, define a Book model with title: str and pages: int = Field(gt=0), and feed it one good dict and one bad dict.

Hints describe, Pydantic enforces. await waits; tasks overlap. That’s the whole issue in two lines.

Where this fits in my roadmap: these are the concepts behind the foundations block of my roadmap (Python typing, errors and async; FastAPI and Pydantic models). That block’s exercises (a typed document loader, a small validated LLM endpoint and its tests) are mine to build, so nothing above is that code; the examples are deliberately toy ones. Next issue: running many calls at once safely ( gather, TaskGroup, timeouts) and FastAPI beyond hello-world.

Over to you: which bit you first: a missing await, or an await that should have been a task?

Originally published on The Path to AI FDE on Substack. Subscribe for the next issue: https://dhanushkumarsivaji.substack.com/subscribe

Quelle: dev.to (dev.to). Original öffnen