Type Hint ও Pydantic

AI এর লেখা code এর ভুল আগেই ধরার হাতিয়ার

রোডম্যাপের মাস ২পড়তে প্রায় ১৭ মিনিট

Dart এ int x = "hello"; লিখলে code চালানোর আগেই লাল দাগ পড়ে। Python এ এমন কিছু আটকায় না, ভুলটা ধরা পড়ে চালানোর সময়, অনেক সময় production এ, user এর সামনে। Type hint আর Pydantic এই সমস্যার সমাধান। AI এর যুগে এগুলোর গুরুত্ব আরও বেশি: AI যখন তোমার জন্য code লেখে, type checker সেই code এর অনেক ভুল সাথে সাথে ধরে দেয়, তোমাকে প্রতিটা লাইন পরীক্ষা করে বের করতে হয় না।

#দুই ধরনের যাচাই

তোমার code Type checker Pyright / mypy, চালানোর আগে ভুল সাথে সাথে ধরা editor এ লাল দাগ বাইরের data API request, JSON Pydantic চালানোর সময় যাচাই ঠিক থাকলে typed object ভুল হলে পরিষ্কার error উপরে: তোমার লেখা code এর ভুল। নিচে: বাইরে থেকে আসা data এর ভুল।
Type hint দুই জায়গায় কাজে লাগে: code চালানোর আগে type checker দিয়ে, আর চালানোর সময় Pydantic দিয়ে বাইরের data যাচাই করে।

#মৌলিক type hint

Python
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 হতে পারে এমন জিনিস

Python
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

Python
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

Python
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 | None

Dart এর T first<T>(List<T> items) এর মতো।

#Protocol: "এমন কিছু যার এই method আছে"

Python
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 করতে:

Terminal
uv add --dev pyright
uv run pyright
Text
src/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।

Terminal
uv add pydantic "pydantic[email]"
Python
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 দিলে:

Python
try:
    UserCreate(name="A", email="not-email", age=8, phone="123", address={})
except ValidationError as e:
    print(e)
Text
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 কে পাঠায়, তুমি এক লাইনও না লিখে।

#নিজের যাচাইয়ের নিয়ম

Python
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

Terminal
uv add pydantic-settings
Python
from 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:

INI
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 ভরে যাবে।