DEV Community

Dhanush Kumar Sivaji
Dhanush Kumar Sivaji

Posted on Originally published at dhanushkumarsivaji.substack.com

The path to AI FDE #02: Python for AI backends: a TypeScript engineer's refresher

Most AI engineering tutorials assume Python. I’ve written TypeScript for years and have working, not deep, Python. My roadmap starts with a short Python + FastAPI refresher rather than a full course, and this post is the map I’m following through it: eight concepts AI backends lean on, each mapped to TypeScript I already know, plus the spot where my TS instincts are most likely to mislead me.

Python for AI backends: eight TypeScript-to-Python comparisons, each with a gotcha


Each concept gets its own figure first, then the short version in words.

Setup and types: concepts 1–3

One environment per project: two project folders each with their own .venv and different versions of the same library; create, activate, install; warnings about skipping activation and PEP 668 externally managed Pythons

1. Isolated dependencies: node_modules ↔ virtual environments. A virtual environment is a self-contained directory with its own interpreter and packages, so two projects can pin different versions of the same library. Create it with python -m venv .venv, then activate it: activation changes your shell so python points into that environment. The TS instinct that misleads: there’s no automatic per-folder install. Forget to activate, and pip targets whichever Python comes first on your PATH. On Pythons marked “externally managed” (PEP 668), as some OS-provided ones are, pip refuses to install outside a virtual environment and exits with an error.

