跳到正文
DEV Community· Dhanush Kumar Sivaji·· 2 小時前AI 評分62

AI FDE 學習路線 #03:可信的 Python 型別與妥善處理非同步等待

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

AI 導讀

《AI FDE 路線》第 03 期講解 Python 型別提示、Pydantic 資料驗證,以及 asyncio 非同步等待的實作要點。型別提示不會在執行時驗證輸入,Pydantic 可在建立模型時檢查資料;對模型或其他服務傳入的資料,可設定 strict=True 和 extra='forbid'。

正文

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.

Two panels. Part A, types you can trust: six cards from type hints to Pydantic errors and JSON Schema. Part B, waiting done well: six cards from the event loop to not blocking it


Part A · Types you can trust

Type hints: the parts of def count_words(text: str) -> int, collection hints like list[str], str | None as the Python 3.10 spelling of Optional[str], and count_words(42) running anyway unless mypy or Pyright flags it

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.

Pydantic models check data when it arrives: incoming data passes through a toy Book model with Field constraints and becomes a Book or a ValidationError; a table shows str | None without a default is still required

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.

Lax by default: the same input with pages as the string

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'.

Errors that list everything: one ValidationError reports title, pages and tags.2 at once; model_json_schema exports the model as a JSON Schema with exclusiveMinimum for gt=0

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).

Part B · Waiting done well

Waiting, overlapped: one LLM call is mostly waiting on the provider; an event loop runs tasks A, B and C one at a time, switching whenever a task awaits a reply

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.

await waits, tasks overlap: the asyncio docs' say_after example takes 3 seconds with two back-to-back awaits and about 2 seconds with two tasks

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).

One blocking call stalls everyone: two tasks finish in 2 seconds with await asyncio.sleep but take 3 seconds with time.sleep; asyncio.to_thread moves unavoidable blocking I/O off the loop

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.


Try it (15 minutes, toy example)

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.

Common beginner mistakes

  • Assuming hints validate input. Only Pydantic (or your own checks) does that.
  • Writing x: str | None and expecting it to be optional in Pydantic v2. It needs = None.
  • Thinking async def plus await equals parallel. It doesn’t without tasks.
  • Creating tasks and dropping the reference.

Takeaway

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?


Sources

  1. Python docs: typing (annotations aren’t enforced at runtime). https://docs.python.org/3/library/typing.html
  2. PEP 604: Allow writing union types as X | Y (Python 3.10). https://peps.python.org/pep-0604/
  3. mypy documentation. https://mypy.readthedocs.io/en/stable/
  4. Pyright. https://github.com/microsoft/pyright
  5. Pydantic: Models (validation on instantiation, guarantees, coercion, extra data ignored by default, ValidationError lists all errors, model_validate_json, model_json_schema). https://pydantic.dev/docs/validation/latest/concepts/models/
  6. Pydantic: Fields (Field() constraints such as gt and max_length; v2 no longer gives Optional an implicit None default). https://pydantic.dev/docs/validation/latest/concepts/fields/
  7. Pydantic: Strict Mode (default coercion of '123' to 123; enabling strict mode). https://pydantic.dev/docs/validation/latest/concepts/strict_mode/
  8. Python docs: Coroutines and Tasks (calling a coroutine doesn’t schedule it; asyncio.run; the 3-second vs 2-second example; create_task; keep task references; sleep always suspends; to_thread; cooperative scheduling). https://docs.python.org/3/library/asyncio-task.html
  9. Python docs: Developing with asyncio (”Running Blocking Code”). https://docs.python.org/3/library/asyncio-dev.html

Sources checked Oct 18, 2026.


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

來源:DEV Community · dev.to