Fastapi Module3
Fastapi Module3
Contents
Code Files in This Module 2
4 Asynchronous Programming 15
4.1 What Is Asynchronous Programming? . . . . . . . . . . . . . . . . . . . . . . . . 15
4.2 Synchronous vs Asynchronous Visual . . . . . . . . . . . . . . . . . . . . . . . 15
4.3 Sync vs Async By Operation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.4 The Sync vs Async Demo Measured . . . . . . . . . . . . . . . . . . . . . . . . 15
4.4.1 Synchronous Version [Link] . . . . . . . . . . . . . . . . . . . . 16
4.4.2 Asynchronous Version [Link] . . . . . . . . . . . . . . . . . . 16
4.4.3 Decoding the Async Mechanics . . . . . . . . . . . . . . . . . . . . . . . . 17
4.5 async and await The Keywords . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.6 Internal Working The Event Loop . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.7 Blocking vs Non-Blocking Sleep . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.8 From Demos to FastAPI async_main.py . . . . . . . . . . . . . . . . . . . . . 18
4.9 When to Use async def vs def in FastAPI . . . . . . . . . . . . . . . . . . . . . 19
4.10 A Concrete Async Win Calling Two APIs in Parallel . . . . . . . . . . . . . . 20
1
FastAPI Module 3 CONTENTS
[Link] [Link]
4 Asynchronous programming
Read the diagram this way: the solid teal arrow is a real Python import statement ([Link]
has from models_val import Employee); the dashed grey arrows show conceptual evolution
(one le is the next version of the previous one); the orange dashed double-arrow shows two les
meant to be compared side-by-side.
2
FastAPI Module 3 1 CREATING APIS USING FASTAPI
Component Role
Deep Dive
Why path operation function and not just handler? In FastAPI's vocabulary,
the term emphasises that one function corresponds to one path + one method. The same
path /items/{id} can have several operations : GET (read), PUT (replace), DELETE (remove)
each a separate path operation function. This vocabulary maps directly onto the OpenAPI
specication, where each (path, method) pair is called an operation.
3
FastAPI Module 3 1 CREATING APIS USING FASTAPI
The simplest case: return a Python dict, and FastAPI calls [Link] on it (technically,
jsonable_encoder rst, which handles dates, UUIDs, Pydantic objects, etc.).
This returns:
1 { " status " : " ok " , " uptime_seconds " : 142}
Returning a Pydantic instance gives you the same JSON output plus automatic validation that
the response matches your declared schema. This is the recommended pattern.
2. Validates that the returned object matches the response_model (here, the same class, so it
always passes).
4
FastAPI Module 3 1 CREATING APIS USING FASTAPI
1.3 Returning Pydantic Models What FastAPI Does Under the Hood
When you write the route in [Link] withresponse_model=User, you might think
the keyword is just decoration. In fact, the decorator is syntactic sugar for a more verbose
registration call.
1. Inserts /user into Starlette's URL router with the methods list ["GET"].
2. Registers get_user as the callable to invoke.
3. Sets User as the response model, which (a) lters the returned dict so only elds declared on
User survive, (b) re-validates the output, (c) generates the JSON schema entry in OpenAPI.
4. Adds the route to the OpenAPI 3.1 spec at /[Link].
Note
5
FastAPI Module 3 2 CRUD OPERATIONS EMPLOYEES API
Deep Dive
Idempotent means: calling the same request multiple times produces the same server
state as calling it once. DELETE /items/5 called ve times leaves the same end state as one
call the item is gone. POST /items called ve times creates ve dierent items, which
is why it is not idempotent. This matters in production: clients (and CDNs, and proxies)
may retry idempotent requests automatically on network failure; they will refuse to retry
POSTs to avoid duplicate creation.
6
FastAPI Module 3 2 CRUD OPERATIONS EMPLOYEES API
models_val.py denes the Employee Pydantic schema. Pure data, no routes, no FastAPI.
1. Other modules (tests, background workers, CLI scripts) can import Employee without drag-
ging in FastAPI.
But it doesn't catch any of these bad inputs: id = -5, name = "X", age = 5. Those would all
pass. We need explicit constraints, which leads us to the validated version.
7
FastAPI Module 3 2 CRUD OPERATIONS EMPLOYEES API
Deep Dive
Why StrictInt only on age, and not on id? Because id is more often passed as a string
from URL paths or form data, where coercion is helpful. age is part of the JSON body,
where a client sending "21" as a string usually indicates a frontend bug worth surfacing.
This is a real design trade-o coercion is convenience, strictness is correctness, and you
choose per eld.
8
FastAPI Module 3 2 CRUD OPERATIONS EMPLOYEES API
23
24
25 # 3. Add an employee
26 @app . post ( '/ add_employee ' , response_model = Employee )
27 def add_employee ( new_emp : Employee ) :
28 for employee in employees_db :
29 if employee . id == new_emp . id :
30 raise HTTPException ( status_code =400 , detail = ' Employee already
exists ')
31 employees_db . append ( new_emp )
32 return new_emp
33
34
35 # 4. Update an employee
36 @app . put ( '/ update_employee /{ emp_id } ' , response_model = Employee )
37 def update_employee ( emp_id : int , updated_employee : Employee ) :
38 for index , employee in enumerate ( employees_db ) :
39 if employee . id == emp_id :
40 employees_db [ index ] = updated_employee
41 return updated_employee
42 raise HTTPException ( status_code =404 , detail = ' Employee Not Found ')
43
44
45 # 5. Delete an employee
46 @app . delete ( '/ delete_employee /{ emp_id } ')
47 def delete_employee ( emp_id : int ) :
48 for index , employee in enumerate ( employees_db ) :
49 if employee . id == emp_id :
50 del employees_db [ index ]
51 return { ' message ': ' Employee deleted successfully '}
52 raise HTTPException ( status_code =404 , detail = ' Employee Not Found ')
9
FastAPI Module 3 2 CRUD OPERATIONS EMPLOYEES API
If you send invalid data, the constraint failure is reported before the function even runs:
Note
A note on path style. Your URLs use action verbs in the path: /add_employee,
/update_employee/{id}, /delete_employee/{id}. This works perfectly but is not strictly
RESTful. The REST convention is to use the same resource path (/employees and
/employees/{id}) for all ve operations and let the HTTP method (POST, PUT, DELETE)
communicate the action. Both styles are common in industry; the verb-in-path style is
10
FastAPI Module 3 3 HANDLING VALIDATIONS AND ERRORS
sometimes preferred when generating client SDKs that map URLs to function names. For
your learning, recognise that the two styles exist and pick consciously.
Deep Dive
11
FastAPI Module 3 3 HANDLING VALIDATIONS AND ERRORS
Parameter Description
Note
In Pydantic v2 the parameter is named pattern, not regex. The older regex keyword
is deprecated; many tutorials still show it. If you upgrade and see a deprecation warning,
change regex= to pattern=.
Strict Types
By default, Pydantic is coercive: a string "5" for an int eld is silently converted to 5. Some-
times you want strict behavior reject anything that is not literally the right type.
Deep Dive
When to use Strict types. In nancial or scientic APIs where confusing 1 (int) with
1.0 (oat) could change the meaning e.g., share counts vs share prices strict types make
the contract explicit. For general web APIs, the default coercion is usually what you want.
Back to models_val.py: this is exactly why age uses Optional[StrictInt] if a client sends
"age": "25" (a string), Pydantic refuses to silently coerce it. A string age is almost always a
frontend bug, and StrictInt surfaces it as a validation error instead of letting it slip through.
12
FastAPI Module 3 3 HANDLING VALIDATIONS AND ERRORS
The semantic dierence between Optional[str] = None and str | None is purely syntactic
both mean a string or None. Use the syntax your team prefers; both are accepted by Pydantic.
13
FastAPI Module 3 3 HANDLING VALIDATIONS AND ERRORS
If a client sends {"username": "ab", "age": 12, "password": "123"}, the response is
one 422 with all three errors listed:
1 {
2 " detail " : [
3 { " loc " : [ " body " ," username " ] , " msg " : " String should have at least 3
characters " , ...} ,
4 { " loc " : [ " body " ," age " ] , " msg " : " Input should be >= 13 " , ...} ,
14
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
5 { " loc " : [ " body " ," password " ] , " msg " : " String should have at least 8
characters " , ...}
6 ]
7 }
Notice: the client gets every error in one round trip. They don't have to x one, retry, x the
next, retry. This single design choice saves your frontend team enormous pain.
4 Asynchronous Programming
4.1 What Is Asynchronous Programming?
Asynchronous programming is a paradigm that lets your program perform other tasks while
waiting for a slow operation (a database query, an HTTP call, a le read) to complete, without
blocking the rest of the code.
In synchronous code, when you call [Link](...), the entire program freezes for the 100 ms it
takes the database to answer. In asynchronous code, you say await [Link](...) which
means pause this function here, do other work, and come back when the answer is ready.
time
Async: total ≈ longest single wait Sync: ∼total = sum of all waits
In the synchronous timeline, Request B cannot even start until Request A is done. In the
asynchronous timeline, both requests start almost immediately and their database waits overlap.
The total wall-clock time is roughly the longest single wait, not the sum.
API call Waits for response before Sends request, does other work,
proceeding returns later
Database query Blocks until data is fetched Queries DB, resumes when data
arrives
File read Waits for disk I/O to com- Reads in background while other
plete code runs
15
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
Total =2+1+3=6 seconds. Each task blocks until the previous one nishes. [Link] is
blocking
the canonical call the entire interpreter freezes during it.
16
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
20
21 asyncio . run ( main () )
Observed output:
1 Task 1 started at : 0.00
2 Task 2 started at : 0.00
3 Task 3 started at : 0.00
4 Task 2 completed at : 1.00
5 Task 1 completed at : 2.00
6 Task 3 completed at : 3.00
7
8 Total time taken : 3.00 s
Total = max(2, 1, 3) = 3 seconds exactly half the synchronous version. Look at the start
times: all three tasks start at 0.00. This is the smoking gun of concurrency. They are not
running on three threads; they are running on a single thread that suspends at each await
[Link](...) and lets the others run.
1. async def run_task declares a coroutine. Calling run_task('Task 1', 2) does not
execute the body. It returns a coroutine object.
2. [Link](...) takes those coroutine objects and submits them all to the event
loop as tasks to run concurrently.
3. await [Link](...) waits until all submitted tasks complete, then returns their
results.
4. [Link](main()) creates a new event loop, runs main() to completion, and closes the
loop.
Note
async def declares an asynchronous function (called a coroutine). Calling it does not run
it; it returns a coroutine object that must be awaited or scheduled.
await pauses execution of the current coroutine until the awaited operation completes, and
yields control back to the event loop so other tasks can run in the meantime.
17
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
5. When the awaited operation nishes, the loop resumes the original task from where it paused.
Task 1
Task 1 yield
result ready
awaiting DB
resume
Task 3 yield
awaiting HTTP
The crucial insight: there is only one thread. Concurrency comes not from threads or
processes, but from cooperative yielding at every await. This means async code is free of the
classic locking/race-condition headaches of multithreading but it also means a single CPU-
heavy task will freeze the entire server until it yields.
Why this matters: run this server, then open two browser tabs to [Link]
wait at roughly the same time. Both responses arrive at ≈ 3 s, not 6 s. The second request
didn't wait for the rst to nish the event loop simply ran them concurrently. If you replaced
18
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
await [Link](3) with [Link](3), both requests would take 6s total instead of
3 s. This is precisely the warning in the next subsection.
Note
No [Link] here. In the demo scripts you called [Link](main()) to start the
event loop. In async_main.py there is no such call because Uvicorn already runs the
event loop. You only declare async def and FastAPI plugs it in.
Listing 18: Three sleep patterns side by side the right one, the wrong one, the alternative
1 import time , asyncio
2 from fastapi import FastAPI
3
4 app = FastAPI ()
5
6 @app . get ( " / sync - block ")
7 def sync_block () :
8 time . sleep (3) # OK : runs in a thread pool
9 return { " finished " : True }
10
11 @app . get ( " / async - good ")
12 async def async_good () :
13 await asyncio . sleep (3) # CORRECT : yields to the event loop
14 return { " finished " : True }
15
16 @app . get ( " / async - bad " )
17 async def async_bad () :
18 time . sleep (3) # BUG : blocks the entire event loop !
19 return { " finished " : True } # nothing else can run for 3 s
Important
The async-bad pattern is the single most common async mistake. The function is declared
async, but it calls a synchronous blocking library ([Link], [Link], psycopg2).
Because async functions run on the event loop, blocking them blocks every other request.
The whole server stops responding for 3 seconds. Rule: if your function is async, every
blocking call inside it must be awaited.
async def runs directly on the event loop. Fast, but you must not call blocking libraries.
def run in a thread pool (managed by Starlette). Slightly more overhead per request, but
safe to call blocking libraries.
19
FastAPI Module 3 4 ASYNCHRONOUS PROGRAMMING
You are calling a blocking library (e.g. requests, psycopg2, sync SDKs).
You don't have time to rewrite to async and the endpoint isn't on the hot path.
Deep Dive
The simple rule. If you can write the whole function body using libraries that sup-
port await, use async def. Otherwise, write a plain def. Never mix async def with
synchronous blocking calls that combination has the worst of both worlds: thread-pool
unavailable, event-loop blocked.
20
FastAPI Module 3 5 SUMMARY ONE-PAGE MENTAL MODEL
If each call takes 4 seconds, the sync version takes 8 seconds. The async version takes 4. That
doubling of throughput compounds as you add more parallel calls which is why every modern
LLM-powered backend is async.
Return types: dict (auto-JSON), Pydantic model (recommended, validates output), or cus-
tom Response (HTML, streaming, etc.).
HTTP verbs: GET (read, idempotent), POST (create), PUT (replace), PATCH (partial
update), DELETE (remove, idempotent).
CRUD pattern: list / get one / create / update / delete ve endpoints per resource, with
separate ResourceIn and ResourceOut models.
Three validation layers: type hint → Field() constraints → @field_validator for cross-
eld logic. All errors are collected and returned together.
HTTPException is how you abort with a custom status code and message.
Async: async def runs on the event loop use with async I/O libraries. def runs in a thread
pool safe for blocking code. Never mix async def with blocking calls.
Async wins big when calling multiple slow services in parallel the dening workload of
modern AI backends.
21