Types vanish at runtime in both languages: TypeScript types are checked by tsc then erased; Python annotations are checked by mypy or Pyright but not enforced, so f(

2. Type hints are for tools, just like TS types. The TypeScript handbook shows types being erased when tsc compiles to JavaScript. Python’s typing docs say the runtime does not enforce annotations; they exist for type checkers, IDEs and linters. So def f(x: float) -> str happily accepts a string at runtime. Run mypy or Pyright in CI, the way you’d run tsc --noEmit.

Pydantic guards the edges: HTTP request bodies and LLM output pass through a Pydantic model before becoming typed objects; the OpenAI Python SDK turns the model class into a JSON Schema and parses the reply back

3. Runtime validation: Zod ↔ Pydantic. Since annotations aren’t enforced, untrusted data needs a validator. Zod describes itself as TypeScript-first schema validation with static type inference. Pydantic plays the same role in Python: validation is driven by type annotations, and once data passes, the fields are guaranteed to match the declared types. For LLM work, FastAPI uses it to validate requests and responses, and OpenAI’s Python SDK accepts a Pydantic class directly (client.responses.parse(..., text_format=MyModel)): it converts the class to a JSON Schema, sends it and parses the reply back into the model. Only the subset of JSON Schema that structured outputs supports will work.

Errors and cleanup: concepts 4–5

Errors in Python: which of try, except, else and finally run with no error, a matching error or another error; FastAPI's HTTPException is swallowed by a broad except Exception

4. Errors: throw/catch ↔ raise/except. The shape maps closely, plus one extra: else, which runs only if the try block raised nothing. Idiomatic Python catches specific exception classes. In FastAPI, raise HTTPException(...) returns an HTTP error and ends the request, even from deep inside a helper. But FastAPI’s docs call HTTPException a normal Python exception, so a broad except Exception: between the raise and FastAPI swallows it. One more reason to catch narrowly.

Cleanup that always runs: acquire, then whether the block finishes or raises, cleanup runs; TypeScript using with Symbol.dispose next to Python with and async with

5. Guaranteed cleanup: using ↔ with. TypeScript 5.2 added using declarations (explicit resource management). Python has had the with statement for a long time: the cleanup step runs whether the block finishes normally or raises, and async with is the asynchronous version. In AI backends, that’s the home for HTTP clients, open files and database sessions.

Async: concepts 6–8

Starting async work: in TypeScript calling an async function starts it immediately; in Python calling an async def only creates a coroutine until it is awaited; a forgotten await in Python means the work never runs and asyncio warns

6. Starting async work: the subtle one. The syntax looks the same; what happens at call time doesn’t. MDN: an async function runs synchronously up to its first await, so calling fetchDoc() starts the work immediately. Python’s asyncio docs: simply calling a coroutine will not schedule it. Nothing runs until you await it or wrap it in a task, so a forgotten await means the work never happens. asyncio emits a RuntimeWarning (”coroutine ... was never awaited”); treat it as a bug.

One blocking call stalls everything: a timeline where time.sleep(1) on the event loop delays three ready requests; fixes for blocking I/O, CPU-bound work and FastAPI

7. Blocking the event loop: same trap, both runtimes. Node’s docs warn against blocking the event loop. Python’s asyncio docs say a CPU-intensive calculation that takes 1 second delays every concurrent task by 1 second, and blocking I/O (time.sleep(), a synchronous HTTP client inside async def) does the same. The fix depends on the work: for blocking I/O, an awaitable client or asyncio.to_thread(), which the docs say is mainly for I/O-bound work because the GIL usually stops threads speeding up CPU-bound Python; for CPU-bound work, a process pool via loop.run_in_executor(ProcessPoolExecutor(), fn); in FastAPI, a plain def endpoint runs in a threadpool instead of on the loop.

Fan-out when one call fails: Promise.all and asyncio.gather let the other calls keep running, gather with return_exceptions returns results and errors like Promise.allSettled, and TaskGroup cancels the rest

8. Fan-out: Promise.all ↔ asyncio.gather / TaskGroup. You’ll want this for parallel calls, like embedding several chunks or querying two retrievers. Promise.all rejects with the first rejection but doesn’t stop the other operations: MDN notes promises have no built-in cancellation (you’d reach for AbortController). asyncio.gather behaves the same by default, and with return_exceptions=True it collects every result and error, much like Promise.allSettled. asyncio.TaskGroup (Python 3.11+) is the real difference: when one task fails, the rest of the group is cancelled, and the failures are raised together as an ExceptionGroup. That’s usually what you want when partial results are useless.


Takeaway

Coming from TypeScript, the biggest shift isn’t syntax. It’s where the guarantees live. In both languages, types vanish at runtime. Real safety comes from validating at the edges (Pydantic, the way Zod does on the front end), closing resources deterministically (with) and respecting the event loop. Points 6 and 8 are the two I expect to trip over most, so they go on my checklist.

Where this fits in my roadmap: the foundations block (Python typing, errors, environments and async, plus the FastAPI tutorial’s request/response models, errors, dependencies and testing), building toward a small typed LLM API. Next post: structured outputs, from “please return JSON” to contracts.

Over to you: if you moved from TypeScript to Python, which habit took longest to unlearn?


Sources

  1. Python Tutorial: Virtual Environments and Packages. https://docs.python.org/3/tutorial/venv.html
  2. PEP 668: Marking Python base environments as “externally managed”. https://peps.python.org/pep-0668/
  3. TypeScript Handbook: The Basics, “Erased Types”. https://www.typescriptlang.org/docs/handbook/2/basic-types.html
  4. Python docs: typing (”The Python runtime does not enforce function and variable type annotations”). https://docs.python.org/3/library/typing.html
  5. mypy documentation (”a static type checker for Python”). https://mypy.readthedocs.io/en/stable/
  6. Pyright (”a full-featured, standards-based static type checker for Python”). https://github.com/microsoft/pyright
  7. Zod documentation (TypeScript-first schema validation).

https://zod.dev/

  1. Pydantic: Why use Pydantic (type-hint-driven validation, JSON Schema). https://pydantic.dev/docs/validation/latest/get-started/why/
  2. Pydantic: Models (post-validation guarantees). https://pydantic.dev/docs/validation/latest/concepts/models/
  3. OpenAI: Structured model outputs (pydantic.BaseModel schemas, text_format, supported JSON Schema subset). https://developers.openai.com/api/docs/guides/structured-outputs
  4. openai-python helpers.md (.parse() converts a Pydantic model to JSON Schema and parses the reply; output_parsed). https://github.com/openai/openai-python/blob/main/helpers.md
  5. Python Tutorial: Errors and Exceptions (else, finally). https://docs.python.org/3/tutorial/errors.html
  6. FastAPI: Handling Errors (HTTPException is a normal Python exception; raising it from a helper ends the request). https://fastapi.tiangolo.com/tutorial/handling-errors/
  7. TypeScript 5.2 release notes: using declarations and explicit resource management. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html
  8. Python Language Reference: the with statement. https://docs.python.org/3/reference/compound_stmts.html#the-with-statement
  9. MDN: async function (runs synchronously up to the first await). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
  10. Python docs: Coroutines and Tasks (calling a coroutine doesn’t schedule it; create_task; gather; TaskGroup and ExceptionGroup; to_thread and the GIL). https://docs.python.org/3/library/asyncio-task.html
  11. Python docs: Developing with asyncio (”Running Blocking Code”; “Detect never-awaited coroutines”). https://docs.python.org/3/library/asyncio-dev.html
  12. Python docs: Event loop, run_in_executor with ProcessPoolExecutor. https://docs.python.org/3/library/asyncio-eventloop.html
  13. Node.js: Don’t Block the Event Loop (or the Worker Pool). https://nodejs.org/learn/asynchronous-work/dont-block-the-event-loop
  14. FastAPI: Concurrency and async / await (plain def path operations run in a threadpool). https://fastapi.tiangolo.com/async/
  15. MDN: Promise.all(). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
  16. MDN: Promise (no first-class cancellation; use AbortController). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  17. MDN: Promise.allSettled(). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

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

Top comments (0)