Type Hint ও Pydantic
AI এর লেখা code এর ভুল আগেই ধরার হাতিয়ার
Dart এ int x = "hello"; লিখলে code চালানোর আগেই লাল দাগ পড়ে। Python এ এমন কিছু আটকায় না, ভুলটা ধরা পড়ে চালানোর সময়, অনেক সময় production এ, user এর সামনে। Type hint আর Pydantic এই সমস্যার সমাধান। AI এর যুগে এগুলোর গুরুত্ব আরও বেশি: AI যখন তোমার জন্য code লেখে, type checker সেই code এর অনেক ভুল সাথে সাথে ধরে দেয়, তোমাকে প্রতিটা লাইন পরীক্ষা করে বের করতে হয় না।
#দুই ধরনের যাচাই
#মৌলিক type hint
name: str = "Rahim"
age: int = 26
price: float = 99.5
active: bool = True
def greet(name: str, times: int = 1) -> str:
return ("হ্যালো " + name + "! ") * times
# collection (Python 3.9+ এ সরাসরি list[...], dict[...])
scores: list[int] = [90, 85]
prices: dict[str, int] = {"আম": 120}
location: tuple[float, float] = (23.8, 90.4)
tags: set[str] = {"food", "fast"}#None হতে পারে এমন জিনিস
def find_user(user_id: int) -> dict | None: # dict অথবা None
...
user = find_user(5)
print(user["name"]) # ⚠ type checker সতর্ক করবে: user None হতে পারে
if user is not None:
print(user["name"]) # ✓ এখন নিশ্চিতDart এর null safety (String?) এর মতো। পুরনো code এ Optional[dict] লেখা দেখবে, মানে একই জিনিস।
#আরও কিছু দরকারি type
from typing import Literal, Any, Callable, TypedDict
# নির্দিষ্ট কয়েকটা value
def set_theme(mode: Literal["light", "dark"]) -> None: ...
set_theme("blue") # ⚠ type checker ধরবে
# function কে parameter হিসেবে
def retry(func: Callable[[], str], times: int) -> str: ...
# dict এর key গুলোর type ঠিক করা (JSON এর গঠন বোঝাতে)
class UserDict(TypedDict):
id: int
name: str
email: str
# type এর নতুন নাম (alias)
type UserId = int
type JSON = dict[str, Any]#Generic: যেকোনো type এর জন্য একই code
def first[T](items: list[T]) -> T | None: # Python 3.12+ এর syntax
return items[0] if items else None
x = first([1, 2, 3]) # type checker জানে x হলো int | None
y = first(["a", "b"]) # y হলো str | NoneDart এর T first<T>(List<T> items) এর মতো।
#Protocol: "এমন কিছু যার এই method আছে"
from typing import Protocol
class Notifier(Protocol):
def send(self, to: str, message: str) -> None: ...
class SmsNotifier: # Notifier থেকে inherit করেনি
def send(self, to: str, message: str) -> None:
print(f"SMS → {to}: {message}")
def notify_customer(n: Notifier) -> None:
n.send("017...", "Order confirmed")
notify_customer(SmsNotifier()) # ✓ চলবে, কারণ send() আছেProtocol বলে "আমার শুধু এমন একটা জিনিস দরকার যার send() আছে"। Inheritance লাগে না। এটা test এ নকল (fake) জিনিস দেওয়া খুব সহজ করে।
#Type checker চালানো
VS Code এ Pylance আগে থেকেই type check করে (setup অধ্যায়ে typeCheckingMode: standard দিয়েছিলাম)। Terminal থেকে পুরো project check করতে:
uv add --dev pyright
uv run pyrightsrc/app/orders.py:12:15 - error: "None" is not subscriptable
src/app/orders.py:20:9 - error: Argument of type "str" cannot be assigned to parameter "qty" of type "int"
2 errors#Pydantic: বাইরের data যাচাই
Type hint চালানোর সময় কিছু আটকায় না। কিন্তু API তে user যা খুশি পাঠাতে পারে: সংখ্যার জায়গায় লেখা, দরকারি field বাদ, ঋণাত্মক দাম। Pydantic type hint পড়ে চালানোর সময় data যাচাই করে। FastAPI এর ভেতরে পুরোটা Pydantic।
uv add pydantic "pydantic[email]"from pydantic import BaseModel, Field, EmailStr, ValidationError
class Address(BaseModel):
area: str
city: str = "Dhaka"
class UserCreate(BaseModel):
name: str = Field(min_length=2, max_length=50)
email: EmailStr
age: int = Field(ge=13, le=120) # ১৩ থেকে ১২০
phone: str = Field(pattern=r"^01[3-9]\d{8}$") # বাংলাদেশি mobile নম্বর
address: Address # nested model
tags: list[str] = []
# সঠিক data
data = {
"name": "Nusrat",
"email": "nusrat@example.com",
"age": "27", # string, কিন্তু Pydantic int এ বদলে নেবে
"phone": "01712345678",
"address": {"area": "Mirpur"},
}
user = UserCreate(**data) # অথবা UserCreate.model_validate(data)
print(user.age + 1) # 28, এখন int
print(user.address.city) # Dhaka (default)
print(user.model_dump()) # আবার dict এ
print(user.model_dump_json()) # JSON string এভুল data দিলে:
try:
UserCreate(name="A", email="not-email", age=8, phone="123", address={})
except ValidationError as e:
print(e)5 validation errors for UserCreate
name
String should have at least 2 characters
email
value is not a valid email address
age
Input should be greater than or equal to 13
phone
String should match pattern '^01[3-9]\d{8}$'
address.area
Field requiredএকবারে সব ভুল, কোন field এ কী সমস্যা সহ। FastAPI ঠিক এই error গুলোকেই 422 response হিসেবে client কে পাঠায়, তুমি এক লাইনও না লিখে।
#নিজের যাচাইয়ের নিয়ম
from pydantic import BaseModel, field_validator, model_validator
class OrderCreate(BaseModel):
items: list[str]
coupon: str | None = None
delivery_slot_start: int # ঘণ্টা (0-23)
delivery_slot_end: int
@field_validator("coupon")
@classmethod
def normalize_coupon(cls, v: str | None) -> str | None:
return v.strip().upper() if v else v
@model_validator(mode="after")
def check_slot(self) -> "OrderCreate":
if self.delivery_slot_end <= self.delivery_slot_start:
raise ValueError("Delivery এর শেষ সময় শুরুর পরে হতে হবে")
return self#Pydantic বনাম dataclass
| dataclass | Pydantic BaseModel | |
|---|---|---|
| যাচাই | করে না | করে |
Type বদল ("27" → 27) |
না | হ্যাঁ |
| JSON এ/থেকে | হাতে লিখতে হয় | model_dump_json(), model_validate_json() |
| গতি | সবচেয়ে দ্রুত | দ্রুত (ভেতরে Rust) |
| কোথায় | ভেতরের data | সীমানায়: API, config, বাইরের file |
নিয়ম: যেখানে বাইরের data system এ ঢোকে, সেখানে Pydantic। ভেতরে সাধারণ dataclass বা class চলে।
#Settings: environment variable থেকে config
uv add pydantic-settingsfrom pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
redis_url: str = "redis://localhost:6379/0"
jwt_secret: str
debug: bool = False
settings = Settings() # .env আর environment থেকে পড়বে, না পেলে error.env file:
DATABASE_URL=postgresql+asyncpg://app:secret@localhost:5432/notes
JWT_SECRET=change-me-in-production
DEBUG=trueদরকারি কোনো setting না থাকলে app চালু হওয়ার সময়েই error দেবে, production এ গিয়ে মাঝপথে না। এটা আমরা FastAPI project এ সরাসরি ব্যবহার করবো।
#অনুশীলন
#নিজেকে যাচাই করো
Type hint কি Python চালানোর সময় ভুল type আটকায়?
না। সাধারণ type hint চালানোর সময় কিছু আটকায় না, এটা শুধু তথ্য। Type checker (Pyright, mypy) চালানোর আগে code পরীক্ষা করে। চালানোর সময় যাচাই চাইলে Pydantic লাগে।
`def f() -> dict | None` এর return value নিয়ে কাজ করার আগে কী করবে?
None কিনা check করবে (if result is not None:)। না করলে type checker সতর্ক করবে, আর চালানোর সময় None আসলে TypeError হবে।
Pydantic আর dataclass কখন কোনটা?
বাইরে থেকে আসা data (API request, JSON file, environment config) যাচাই করতে Pydantic। ভেতরের data যেটা তুমি নিজে বানাচ্ছো আর যাচাই লাগবে না, সেখানে dataclass।
Protocol এর সুবিধা কী?
Inheritance ছাড়াই বলা যায় "এমন জিনিস দরকার যার এই method গুলো আছে"। ফলে যেকোনো class, এমনকি test এর জন্য বানানো ছোট নকল class ও ব্যবহার করা যায়, যদি method গুলো মেলে।
AI এর লেখা code এ `Any` দেখলে কী করবে?
জিজ্ঞেস করবে এখানে আসল type কী হওয়া উচিত, আর সেটা বসাবে। Any type checking বন্ধ করে দেয়, ফলে ওই জায়গার ভুল আর ধরা পড়ে না।
চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।