TypeScript 會 erase types。Python 從未要求過 types。 Duck typing 是 default:只要它有 id 與 email,它就走得通。Compiler 不在門口等。一支帶 Pydantic body 的 FastAPI route,跟 Zod-validated Hono route 是同一直覺 —— check 搬到 HTTP boundary,而不是 language。
這篇 note 是以 Hono developer 身分讀 FastAPI API 時用的 mapping。Samples 對準 Python 3.12+:type parameter syntax、match、dataclasses。Pydantic v2 與 SQLAlchemy 2 各出現一次,作為 Zod 與 Drizzle 的表親。不是把這個 site 重寫成 Python。Typed Hono stack 見 用 Hono、Drizzle、Zod OpenAPI 與 SST 打造 Backend APIs。Types 真的活過 compilation 的 Java 兄弟篇是 以 TypeScript Developer 身分學 Java。你已有的 object model 見 TypeScript Class 與 Runtime Identity。
TS / Node: .ts → tsc/bun erase types → V8, one event loop
Py / CPython: .py → CPython bytecode → CPython, GIL + optional asyncio loop四種 statements:
- 一份 TypeScript contract,例如 runtime 會消失的 interface,或沒有 type 的
throw。 - 一條 Python language rule,例如 duck typing,或
x: str | None。 - 一個 CPython observation,例如 GIL,或
asyncioloop。Application code 不該依賴超出那個範圍的 interpreter internals。 - 一個 FastAPI convention,例如 decorated route 與
BaseModelbody。
1. What This Note Is
TypeScript engineer 已經有對的 abstractions:request 進、JSON 出、validated body、打 Postgres 的 query、一份 session。Python 把每一項 remap 到不同的 runtime。
- Types 是 annotations,不是一道牆。
def f(x: int)不會拒絕 string。mypy/pyright可以。Pydantic 可以在 edge 做到。 - Code 的單位是 module。 一個
.pyfile 已經是 namespace。沒有 one-class-per-file 規則,也沒有exportkeyword。 - Host 是 CPython,不是 V8,也不是 JVM。GIL 意味著一次只有一條 thread 跑 bytecode。
asyncio是你已經認識的 event loop。 - FastAPI 是 router 加上 validation。 Hono 是你用 Zod 組起來的 router。FastAPI decorate functions,並請 Pydantic parse body。
Throughline 是一個小的 users resource:依 id fetch、list、create。夠用來讀 router、model,與 session。
2. Duck Typing vs Structural Types
TypeScript 在 compile time 是 structural。Python 在 runtime 是 duck。兩邊都讓一個 plain object 通過。只有 Python 會在凌晨兩點仍然呼叫它。
interface UserLike {
id: string
email: string
}
function greet(u: UserLike): string {
return u.email
}def greet(u) -> str:
return u.email # anything with .email workstyping.Protocol是 TypeScriptinterface。它是 shape,不是 base class。mypy會強制;CPython 不會。isinstance檢查的是 class。它不證明 shape。一個 keys 正確的dict仍然會失敗。- 沒有
instanceof習慣,因為 objects 通常不是靠new來證明 identity。它們因為會 quack 而被傳遞。
Failure: ship 一個呼叫 u.email 的 greet(u),然後指望 type hint 救了你。它沒有。Parse input,或 annotate Protocol 並跑 checker。
3. None, Not Undefined
TypeScript 有 null 與 undefined。Python 只有一種 empty:None。
function emailOf(user: User | null): string | undefined {
return user?.email
}def email_of(user: User | None) -> str | None:
return user.email if user is not None else Nonex: str | None是 annotation。它是 documentation 加一條 mypy 規則。CPython 仍然接受任何東西。- Pydantic 把那個 annotation 變成真正的 check。
BaseModel上的email: str | None意味著缺 key 是 error,而 JSONnull變成None。 - 沒有 truthiness 捷徑。
if not user會吞掉""、0、[],以及None。Checkis None或is not None。
Failure: 當空字串是合法、已驗證的 email 時寫 if not user.email。空字串不是 None。
4. Modules, Functions, Dataclasses, Classes
在 Java 裡 class 是單位。在 Python 裡 module 是。一個 file 是 namespace。Functions 是可以傳遞的 values。
# users/models.py
from dataclasses import dataclass
@dataclass
class User:
id: str
email: str# users/service.py
from users.models import User
def require(users, id: str) -> User:
user = users.find_by_id(id)
if user is None:
raise LookupError(id)
return user@dataclass是 Javarecord。它給你__init__、__eq__、__repr__。用在 values。它不是 Pydantic model;它不 validate。- Classes 存在。
__init__是 constructor,self是 explicit。你比在 Java 裡更少需要它們。一個 module 的 functions 就是正常的 service。 - 一切預設都是
publicby convention。 開頭底線(_private)是標誌,不是鎖。Compiler 不會阻止 import_private。
Failure: 寫一個只有一個 method、收 self、再加上一整個 module imports 的 class UserService:。那是多幾道手續的 module。
5. Type Hints Are Not the Compiler
TypeScript 的 types 會被 erase,但你寫的時候有強制。Python 的 annotations 在 runtime 是 data。Interpreter 把它們讀進 __annotations__,然後繼續走。
def add(a: int, b: int) -> int:
return a + b
add("1", "2") # returns "12"; no error- 跑
mypy或pyright,hints 才有意義。它們是獨立工具。CI 跑它們;CPython 不跑。 - Pydantic 是 runtime 對應。
BaseModelparse 並 coerce。model_validate在錯的 type 上 raise。那是 Zod。 Protocol、dataclass、BaseModel是三種不同的答案。按 boundary 選:internal code 用 shape,value 用 dataclass,HTTP edge 用 Pydantic。
Failure: 因為 annotation 存在,就以為 def create_user(body: UserCreate) 會拒絕壞 body。沒有 Pydantic,那只是 comment。
6. Collections
Python 內建 list、dict、set、tuple。JSON object 到達時是 dict,不是 typed record,直到你 parse 它。
const ids = users.map((u) => u.id).filter(Boolean)ids = [u.id for u in users if u.id]- Comprehensions 是
map/filter習慣。List comprehension 建出 list。Generator expression(u.id for u in users)保持 lazy。 - Mutation 是 default。
list.append會改它。tuple是 frozen array。frozenset是 frozen set。 - Iteration order:
dict自 3.7 起保留 insertion。set不保留。從json.loads來的一串dictrows 不是 typed API。
Failure: 因為「已經是 JSON 形狀」就把 [dict(row) for row in rows] 當 response 回傳。那是 Java note 裡的 HashMap 錯誤。Parse 成 model。
7. Exceptions
TypeScript 的 throw 沒有 type。Python 的 raise 也沒有 type。沒有 checked exceptions。風格是 EAFP —— easier to ask forgiveness than permission —— 對上 LBYL —— look before you leap。
def require(users, id: str) -> User:
user = users.find_by_id(id)
if user is None:
raise LookupError(id)
return usertry:
user = require(users, id)
except LookupError:
raise HTTPException(status_code=404, detail="user not found")- Catch specific exception。
except Exception:是 Java 的throws Exception。它藏起你真正造成的 bug。 - Miss 的 function 會 raise。想要 404 的 route 在 boundary catch。不要同一個 miss 又回
None又 raise。 HTTPException是 FastAPI 的 typed exit。它不是 language feature。它是 framework 把 exception map 成 status。
Failure: 在 database call 外面包裸 except:,然後回 500。Connection error 與 missing user 是不同的 exceptions。讓 driver raise;翻譯你打算處理的那一個。
8. Packages, venv, Visibility
TypeScript project 在 code 旁邊有 node_modules。Python project 有一個 interpreter 指向的 virtual environment。
.venv/
lib/python3.12/site-packages/...
pyproject.toml
src/
users/
__init__.py
models.pyvenv(或uv管理的 env)是 dependencies 落地的地方。它不是你 ship 的 path 上的node_modules;它是 interpreter 的 search path。pyproject.toml是package.json。pip是 installer。uv是連 lock 一起做的快的那個。requirements.txt是舊 lockfile。__init__.py把 directory 變成 package。它也是 barrel file:你from users import到的,是users/__init__.pyexpose 的東西。_private是 convention,不是 compiler。
Failure: 因為 pip install fastapi 曾經成功,就裝進 system Python。下一個 project 會繼承它。用 venv。
9. CPython vs Node
Node 是一條跑 JavaScript 的 thread,加上 I/O 的 pool。CPython 有 GIL:一次只有一條 thread 跑 bytecode。模型仍是 JavaScript 核心概念 —— 一個 loop、queues、callbacks —— 但 loop 是 asyncio,而且 process 裡有真的 threads,大多在等。
Node: stack (JS) → microtasks → one macrotask → poll I/O
CPython: GIL (one bytecode thread)
asyncio loop: tasks yield at await
blocking I/O blocks the loop, not a "thread you forgot"async def是 coroutine。它在awaityield。它不是 Java virtual thread,也不是 background worker。裡面一個requests.get會停住整個 loop。- GIL 意味著 CPython 上
threading不給你 parallel CPU。multiprocessing或 worker pool 才會。I/O 的話,asyncio就夠。 - Async handler 裡的 CPU-bound 工作,跟 Node 裡一段長的 sync loop 是同一個錯。搬到 thread(
run_in_executor)或 process。
Failure: async def get_user(...) 裡呼叫 blocking 的 SQLAlchemy session。Loop 現在是只有一人的 queue。用 asyncpg / SQLAlchemy async,或把 route 改成 sync。
10. Build
package.json 加 lockfile 是 TypeScript 習慣。Python 把 project metadata 與 resolver 分開。
| TypeScript / bun | Python | |
|---|---|---|
| Manifest | package.json | pyproject.toml |
| Lock | bun.lock | uv.lock,或 compiled 的 requirements.txt |
| Install | node_modules | venv 的 site-packages |
| Ship | 一個 server process,或一份 bundle | source 加上 interpreter(或帶 venv 的 Docker image) |
| Entry | src/index.ts | 以 python -m 跑的 module,或 uvicorn app:app |
- 沒有 fat JAR。Production ship 的是 source 與 interpreter,通常在已經裝好 venv 的 container 裡。Serverless ZIP 跟 Lambda bundle 是同一個想法。
uvresolve 與 install 都快。它讀pyproject.toml並寫 lock。只用pip就得自己 pin。src/layout 讓 imports 誠實。Tests 旁邊放平的 package,在糟糕的一天會 import 到錯的那份。
Failure: 實驗了一個週末之後 pip freeze > requirements.txt。Lock 現在是垃圾場。從 pyproject.toml 開始。
11. FastAPI as Hono
Hono 是你用 request 呼叫的 function。FastAPI 是一個由 decorated functions 組成的 router,並由 Pydantic parse body。
app.get("/users/:id", async (c) => {
const id = c.req.param("id")
const user = await users.require(id)
return c.json(user)
})from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/users/{id}")
async def get_user(id: str) -> UserRead:
user = await users.require(id)
if user is None:
raise HTTPException(status_code=404, detail="user not found")
return user@app.get("/users/{id}")是app.get。Path parameter 是 function argument,不是c.req.param。Return annotation 是 response model。Depends是 middleware 加 injection。它不是 Spring bean container。它按 request resolve。Dependency 可以讀 request、開 DB session,然後 yield 它。HTTPException是 typed exit。底下是 Starlette。FastAPI 在上面加 OpenAPI 與 Pydantic。uvicorn是 process。FastAPI是 app object。除了定義app的 module 之外,沒有index.ts。
Failure: 把 dependency 當成可以 stash request 的 singleton。Depends 按 request 跑。Request-scoped state 放在 yield 出來的 object 上,不是 app 上。
12. Pydantic and SQLAlchemy
Zod 是你 parse 的 schema。Pydantic BaseModel 是同一個想法放在 class 上。SQLAlchemy 2 是 Drizzle 較重的表親:一個 explicit session,不是你忘記關的 request-scoped implicit。
const UserCreate = z.object({
email: z.string().email(),
})
await db.insert(users).values({ email }).returning()from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
# SQLAlchemy 2
from sqlalchemy import select
stmt = select(User).where(User.id == id)
user = session.scalars(stmt).first()BaseModel把 JSON parse 成 typed object。EmailStr是 validator。model_dump()是回 JSON 的路。它不是 ORM。- SQLAlchemy 有
Session/AsyncSession。它是 unit of work。不要跨 requests 共享。一個 yield session 然後關掉的Depends是 FastAPI 習慣。 select()是 SQL 形狀。Session 追蹤 identity。Lazy loads 是第二次 query。Postgres 仍然求值一條 statement:SQL 核心概念。Tenant filters 仍屬於 query 與 RLS:用 Hono、Better Auth、Drizzle 與 Postgres RLS 打造 Multi-Tenant 後端。
Failure: 因為「FastAPI 會 serialize」就把 ORM object 當 JSON 回傳。那是 detached instance、lazy load,或 500。在 boundary 把 ORM → Pydantic,跟 Java note 裡 entity → record 同一直覺。
13. Where It Sits
Python 是 這個 interpreter 如何把 value 傳來傳去。FastAPI 是 這個 process 如何接收 request。兩者都不取代 authorization、SQL,或 session。
- JVM 兄弟篇:以 TypeScript Developer 身分學 Java
- TypeScript 的 runtime identity:TypeScript Class 與 Runtime Identity
- JS 何時執行:JavaScript 核心概念
- Hono analog:用 Hono、Drizzle、Zod OpenAPI 與 SST 打造 Backend APIs
- Statement:SQL 核心概念
- Identity、membership,以及那一行:用 Hono、Better Auth、Drizzle 與 Postgres RLS 打造 Multi-Tenant 後端