সরাসরি পর্বে যান
পর্ব ৩৭ · ৫৫-এর মধ্যে · মডিউল ৬

type hint আর mypy — চালানোর আগেই বাগ ধরা

Type Hints, mypy and Gradual Typing in Modern Python

Taught in Bangla, this lesson introduces type hints and static checking: annotating functions, collection and callable types, handling None and type narrowing, aliases, TypedDict, Literal and Final, generics with the new def first[T] syntax and Protocol, gradual adoption with mypy strict mode, and runtime annotations with lazy evaluation and pydantic.

এই পর্বে যা শিখবেন

  • Type hint কী, কারা পড়ে, আর কেন Python চালানোর সময় এগুলো যাচাই করে না
  • mypy install করে চালানো, তার error বার্তা পড়া, strict mode আর পুরোনো প্রজেক্টে ধাপে ধাপে যোগ করা
  • list[int], tuple[int, ...], X | None, Any বনাম object, Callable, type alias, TypedDict, Literal, Final
  • Generic — TypeVar আর Python 3.12-এর নতুন def first[T] syntax, আর Protocol দিয়ে structural typing
  • __annotations__, get_type_hints, Python 3.14-এর lazy annotation, আর pydantic/FastAPI-র মতো যে টুল চালানোর সময় hint ব্যবহার করে

১ · Type hint — মানুষ আর টুলের জন্য লেবেল

পর্ব ০৬-এ দেখেছেন, Python-এ dynamic typing: একটি নাম আজ একটি সংখ্যার দিকে, কাল একটি string-এর দিকে নির্দেশ করতে পারে। এই স্বাধীনতাই Python-কে দ্রুত লেখার ভাষা বানিয়েছে। কিন্তু এর একটি দাম আছে। একটি ফাংশনের def লাইন দেখে বোঝা যায় না সে কী চায়:

def add_vat(price, rate):
    return price + price * rate

price কি টাকার অঙ্ক (সংখ্যা), নাকি একটি ফর্ম থেকে পড়া লেখা (string)? rate কি 15 না 0.15? ফেরত আসে কী? উত্তর জানতে হয় ফাংশনের ভেতর পড়ে, docstring খুঁজে, বা লেখককে জিজ্ঞেস করে। দুই মাস পর লেখক নিজেও হয়তো ভুলে যাবেন। এবার একই ফাংশন type hint দিয়ে:

def add_vat(price: float, rate: float = 0.15) -> float:
    return price + price * rate

এখন def লাইনটিই একটি ছোট চুক্তি: “এই ফাংশন একটি float দাম আর একটি float হার নেয়, আর একটি float ফেরত দেয়”। : float অংশগুলোকে বলে annotation, আর -> float হলো return type-এর annotation. যখন annotation-এ একটি type লেখা হয়, তখন তাকেই আমরা type hint বলি।

ওষুধের দোকানে প্রতিটি বাক্সের গায়ে লেবেল থাকে: “দিনে দুইবার, খাওয়ার পরে”। লেবেল নিজে কাউকে থামায় না — চাইলে কেউ খালি পেটে খেতেই পারে। কিন্তু লেবেল থাকলে রোগী জানে কী করতে হবে, ফার্মাসিস্ট প্রেসক্রিপশনের সাথে মিলিয়ে ভুল ধরতে পারেন, আর হাসপাতালের সফটওয়্যার দুটি ওষুধের বিপজ্জনক মিশ্রণে সতর্ক করতে পারে। Type hint ঠিক এমন লেবেল। Python নিজে লেবেল মানতে বাধ্য করে না; কিন্তু মানুষ আর টুল লেবেল পড়ে ভুল ধরে।

তাহলে লেবেলটি পড়ে কারা? তিন পক্ষ:

👩‍💻 মানুষ

আপনি, আপনার দলের সদস্য, ছয় মাস পরের আপনি। def লাইন দেখেই বোঝা যায় কী দিতে হবে, কী পাবেন। এটি এমন documentation, যা কোডের সাথে বাঁধা — তাই পুরোনো হয়ে মিথ্যা হয়ে যাওয়ার সম্ভাবনা কম।

🔍 Type checker

mypy, pyright — এমন type checker, যারা কোড না চালিয়ে শুধু পড়ে, আর hint-এর সাথে না মিললে লাইন নম্বরসহ error দেখায়। ভুলটা ধরা পড়ে প্রোগ্রাম চালানোর আগেই।

⌨️ Editor

VS Code বা PyCharm hint পড়ে autocomplete দেয়: name: str লিখলে name. চাপামাত্র upper(), split() দেখায়। ভুল জায়গায় লাল দাগ পড়ে, আর নাম বদলানো (refactoring) নিরাপদ হয়।

মূল ধারণা

Python চালানোর সময় type hint যাচাই করে না। Hint শুধু জমা থাকে, যাতে অন্য টুল পড়তে পারে। ভুল type দিলে Python দিব্যি চালায় — যতক্ষণ না কোনো operation সত্যিই অসম্ভব হয়ে ভেঙে পড়ে। আগাম যাচাই চাইলে একটি আলাদা টুল চালাতে হয়। এই একটি কথা মনে রাখলে এই পর্বের বাকিটা সহজ।

কথাটি নিজের চোখে দেখুন। নিচের প্রোগ্রামে hint বলছে price একটি float, অথচ আমরা ইচ্ছা করে string আর list পাঠাচ্ছি। চালানোর আগে অনুমান করুন — কোন লাইন চলবে, কোনটি ভাঙবে?

def add_vat(price: float, rate: float = 0.15) -> float:
    return price + price * rate

print(add_vat(500))          # ঠিক type
print(add_vat(500, 2))       # int দিলাম, float চাওয়া ছিল
print(add_vat("500", 2))     # str! Python আপত্তি করে না
print(add_vat([1], 1))       # list!

count: int = "অনেক"           # ভ্যারিয়েবলের hint-ও যাচাই হয় না
print(count, type(count).__name__)

try:
    add_vat("500")           # এবার সত্যিই ভাঙল
except TypeError as e:
    print("TypeError:", e)

আউটপুট ছয় লাইনের। 575.0 — স্বাভাবিক হিসাব। 1500 — int দিয়েও চলে, কারণ hint-এর নিয়মে float চাওয়া জায়গায় int গ্রহণযোগ্য (এটি mypy-ও মেনে নেয়)। 500500500 — চমক! "500" * 2 হলো "500500", তার সাথে "500" জোড়া লাগল; একটি ভ্যাটের হিসাব নিঃশব্দে একটি লম্বা লেখা ফেরত দিল, কোনো error নেই। [1, 1] — list-এর সাথেও একই কাণ্ড। অনেক str — ভ্যারিয়েবলের hint int হলেও ভেতরে একটি string বসে আছে। শেষে TypeError: can't multiply sequence by non-int of type 'float' — শুধু তখনই ভাঙল, যখন "500" * 0.15 অসম্ভব হলো। অর্থাৎ ভুল type কখনো ক্র্যাশ করে, কখনো চুপচাপ ভুল উত্তর দেয় — আর দ্বিতীয়টিই বেশি বিপজ্জনক।

Python 3.0 (২০০৮) থেকেই ফাংশনের parameter আর return-এ annotation লেখা যেত — PEP 3107 (২০০৬, Collin Winter ও Tony Lownds). কিন্তু তখন এর কোনো নির্দিষ্ট অর্থ ছিল না; যে কেউ যা খুশি লিখতে পারত। ২০১৪ সালে Guido van Rossum, Jukka Lehtosalo আর Łukasz Langa লেখেন PEP 484 — Type Hints, যা ঠিক করে দেয় annotation-এ type লেখার মানক ভাষা, আর নিয়ে আসে typing module — Python 3.5-এ (২০১৫)। ভ্যারিয়েবলে hint (count: int = 0) এসেছে Python 3.6-এ (PEP 526).

২ · mypy — চালানোর আগেই বাগ ধরা

Hint থেকে আসল সুরক্ষা পেতে একটি type checker চালাতে হয়। সবচেয়ে পুরোনো আর বহুল ব্যবহৃত হলো mypy. এটি একটি সাধারণ Python প্যাকেজ — পর্ব ২৩-এর মতো আপনার virtual environment-এ install করুন, তারপর ফাইলের নাম দিয়ে চালান:

pip install mypy
mypy vat.py

mypy আপনার কোড চালায় না. সে ফাইলটি পড়ে, প্রতিটি নাম আর প্রতিটি expression-এর type হিসাব করে, আর যেখানে hint-এর সাথে মেলে না সেখানে error দেখায়। এই ধরনের যাচাইকে বলে static analysis — কোড স্থির অবস্থায় রেখে বিশ্লেষণ। একটি বাস্তব বাগ ধাপে ধাপে দেখুন: একটি দোকানের হিসাব-প্রোগ্রাম দাম পড়ে একটি ফর্ম থেকে, আর ফর্মের সব মান আসে string হিসেবে।

  1. কোডvat.pyhint সহ
  2. শুধু চালালেpythonপরে ক্র্যাশ
  3. আগে যাচাইmypyলাইন ৭
  4. ঠিক করাfloat()Success
  5. Editorpyrightলেখার সময়ই
vat.pyফর্মের মান = str
def add_vat(price: float) -> float:
    return round(price * 1.15, 2)

def checkout(form: dict[str, str]) -> None:
    raw = form["price"]          # "500" — একটি str
    ...
    total = add_vat(raw)         # লাইন ৭
$ python vat.pyদিনের শেষে, আসল ক্রেতার সামনে
Traceback (most recent call last):
  ...
  File "vat.py", line 2, in add_vat
    return round(price * 1.15, 2)
TypeError: can't multiply sequence by non-int of type 'float'
$ mypy vat.pyকোড চালানো হয়নি
vat.py:7: error: Argument 1 to "add_vat" has incompatible
type "str"; expected "float"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
$ mypy vat.pyraw-কে সংখ্যায় বদলানো হলো
    total = add_vat(float(raw))

Success: no issues found in 1 source file
VS Code (Pylance)টাইপ করার মুহূর্তে
    total = add_vat(raw)
                    ~~~
Argument of type "str" cannot be assigned to
parameter "price" of type "float"
  1. কোডটি দেখতে নির্দোষ। add_vat-এর hint বলছে price: float. checkout-এর hint বলছে form একটি dict[str, str] — অর্থাৎ ভেতরের সব মান string. তাই raw একটি str. লাইন ৭-এ সেটি সরাসরি add_vat-এ চলে গেছে। একজন মানুষ চোখে দেখে হয়তো ধরতে পারবেন না — দুটি ফাংশন দুই ফাইলে থাকলে তো আরও কঠিন।
  2. শুধু চালালে — দেরিতে, ভুল জায়গায়। mypy ছাড়া এই বাগ ধরা পড়বে শুধু তখন, যখন কোনো ক্রেতা সত্যিই checkout করবেন। Traceback দেখাবে লাইন ২ — add_vat-এর ভেতরে — অথচ আসল ভুল লাইন ৭-এ, ডাকার জায়গায়। আর যদি এই পথ শুধু মাসের শেষ দিনে চলে, তাহলে বাগটি সপ্তাহের পর সপ্তাহ লুকিয়ে থাকবে।
  3. mypy — আগে, ঠিক জায়গায়। mypy vat.py কোড না চালিয়েই বলল: লাইন ৭-এ add_vat-এর প্রথম argument str, অথচ চাওয়া হয়েছিল float. শেষে বন্ধনীতে [arg-type] হলো error-এর কোড — পরে খুঁজতে বা নির্দিষ্ট কোনো error বন্ধ করতে কাজে লাগে।
  4. ঠিক করা আর আবার যাচাই। ফর্মের লেখাকে সংখ্যায় বদলাতে হবে — float(raw). আবার mypy: Success. (বাস্তবে এখানে ValueError-ও সামলাতে হবে, যদি কেউ “পাঁচশো” লেখেন — পর্ব ২৪. mypy type মেলায়, মানের সঠিকতা নয়।)
  5. Editor — লেখার মুহূর্তেই। VS Code-এর Python extension (Pylance) ভেতরে pyright ব্যবহার করে। তাই mypy চালানোর আগেই, টাইপ করার সময়ই raw-এর নিচে লাল ঢেউ-দাগ পড়ে। বাগ ধরার সবচেয়ে সস্তা মুহূর্ত এটাই — কোড লেখা শেষ হওয়ার আগেই।

mypy-র error বার্তা পড়া

mypy-র প্রতিটি বার্তার একই কাঠামো — ফাইল:লাইন: error: বর্ণনা [কোড]. কয়েকটি সবচেয়ে সাধারণ বার্তা নিচে (সংক্ষেপে, ফাইলের নাম ও লাইন বাদ দিয়ে):

error: Argument 1 to "add_vat" has incompatible type "str"; expected "float"  [arg-type]
error: Incompatible return value type (got "str", expected "float")  [return-value]
error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
error: Item "None" of "Student | None" has no attribute "name"  [union-attr]
error: "int" has no attribute "upper"  [attr-defined]
error: Missing return statement  [return]
Found 6 errors in 1 file (checked 1 source file)

প্রথমটি ভুল argument, দ্বিতীয়টি ফাংশন যা বলেছিল তার বদলে অন্য কিছু ফেরত দিচ্ছে, তৃতীয়টি একটি int ভ্যারিয়েবলে string বসানো (ঠিক আগের runnable প্রোগ্রামের count), চতুর্থটি None-এর বাগ (অংশ ৪-এ বিস্তারিত), পঞ্চমটি ভুল type-এ method ডাকা, আর ষষ্ঠটি — -> float বলা ফাংশনের কোনো একটি শাখায় return নেই। লক্ষ করুন, প্রতিটিই এমন বাগ যা চালানোর সময় হয়তো অনেক পরে, অনেক দূরে ধরা পড়ত।

mypy যা পারে না: সে type মেলায়, যুক্তি নয়। add_vat(500)-এর জায়গায় কেউ add_vat(50) লিখলে, বা ১৫%-এর জায়গায় ১০% বসালে mypy কিছু বলবে না। তাই type checker টেস্টের (পর্ব ৩৯) বিকল্প নয় — দুটি দুই ধরনের বাগ ধরে। Type checker ধরে “ভুল ধরনের জিনিস”, টেস্ট ধরে “ভুল উত্তর”।

৩ · Collection-এর type — list[int] থেকে Callable

শুধু list লিখলে type checker জানে “একটি list”, কিন্তু ভেতরে কী আছে জানে না। ভেতরের type বলতে বর্গ বন্ধনী ব্যবহার করুন। এই ধরনের type — যার ভেতরে আরেকটি type বসানো যায় — তাকে বলে generic type:

marks: list[int] = [78, 91, 64]                  # সব উপাদান int
rates: dict[str, float] = {"USD": 122.5}          # চাবি str, মান float
point: tuple[float, float] = (23.81, 90.41)       # ঠিক দুটি float
scores: tuple[int, ...] = (4, 6, 1, 0)            # যেকোনো সংখ্যক int
tags: set[str] = {"python", "বাংলা"}
matrix: list[list[int]] = [[1, 2], [3, 4]]        # ভেতরে ভেতরে list

tuple-এর নিয়মটি আলাদা, খেয়াল করুন। tuple[float, float] মানে ঠিক দুটি ঘর, প্রতিটির type আলাদাভাবে বলা (পর্ব ১৫-এর রেকর্ডের মতো)। আর tuple[int, ...] — তিনটি বিন্দুসহ — মানে “যত খুশি, সবগুলো int”, list-এর মতো।

পুরোনো ধরন — typing.List, Dict: পুরোনো কোড আর অনেক টিউটোরিয়ালে দেখবেন from typing import List, Dict আর List[int]. Python 3.9-এর আগে বাধ্য ছিল, কারণ list[int] লেখা যেত না। Python 3.9 থেকে (PEP 585) সাধারণ list, dict, tuple, set নিজেই বর্গ বন্ধনী নেয়, আর পুরোনো নামগুলো deprecated — এখনো চলে, কিন্তু নতুন কোডে লেখার দরকার নেই।

ফাংশন যখন argument: Callable

পর্ব ৩৫-এ দেখেছেন, ফাংশনও একটি object, আরেকটি ফাংশনে পাঠানো যায়। তার hint হলো Callable[[argument-এর type], return type], যা আনতে হয় collections.abc থেকে:

from collections.abc import Callable

def apply_all(prices: list[float], rule: Callable[[float], float]) -> list[float]:
    return [rule(p) for p in prices]

apply_all([100, 250], lambda p: p * 0.9)    # ১০% ছাড়

Callable[[float], float] পড়ুন এভাবে: “যাকে একটি float দিয়ে ডাকা যায় আর যে একটি float ফেরত দেয়”। Callable[[int], str] হলে একটি int নিয়ে string দেয় এমন ফাংশন। কেউ যদি দুই argument-এর ফাংশন পাঠায়, mypy আপত্তি করবে।

একটি সূক্ষ্ম নিয়ম — list কেন “অনড়”

আগেই দেখেছেন, float চাওয়া জায়গায় int চলে। তাহলে list[float] চাওয়া জায়গায় list[int] চলবে? অবাক করা উত্তর: না.

def add_fee(prices: list[float]) -> None:
    prices.append(2.5)                  # list[float]-এ float যোগ — বৈধ

counts: list[int] = [1, 2, 3]
add_fee(counts)                         # mypy: error! কেন?

কারণটি ভেতরের লাইনে। add_fee list-টিতে 2.5 যোগ করতে পারে। যদি list[int] পাঠানো চলত, তাহলে counts — যা “সবসময় int” বলে ঘোষিত — তাতে একটি float ঢুকে যেত। তাই বদলানো যায় এমন container-এর ভেতরের type হুবহু মিলতে হয়; একে বলে invariance. mypy-র বার্তাতেই সমাধান লেখা থাকে: যদি ফাংশন শুধু পড়ে, তাহলে list-এর বদলে লিখুন Sequence[float] (বা Iterable[float]), যা শুধু পড়ার চুক্তি — তাতে list[int]-ও চলবে, tuple-ও চলবে। পর্ব ৩৩-এ Iterable[HasArea] লেখার কারণ এটাই। নিয়মটি মনে রাখুন: parameter-এ যতটা সম্ভব উদার type (Sequence, Iterable, Mapping), return-এ যতটা সম্ভব নির্দিষ্ট (list, dict).

Any বনাম object — দুটি “যেকোনো কিছু”, অর্থ উল্টো

প্রশ্নAnyobject
কী গ্রহণ করেসবকিছুসবকিছু (সব class-ই object)
তার ওপর কী করা যায়যা খুশি — mypy কিছুই যাচাই করে নাশুধু সব object-এর সাধারণ কাজ (str(), ==); .upper() ডাকলে error
অন্য type-এ পাঠানো যায়?হ্যাঁ, যেকোনো জায়গায়না, আগে isinstance দিয়ে নিশ্চিত হতে হয়
অর্থ“এখানে type যাচাই বন্ধ”“যেকোনো কিছু হতে পারে, তাই সাবধানে ব্যবহার করো”

Any হলো জরুরি নির্গমন-দরজা। কোনো কিছুর type সত্যিই জানা না থাকলে, বা পুরোনো কোডে ধাপে ধাপে hint যোগ করার সময়, Any বলে “এখানে আপাতত যাচাই কোরো না”। কিন্তু প্রতিটি Any একটি অন্ধ বিন্দু — তার ভেতর দিয়ে যে বাগই যাক, mypy দেখবে না। আর Any সংক্রামক: Any থেকে পাওয়া মানও Any. যেখানে বলতে চান “যেকোনো কিছু নেব, কিন্তু অন্ধ হব না”, সেখানে object লিখুন।

৪ · None — type hint-এর সবচেয়ে বড় শিকার

বাস্তব Python প্রজেক্টে যে বাগটি সবচেয়ে বেশি দেখা যায়, তার একটি হলো এই traceback:

AttributeError: 'NoneType' object has no attribute 'name'

কাহিনি প্রায় সবসময় এক। একটি ফাংশন কিছু খোঁজে — রোল নম্বর দিয়ে ছাত্র, ফোন নম্বর দিয়ে গ্রাহক, আইডি দিয়ে অর্ডার। পেলে ফেরত দেয়, না পেলে None. যে ফাংশনটি ডাকে, সে “না পাওয়ার” সম্ভাবনা ভুলে যায়। পরীক্ষার সময় সব রোল ঠিক ছিল, তাই বাগটি ধরা পড়েনি; প্রথম ভুল রোল আসামাত্র প্রোগ্রাম ভেঙে পড়ল।

Type hint এই সম্ভাবনাটিকে def লাইনে দৃশ্যমান করে। “একটি Student, অথবা None” লেখা হয় Student | None দিয়ে — দুটি type-এর মাঝে খাড়া দাগ মানে “এটা, নয়তো ওটা”। একে বলে union type:

def get_student(roll: int) -> Student | None:
    return students.get(roll)       # dict.get না পেলে None দেয়

X | None লেখা যায় Python 3.10 থেকে (PEP 604). পুরোনো কোডে একই জিনিস দেখবেন Optional[Student] (from typing) হিসেবে — নামটি বিভ্রান্তিকর, কারণ এর অর্থ “argument ঐচ্ছিক” নয়, অর্থ “None-ও হতে পারে”। তাই আধুনিক কোডে Student | None-ই পরিষ্কার। একইভাবে int | str মানে “int অথবা str”, পুরোনো রূপে Union[int, str].

এবার আসল জাদু। get_student-এর ফল যে ব্যবহার করবে, mypy তাকে None সামলাতে বাধ্য করে — আর সামলানোর পর সে বুঝে নেয় যে এখন আর None নেই। এই বুঝে নেওয়াকে বলে type narrowing. প্রতিটি লাইনে type checker-এর চোখে s-এর type কী, ধাপে ধাপে দেখুন:

1def get_student(roll: int) -> Student | None:2    return students.get(roll)34s = get_student(42)5print(s.name)                # ভুলে গেছি!67if s is None:8    print("রোল পাওয়া যায়নি")9else:10    print(s.name)            # নিরাপদ
type checker-এর চোখে
ফেরত type
s
রায়
  1. সম্ভাবনাটি লেখা হলো। Return type বলছে: এই ফাংশন একটি Student দিতে পারে, অথবা None. dict.get-এর নিজের hint-ও তা-ই বলে — চাবি না থাকলে None. লেখক সত্যি কথাটি লিখে রাখলেন; এবার দায়িত্ব যে ডাকবে তার।
  2. ফল হাতে এল। s = get_student(42). mypy s-এর type হিসাব করল Student | None. এখনো কোনো সমস্যা নেই — একটি “হয়তো-None” মান রাখা নিষেধ নয়, ব্যবহার করার আগে পরীক্ষা করতে হবে।
  3. পরীক্ষা ছাড়াই ব্যবহার — ধরা পড়ল। s.name: কিন্তু s যদি None হয়, তার name নেই। mypy বলছে, union-এর “None” অংশে name নেই। চালানোর সময় এটিই হতো AttributeError: 'NoneType' object has no attribute 'name' — শুধু যেদিন কেউ ভুল রোল দিত।
  4. if s is None — type সংকুচিত। mypy পরীক্ষাটি বোঝে। if-এর ভেতরে সে জানে s নিশ্চিতভাবে None. এখানে s.name লিখলে আবার error দিত। এই শাখায় আমরা শুধু একটি বার্তা দেখাচ্ছি।
  5. else-এ শুধু Student. union থেকে None বাদ পড়েছে, বাকি থাকল Student. এখন s.name নিরাপদ, mypy খুশি। একই কাজ হতো if s is None: return লিখে আগেভাগে বেরিয়ে গেলেও (পর্ব ১৩-এর guard clause) — তার পরের সব লাইনে s হতো Student.

mypy যে পরীক্ষাগুলো বুঝে type সংকুচিত করে, তার মধ্যে আছে if x is None, if x is not None, if x:, isinstance(x, str), আর পর্ব ১১-এর match-এর case. তাই int | str নেওয়া একটি ফাংশনে if isinstance(value, str):-এর ভেতরে value.upper() বৈধ, বাইরে নয়।

if s: আর if s is not None: এক নয়: type narrowing-এ দুটিই None বাদ দেয়, কিন্তু অর্থে পার্থক্য আছে। পর্ব ০৮-এর truthiness মনে করুন — 0, "", খালি list-ও False. def find_marks(roll: int) -> int | None যদি কারও জন্য 0 ফেরত দেয়, তাহলে if marks: সেই ছাত্রকে “পাওয়া যায়নি” ভেবে বাদ দেবে। None পরীক্ষার জন্য সবসময় is None / is not None লিখুন।

৫ · নিজের type বানানো — alias, TypedDict, Literal, Final

Type alias — লম্বা type-এর একটি নাম

কখনো একই জটিল type বারবার লিখতে হয়, বা একটি সাধারণ type-কে অর্থপূর্ণ নাম দিতে চান। Python 3.12 থেকে (PEP 695) এর জন্য আছে type statement:

type Taka = int                              # পয়সা নয়, টাকা
type RateTable = dict[str, float]            # মুদ্রা → টাকায় দাম
type Handler = Callable[[str], None]

def convert(amount: float, table: RateTable, code: str) -> Taka:
    return round(amount * table[code])

এভাবে বানানো নামকে বলে type alias. লক্ষ করুন, Taka আর int mypy-র চোখে একই — alias শুধু একটি ডাকনাম, নতুন type নয়। তাই একটি সাধারণ int দিলেও mypy আপত্তি করবে না। (পুরোনো কোডে alias লেখা হতো সাধারণ assignment দিয়ে — RateTable = dict[str, float] — বা Python 3.10-এর TypeAlias দিয়ে। আর সত্যিই আলাদা type চাইলে আছে typing.NewType, যেখানে সাধারণ int দিলে mypy আপত্তি করে।)

TypedDict — JSON-এর মতো dict-এর আকার

পর্ব ২৬-এ দেখেছেন, API বা JSON ফাইল থেকে আসা ডেটা Python-এ হয়ে যায় dict. ধরুন একটি বিনিময় হারের API এমন উত্তর দেয়: {"currency": "USD", "bdt": 122.5, "date": "2026-10-18"}. dict[str, float] লেখা যাবে না, কারণ ভেতরের মানগুলো ভিন্ন ভিন্ন type-এর। dict[str, Any] লিখলে mypy অন্ধ হয়ে যায়। সমাধান TypedDict — প্রতিটি চাবির নাম আর তার মানের type আলাদাভাবে লেখা (Python 3.8 থেকে):

from typing import TypedDict, NotRequired

class Rate(TypedDict):
    currency: str
    bdt: float
    date: str
    note: NotRequired[str]          # এই চাবি না-ও থাকতে পারে

def to_taka(amount: float, rate: Rate) -> float:
    return amount * rate["bdt"]     # mypy জানে এটি float
    # rate["bdtt"] লিখলে: error: TypedDict "Rate" has no key "bdtt"

চালানোর সময় Rate একটি সাধারণ dict-ই — নতুন কোনো object তৈরি হয় না, কোনো খরচও নেই। কিন্তু mypy প্রতিটি চাবির বানান আর মানের type যাচাই করে। পর্ব ৩২-এর dataclass আর typing.NamedTuple-ও একই রকম hint দিয়ে লেখা, তবে সেগুলো সত্যিকারের নতুন class বানায়, যার attribute ডট দিয়ে পড়া হয় (rate.bdt). নিয়ম: ডেটা বাইরে থেকে dict হিসেবে আসে আর dict হিসেবেই যায় — TypedDict; নিজের প্রোগ্রামের ভেতরের রেকর্ড — dataclass.

Literal আর Final

from typing import Final, Literal

type Method = Literal["bkash", "nagad", "card"]
VAT_RATE: Final = 0.15

def pay(amount: int, method: Method) -> str:
    return f"{amount} টাকা, {method}"

pay(500, "bkash")     # ঠিক
pay(500, "bkas")      # mypy: incompatible type "Literal['bkas']"
VAT_RATE = 0.2        # mypy: Cannot assign to final name "VAT_RATE"

Literal বলে “শুধু এই নির্দিষ্ট মানগুলোর একটি” — একটি বানান ভুল, যা সাধারণ str hint কখনো ধরত না, এখন চালানোর আগেই ধরা পড়ে। Final বলে “এই নামে আর কখনো নতুন মান বসানো যাবে না” — Python-এ ধ্রুবক (constant) বলে আলাদা কিছু নেই, বড় হাতের নাম শুধু একটি রীতি; Final সেই রীতিকে type checker দিয়ে পাহারা দেয়। দুটিই এসেছে Python 3.8-এ। আর দুটিই শুধু mypy-র নিয়ম — চালানোর সময় VAT_RATE = 0.2 দিব্যি চলবে।

৬ · Generic — def first[T] আর Protocol

একটি ছোট সহায়ক ফাংশন লিখতে চান: যেকোনো list-এর প্রথম উপাদান দেবে। hint কী হবে?

  • def first(items: list[str]) -> str — শুধু string-এর list চলবে, int-এর list নয়।
  • def first(items: list[Any]) -> Any — সব চলবে, কিন্তু ফল Any; ফেরত পাওয়া মান নিয়ে mypy আর কিছুই যাচাই করবে না।
  • def first(items: list[object]) -> object — ফল দিয়ে প্রায় কিছুই করা যাবে না; আর invariance-এর কারণে list[str] পাঠানোও যাবে না।

আসলে আমরা যা বলতে চাই, তা হলো একটি সম্পর্ক: “যে type-এর list দেবে, সেই type-এর একটি মান ফেরত পাবে”। এর জন্য দরকার একটি type variable — type-এর জায়গায় বসানো একটি নাম, যা প্রতিবার ডাকার সময় আসল type দিয়ে পূরণ হয়। Python 3.12 থেকে (PEP 695) লেখা হয় ফাংশনের নামের পরে বর্গ বন্ধনীতে:

def first[T](items: list[T]) -> T:
    return items[0]

# Python 3.11 বা আগের ধরন — একই অর্থ:
from typing import TypeVar
T = TypeVar("T")
def first_old(items: list[T]) -> T:
    return items[0]

[T] অংশটিকে বলে type parameter. ফাংশনের সাধারণ parameter যেমন মান নেয়, type parameter তেমন type নেয় — তবে সেটি আপনাকে বলে দিতে হয় না, mypy argument দেখে নিজেই বের করে। ধাপে ধাপে দেখুন T কীভাবে “প্রবাহিত” হয়:

1def first[T](items: list[T]) -> T:2    return items[0]34cities = ["ঢাকা", "খুলনা"]5c = first(cities)6print(c.upper())7n = first([10, 20])8print(n + 1)9print(n.upper())
type checker-এর হিসাব
T
items
ফল
  1. T একটি ফাঁকা ঘর। সংজ্ঞায় T কোনো নির্দিষ্ট type নয়। mypy শুধু সম্পর্কটি মনে রাখে: “items-এর ভেতরের type যা, ফেরত type-ও তা-ই”। ফাংশনের ভেতরে items[0]-এর type T, আর ফেরত দেওয়ার কথাও T — তাই সংজ্ঞাটি নিজে বৈধ।
  2. একটি list তৈরি হলো। cities = ["ঢাকা", "খুলনা"] — কোনো hint লেখা নেই, তবু mypy নিজেই অনুমান করল list[str]. প্রতিটি ভ্যারিয়েবলে hint লিখতে হয় না; mypy ডান পাশ দেখে বুঝে নেয়।
  3. T = str. first(cities) ডাকার সময় mypy list[T]-কে list[str]-এর সাথে মেলাল, ফলে T = str. ফেরত type T, অর্থাৎ c: str. তাই c.upper() বৈধ — editor-ও এখানে string-এর সব method দেখাবে।
  4. নতুন ডাক, নতুন T. first([10, 20])-এ T আলাদাভাবে পূরণ হলো: int. প্রতিটি ডাকের জন্য T আলাদা — একটি ফাংশন, অনেক type, কিন্তু প্রতিবার সম্পর্কটি অটুট। n + 1 বৈধ।
  5. লাভটা এখানে। n.upper() — mypy জানে n একটি int, তাই আটকে দিল। যদি first-এর ফেরত type Any হতো, এই লাইন নিঃশব্দে পার পেয়ে চালানোর সময় AttributeError দিত। Generic মানে নমনীয়তা আর যাচাই — দুটোই।

একই syntax-এ class-ও generic হতে পারে — class Box[T]: — যেমন Python-এর নিজের list, dict. আর T-কে সীমিত করা যায়: def biggest[T: (int, float)](xs: list[T]) -> T মানে T শুধু int বা float হতে পারে।

Protocol — আকার দিয়ে চুক্তি (পর্ব ৩৩-এর পুনরালোচনা)

পর্ব ৩৩-এ দেখেছেন, Protocol দিয়ে লেখা যায় “যার area() method আছে, সে-ই চলবে” — inheritance ছাড়াই। এটি structural typing, আর এর পুরো যাচাই করে type checker. Generic আর Protocol মিলিয়ে খুব শক্তিশালী চুক্তি লেখা যায়, যেমন def total_area(shapes: Iterable[HasArea]) -> float — “যেকোনো iterable, যার প্রতিটি উপাদানের area আছে”। Python-এর নিজের Iterable, Sized, Callable-কেও type checker Protocol-এর মতো দেখে, তাই __iter__ আছে এমন যেকোনো নিজের class Iterable হিসেবে গৃহীত হয়। এভাবে type hint Python-এর duck typing-কে বাতিল করে না — লিখিত রূপ দেয়।

৭ · বাস্তব প্রজেক্টে mypy — reveal_type, strict আর ধাপে ধাপে গ্রহণ

reveal_type — mypy কী ভাবছে, জিজ্ঞেস করা

mypy যখন অবাক করা error দেয়, প্রথম প্রশ্ন: “এই লাইনে সে এই নামের type কী ভাবছে?” উত্তর জানতে কোডে লিখুন reveal_type(x), তারপর mypy চালান — সে একটি “note” দিয়ে বলে দেবে। (reveal_type Python 3.11 থেকে typing-এ একটি সাধারণ ফাংশনও, চালালে type ছেপে মান ফেরত দেয়; তবে আসল ব্যবহার mypy-র সাথে, আর কাজ শেষে লাইনটি মুছে ফেলুন।) JSON থেকে পড়া বিনিময় হারের উদাহরণে দেখুন:

1data = json.loads(text)2reveal_type(data)3data["bdt"].uper()           # বানান ভুল!45rate: Rate = json.loads(text)6reveal_type(rate)7reveal_type(rate["bdt"])8rate["bdt"].uper()
mypy-র চোখে type
data
rate
বাগ ধরা পড়ল?
  1. json.loads ফেরত দেয় Any. JSON থেকে কী আসবে তা আগে জানা অসম্ভব — dict, list, সংখ্যা, যেকোনো কিছু। তাই standard library-র hint-এ json.loads-এর ফেরত type Any. reveal_type ঠিক এটাই জানাল। (বাস্তব আউটপুটে type-এর নাম ডাবল কোটেশনে থাকে।)
  2. Any-র অন্ধকার। data["bdt"].uper() — দুটি বাগ: bdt একটি সংখ্যা, তার কোনো upper নেই; আর বানানও ভুল। তবু mypy চুপ! কারণ Any-র ওপর সব কিছু বৈধ। এই লাইন চালানোর সময়ই প্রথম ধরা পড়ত।
  3. hint দিয়ে আকার বলে দেওয়া। rate: Rate = json.loads(text) — Any যেকোনো type-এ বসানো যায়, তাই mypy মেনে নিল, আর এখন থেকে rate-কে Rate নামের TypedDict হিসেবে দেখবে। reveal_type পুরো আকারটি দেখাল।
  4. প্রতিটি চাবির type জানা। rate["bdt"]-এর type এখন float (builtins হলো Python-এর built-in type-গুলোর module-এর নাম)। editor-ও এখানে float-এর method দেখাবে।
  5. এবার বাগ ধরা পড়ল। একই ভুল লাইন, কিন্তু এখন mypy জানে এটি float — error. শিক্ষা: Any যত তাড়াতাড়ি সম্ভব একটি সত্যিকারের type-এ বদলান। তবে মনে রাখুন, ধাপ ৩-এ mypy আপনার কথা বিশ্বাস করেছে মাত্র — JSON-এ সত্যিই bdt একটি সংখ্যা কি না, তা কেউ যাচাই করেনি।

বাইরের ডেটায় hint মানে প্রতিশ্রুতি, প্রমাণ নয়: API যদি কোনোদিন "bdt": "122.5" (string) পাঠায়, rate: Rate লেখা কোড mypy পার হয়ে চালানোর সময় ভুল করবে। সীমানায় — যেখানে ডেটা প্রোগ্রামে ঢোকে — চালানোর সময় যাচাই দরকার: নিজে isinstance পরীক্ষা, অথবা pydantic-এর মতো লাইব্রেরি (অংশ ৮)।

Strict mode আর কনফিগারেশন

সাধারণ অবস্থায় mypy বেশ উদার। সবচেয়ে গুরুত্বপূর্ণ উদারতা: যে ফাংশনে কোনো hint নেই, তার ভেতরের কোড mypy যাচাই করে না — সব parameter ধরে নেয় Any. এটি ইচ্ছাকৃত, পুরোনো কোডে hint ছাড়া অংশ যেন হাজারো error না দেয়। নতুন প্রজেক্টে বা পুরো hint দেওয়া module-এ চাই কঠোরতা:

mypy --strict src/

Strict mode একসাথে অনেকগুলো কঠোর নিয়ম চালু করে — hint ছাড়া ফাংশন নিষেধ, অকারণ Any ফেরত দিলে সতর্কতা, অপ্রয়োজনীয় # type: ignore থাকলে সতর্কতা, ইত্যাদি। নিয়মগুলো প্রতিবার কমান্ডে না লিখে পর্ব ২৩-এর pyproject.toml-এ রাখা যায়:

[tool.mypy]
python_version = "3.12"
check_untyped_defs = true

[[tool.mypy.overrides]]
module = "shop.billing.*"
strict = true

পুরোনো প্রজেক্টে ধাপে ধাপে

পঞ্চাশ হাজার লাইনের একটি পুরোনো প্রজেক্টে একদিনে সব hint লেখা অসম্ভব। দরকারও নেই — এখানেই gradual typing-এর শক্তি: hint দেওয়া আর না দেওয়া কোড পাশাপাশি থাকতে পারে, আর Any দুই জগতের সেতু। একটি পরীক্ষিত পথ:

  1. প্রথম দিন: mypy সাধারণ অবস্থায় চালান। hint ছাড়া ফাংশনগুলো সে এড়িয়ে যাবে, তাই error হবে অল্প — আর যা আসবে, তা প্রায়ই সত্যিকারের বাগ।
  2. সীমানা থেকে শুরু: সবচেয়ে বেশি ব্যবহৃত ফাংশন, public API, আর যে module সবচেয়ে বেশি বাগ দেয় — সেখানে আগে hint দিন। একটি ফাংশনের hint তার সব caller-কে যাচাইয়ের আওতায় আনে।
  3. পুরোনো error আপাতত চাপা: যে লাইন এখনই ঠিক করা সম্ভব নয়, সেখানে # type: ignore[arg-type] — কোডসহ, যাতে শুধু ওই error চাপা পড়ে। পরে এগুলো খুঁজে খুঁজে সারান।
  4. module ধরে কঠোরতা বাড়ানো: একটি module পুরো hint পেলে overrides-এ তাকে strict করুন — সে আর পিছিয়ে যেতে পারবে না।
  5. CI-তে বসানো: প্রতিটি pull request-এ mypy চলুক (পর্ব ৩৯-এ টেস্টের সাথে)। তাহলে নতুন কোড কখনো পুরোনো মানের নিচে নামে না।

বাইরের লাইব্রেরির hint কোথা থেকে আসে? অনেক লাইব্রেরি নিজেই hint দেয়। যারা দেয় না, তাদের জন্য আলাদা stub file (.pyi) থাকে — শুধু সংজ্ঞা আর hint, শরীর ছাড়া। Python-এর standard library-র stub থাকে typeshed নামের একটি সম্প্রদায়-চালিত প্রজেক্টে, যা mypy আর pyright দুজনেই ব্যবহার করে; অনেক জনপ্রিয় লাইব্রেরির stub পাওয়া যায় types-requests-এর মতো প্যাকেজে।

৮ · চালানোর সময় hint — __annotations__, lazy evaluation, pydantic

Python hint যাচাই করে না, কিন্তু ফেলেও দেয় না। প্রতিটি ফাংশন, class আর module-এর annotation জমা থাকে একটি dict-এ, যার নাম __annotations__. যেকোনো প্রোগ্রাম চালানোর সময় এটি পড়তে পারে — এক ধরনের introspection. পড়ার সবচেয়ে নির্ভরযোগ্য উপায় typing.get_type_hints(), যা string আকারে লেখা hint-কেও আসল type-এ বদলে দেয়।

Python 3.14 — annotation এখন অলস

পুরোনো একটি ঝামেলা দিয়ে শুরু করি। ধরুন get_student ফাংশনটি ফাইলের ওপরে, আর Student class নিচে। Python 3.13 পর্যন্ত def get_student(roll: int) -> Student | None: লেখামাত্র NameError হতো — কারণ def চলার মুহূর্তেই annotation হিসাব হতো, আর তখনো Student তৈরি হয়নি। সমাধান ছিল দুটি: hint-কে string বানানো (-> "Student | None"; একে বলে forward reference), অথবা ফাইলের শুরুতে from __future__ import annotations (Python 3.7 থেকে, PEP 563), যা সব annotation-কে নিঃশব্দে string বানিয়ে রাখে।

Python 3.14-এ (PEP 649 আর PEP 749) নিয়ম বদলেছে: annotation আর def বা class চলার সময় হিসাব হয় না। Python একটি ছোট ফাংশনে সেগুলো মুড়ে রাখে, আর প্রথমবার কেউ __annotations__ চাইলে তখন হিসাব করে — এটি lazy evaluation. ফলে পরে সংজ্ঞায়িত class-এর নাম quote ছাড়াই লেখা যায়, আর যে প্রোগ্রাম annotation পড়েই না, সে তা হিসাবের খরচও দেয় না। (তবে নাম তখনো না থাকলে __annotations__ পড়ার মুহূর্তে NameError আসবে; নতুন annotationlib module সেই অবস্থাতেও পড়ার উপায় দেয়।) from __future__ import annotations 3.14-এও কাজ করে, তবে নতুন কোডে আর দরকার নেই।

নিচের প্রোগ্রামটি Python 3.14-এ চলে। লক্ষ করুন, get_student-এর hint-এ Student ব্যবহার হয়েছে তার সংজ্ঞার আগেই:

from typing import TypedDict, get_type_hints

def get_student(roll: int) -> Student | None:    # Student এখনো নেই!
    return students.get(roll)

class Student:
    def __init__(self, name: str) -> None:
        self.name = name

class Rate(TypedDict):
    currency: str
    bdt: float

def first[T](items: list[T]) -> T:
    return items[0]

students = {7: Student("রিনা")}
print(get_type_hints(get_student))
print(Rate.__annotations__)
r: Rate = {"currency": "USD", "bdt": "অনেক"}      # ভুল type, তবু চলে
print(type(r).__name__, r["bdt"])
print(first.__type_params__, first(["ঢাকা", "খুলনা"]), get_student(8))

প্রথম লাইন {'roll': <class 'int'>, 'return': __main__.Student | None} — def-এর সময় Student ছিল না, কিন্তু Python 3.14 তখন hint হিসাবই করেনি; get_type_hints ডাকার সময় class তৈরি হয়ে গেছে, তাই সব ঠিকঠাক মিলল (Python 3.13-এ এই ফাইল def get_student লাইনেই NameError দিত)। দ্বিতীয় লাইন {'currency': <class 'str'>, 'bdt': <class 'float'>} — TypedDict-এর চাবিগুলো hint থেকেই জানা যায়। তৃতীয় লাইন dict অনেক — Rate চালানোর সময় একটি সাধারণ dict, আর bdt-এ string বসলেও কেউ আপত্তি করেনি; mypy অবশ্য এই লাইন আটকাত। চতুর্থ লাইন (T,) ঢাকা None — first-এর type parameter চালানোর সময়ও দেখা যায়, আর রোল ৮ না থাকায় get_student ফেরত দিল None, ঠিক যেমন তার hint সতর্ক করেছিল।

যে টুলগুলো চালানোর সময় hint ব্যবহার করে

যেহেতু hint পড়া যায়, কিছু লাইব্রেরি এগুলো দিয়ে সত্যিকারের কাজ করে:

  • dataclasses: পর্ব ৩২-এর @dataclass class-এর __annotations__ পড়েই জানে কোন কোন field আছে, আর সেই অনুযায়ী __init__ লেখে। তবে type যাচাই করে না — Point(x="ক") দিব্যি চলে।
  • pydantic: একটি জনপ্রিয় তৃতীয় পক্ষের লাইব্রেরি, যা hint অনুযায়ী চালানোর সময় ডেটা যাচাই আর রূপান্তর করে। bdt: float লেখা model-এ "122.5" এলে সে float বানায়, "অনেক" এলে স্পষ্ট error দেয়। বাইরের ডেটার সীমানায় ঠিক এটাই দরকার।
  • FastAPI: pydantic-এর ওপর গড়া একটি ওয়েব ফ্রেমওয়ার্ক। ফাংশনের hint দেখে সে ঠিক করে request থেকে কী পড়বে, কীভাবে যাচাই করবে, আর API-র documentation নিজেই বানায়। পর্ব ৪৮-এ এর সাথে হাতে-কলমে পরিচয় হবে।
  • functools.singledispatch: প্রথম argument-এর hint দেখে ঠিক করে কোন সংস্করণ চলবে (পর্ব ৩৮-এ functools-এর অন্য হাতিয়ারগুলো)।

কোথায় hint সবচেয়ে কাজের — আর কোথায় বাড়াবাড়ি

Hint-এর লাভ সবচেয়ে বেশি যেখানে কোড অনেক মানুষ পড়ে, অনেক দিন বাঁচে, আর অনেক জায়গা থেকে ডাকা হয়: লাইব্রেরির public ফাংশন, বড় কোডবেস, দলের প্রজেক্ট, আর যে কোড প্রায়ই refactor হয়। লাভ সবচেয়ে কম এক বিকেলের একটি ছোট স্ক্রিপ্টে, Jupyter notebook-এর পরীক্ষা-নিরীক্ষায়, বা এমন কোডে যা কাল মুছে যাবে।

বাড়াবাড়ির লক্ষণও চিনুন: x: int = 5 বা names: list[str] = []-এর বাইরে প্রতিটি local ভ্যারিয়েবলে hint (mypy ডান পাশ দেখেই বোঝে); তিন স্তরের dict[str, list[tuple[int, dict[str, Any]]]] — যা আসলে বলছে “এখানে একটি dataclass বা TypedDict দরকার”; আর hint সন্তুষ্ট করতে কোড অকারণ জটিল করা। Hint সহায়ক, শাসক নয়।

C/C++ জানলে

C আর C++-এ type বাধ্যতামূলক, আর compiler নিজেই যাচাই করে — ভুল type-এ প্রোগ্রাম তৈরিই হয় না। Python-এ hint ঐচ্ছিক, আর যাচাই করে আলাদা টুল; mypy error দিলেও python দিব্যি চালাবে। C++-এর template অনেকটা Python-এর generic-এর মতো, আর C++20-এর concepts দিয়ে template-এর ওপর compile-এর সময়ের চুক্তি লেখা যায় — Python-এর Protocol আর bounded T-এর কাছাকাছি ধারণা। পার্থক্য: C++ type দিয়ে আলাদা machine code বানায়, Python-এ একই কোড সব type-এর জন্য চলে। বিস্তারিত দেখুন C++20 concepts.

পরীক্ষা ও ইন্টারভিউতে
  • Python কি চালানোর সময় type hint যাচাই করে? — না; hint শুধু __annotations__-এ জমা থাকে, যাচাই করে mypy/pyright-এর মতো আলাদা টুল।
  • Optional[int] মানে কী? — int | None; মানটি None-ও হতে পারে (argument ঐচ্ছিক — এমন অর্থ নয়)।
  • Any আর object-এর পার্থক্য? — Any যাচাই বন্ধ করে; object সব নেয় কিন্তু তার ওপর শুধু সাধারণ কাজ করা যায়।
  • Generic function কেন দরকার? — input আর output-এর type-এর সম্পর্ক রাখতে, যেমন def first[T](items: list[T]) -> T.
  • Gradual typing কী? — hint দেওয়া আর না দেওয়া কোড একসাথে চলা, যাতে পুরোনো প্রজেক্টে ধাপে ধাপে type যোগ করা যায়।

৯ · AI-এর যুগে — AI-এর লেখা hint যাচাই করা

AI কোড লিখলে প্রায় সবসময় hint দেয় — দেখতে পেশাদার লাগে। কিন্তু hint ভুল হলে তা hint না থাকার চেয়েও খারাপ: পাঠক মিথ্যা লেবেল বিশ্বাস করে, আর type checker ভুল জিনিস যাচাই করে। AI-এর প্রশিক্ষণের ডেটায় Python 3.5–3.8 যুগের কোড প্রচুর, তাই কিছু ভুল বারবার দেখা যায়:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
Any বা dict[str, Any] সব জায়গায়দেখতে hint, আসলে যাচাই বন্ধসত্যিকারের type, TypedDict বা dataclass
from typing import List, Dict, Optionalপুরোনো ধরন; Python 3.9/3.10 থেকে লাগে নাlist[int], X | None
যে ফাংশন None দিতে পারে, তার ফেরত type শুধু Studentমিথ্যা লেবেল; caller None পরীক্ষা করবে নাStudent | None, আর caller-এ is None পরীক্ষা
“type hint দিয়েছি, তাই input যাচাই হয়ে গেছে”Python hint যাচাই করে না; ভুল ডেটা ঢুকে পড়েসীমানায় চালানোর সময় যাচাই, বা pydantic
parameter-এ list[float], যদিও ফাংশন শুধু পড়েtuple বা list[int] পাঠালে অকারণ errorSequence[float] / Iterable[float]
error চাপতে # type: ignore ছিটানোআসল বাগ লুকিয়ে যায়error-এর কারণ ঠিক করা; একান্ত দরকারে কোডসহ ignore
পুরোনো TypeVar syntax, বা generic-এর জায়গায় Anyফেরত type-এর সম্পর্ক হারিয়ে যায়Python 3.12+ হলে def f[T](...) -> T

AI-কে কোড লিখতে বলার সময় type-এর শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:

আমি Python 3.12+ আর mypy --strict ব্যবহার করি। একটি ছোট module লিখে দাও: একটি বিনিময় হারের API-র JSON উত্তর (currency, bdt, date) থেকে টাকায় রূপান্তর, আর রোল নম্বর দিয়ে ছাত্র খোঁজার একটি ফাংশন। শর্ত: ১. আধুনিক hint লেখো: list[int], dict[str, float], X | None. typing.List, Dict, Optional, Union ব্যবহার করবে না। ২. Any ব্যবহার করবে না; যেখানে না করলেই নয়, এক লাইনে কারণ লেখো। ৩. JSON উত্তরের জন্য একটি TypedDict বানাও, আর json.loads-এর ফলকে চালানোর সময় যাচাই করো (চাবি আছে কি না, bdt সংখ্যা কি না) — hint-কে যাচাই ভাববে না। ৪. যে ফাংশন কিছু না পেলে None দেয়, তার ফেরত type-এ | None লেখো, আর caller-এ is None পরীক্ষা দেখাও। ৫. যে parameter শুধু পড়া হয়, সেখানে Sequence বা Iterable দাও। ৬. একটি generic first() helper লেখো Python 3.12-এর def first[T] syntax-এ। ৭. কোনো # type: ignore দেবে না। শেষে বলো mypy --strict চালালে কোনো error আসবে কি না, আর কোন অংশ শুধু চালানোর সময় যাচাই হচ্ছে।

AI-এর লেখা কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Python 3.0-এ annotation এসেছিল ২০০৮ সালে, কিন্তু তার “অর্থ” ঠিক হলো ২০১৫ সালে। মাঝের বছরগুলোতে কী হয়েছিল, আর PEP 484 কেন এভাবে নকশা করা?

২০০৬ সালে Collin Winter আর Tony Lownds লেখেন PEP 3107 — Function Annotations, যা Python 3.0-এ (২০০৮) আসে। এর দর্শন ছিল অদ্ভুতভাবে নিরপেক্ষ: ফাংশনের parameter আর return-এ যেকোনো expression লেখা যাবে, Python সেটি __annotations__-এ জমা রাখবে, ব্যস। অর্থ কী হবে, তা ঠিক করবে তৃতীয় পক্ষের লাইব্রেরি — কেউ type checking করবে, কেউ documentation, কেউ ডেটা রূপান্তর। PEP-টি স্পষ্টভাবে কোনো মানক অর্থ দিতে অস্বীকার করেছিল, এই আশায় যে সম্প্রদায় নিজেই সেরা ব্যবহার খুঁজে নেবে।

ফল হলো প্রায় কিছুই না। পরের কয়েক বছর annotation খুব কম ব্যবহার হয়েছে। কারণ সহজ: সবাই যদি নিজের অর্থে লেখে, তাহলে কোনো টুলই নির্ভরযোগ্যভাবে অন্যের কোড বুঝতে পারে না। একটি লাইব্রেরি x: "positive" লিখলে আরেকটি x: int — দুটি একসাথে চলবে কীভাবে? মানক ছাড়া annotation একটি ফাঁকা কলম ছিল।

মোড় ঘোরে mypy দিয়ে। Jukka Lehtosalo-র mypy প্রথমে নিজস্ব syntax-এর একটি আলাদা ভাষা হিসেবে শুরু হয়েছিল; ২০১৩ সালের দিকে Guido-র পরামর্শে তিনি PEP 3107-এর annotation ব্যবহার করে সাধারণ Python-এর syntax-এ চলে আসেন। ২০১৪ সালে Guido python-ideas মেইলিং লিস্টে প্রস্তাব দেন: mypy-র type-লেখার ধরনকেই Python-এর মানক করা হোক। সেই বছরই Guido van Rossum, Jukka Lehtosalo আর Łukasz Langa লেখেন PEP 484; এটি গৃহীত হয়ে আসে Python 3.5-এ (২০১৫), সঙ্গে typing module. পাশাপাশি PEP 483 (“The Theory of Type Hints”) ধারণাগত ভিত্তি লিখে রাখে।

PEP 484-এর সবচেয়ে গুরুত্বপূর্ণ নকশা-সিদ্ধান্ত তার একটি বাক্যে: Python কখনো type hint বাধ্যতামূলক করবে না, এমনকি রীতি হিসেবেও না। Hint ঐচ্ছিক, চালানোর সময় যাচাই হয় না, আর যাচাইয়ের দায়িত্ব বাইরের টুলের। এর পেছনে দুটি যুক্তি। প্রথমত, কোটি কোটি লাইনের পুরোনো কোড ভাঙা যাবে না। দ্বিতীয়ত, Python-এর আকর্ষণ — দ্রুত লেখা, ছোট স্ক্রিপ্ট, শিক্ষা — hint বাধ্যতামূলক হলে হারিয়ে যেত। পরের বছরগুলোতে PEP 526 (ভ্যারিয়েবল hint, ৩.৬), PEP 544 (Protocol, ৩.৮), PEP 585 (list[int], ৩.৯), PEP 604 (X | Y, ৩.১০) আর PEP 695 (def f[T], ৩.১২) ধাপে ধাপে লেখাকে সহজ করেছে।

মূল উপলব্ধি: একটি ফিচারের মূল্য আসে মানক থেকে। PEP 3107 খালি জায়গা দিয়েছিল, PEP 484 তাতে একটি সাধারণ ভাষা দিল — আর সেটি এসেছিল একটি বাস্তবে কাজ করা টুল (mypy) থেকে, কমিটির কল্পনা থেকে নয়।

প্র ০২mypy একজন PhD ছাত্রের গবেষণা থেকে শুরু হয়েছিল। কীভাবে সেটি লক্ষ লক্ষ লাইনের বাণিজ্যিক কোডে পৌঁছাল, আর Dropbox-এর অভিজ্ঞতা কী শেখায়?

Jukka Lehtosalo ফিনল্যান্ডের মানুষ, PhD করছিলেন যুক্তরাজ্যের University of Cambridge-এ। তাঁর প্রশ্ন ছিল: একটি ভাষা কি একসাথে dynamic আর static হতে পারে — ছোট প্রোগ্রামে type ছাড়া দ্রুত লেখা, আর প্রোগ্রাম বড় হলে ধাপে ধাপে type যোগ করে নিরাপত্তা? ২০১২ সালের দিকে তিনি mypy শুরু করেন; প্রথমে এটি ছিল Python-এর মতো দেখতে কিন্তু আলাদা syntax-এর একটি ভাষা। PyCon US 2013-এ তাঁর সাথে কথা বলে Guido van Rossum পরামর্শ দেন — আলাদা ভাষা নয়, Python-এর annotation ব্যবহার করুন, যাতে mypy-র কোড সাধারণ Python দিয়েই চলে। এই পরামর্শই mypy-র ভাগ্য ঠিক করে দেয়: এখন যেকোনো Python প্রজেক্ট এক লাইনও না বদলে mypy চেষ্টা করতে পারত।

Guido তখন Dropbox-এ কাজ করতেন (২০১৩–২০১৯), আর Dropbox-এর সার্ভার আর ডেস্কটপ অ্যাপের বড় অংশ লেখা ছিল Python-এ। কোডবেস বাড়ার সাথে সাথে Dropbox একটি পরিচিত সমস্যায় পড়ে: নতুন প্রকৌশলীরা কোড বুঝতে হিমশিম খেতেন, আর বড় refactor মানে ভয় — কোথায় কী ভাঙবে কেউ জানে না। Jukka Dropbox-এ যোগ দেন, আর mypy সেখানে একটি পূর্ণাঙ্গ দলের প্রজেক্ট হয়ে ওঠে। ২০১৯ সালে Dropbox-এর প্রকৌশল ব্লগে Jukka লেখেন, তারা প্রায় ৪০ লাখ লাইন Python কোডে type যাচাই চালু করেছেন।

সেই অভিজ্ঞতা থেকে কয়েকটি শিক্ষা পুরো Python জগতে ছড়িয়েছে। প্রথমত, gradual adoption কাজ করে: hint ছাড়া ফাংশন এড়িয়ে যাওয়া, module ধরে কঠোরতা বাড়ানো, আর CI-তে mypy — এই পর্বের অংশ ৭-এর পথটি এভাবেই পরীক্ষিত। দ্বিতীয়ত, গতি গুরুত্বপূর্ণ: লক্ষ লক্ষ লাইনে mypy ধীর হয়ে যাচ্ছিল, তাই দলটি incremental mode (শুধু বদলানো অংশ আবার যাচাই) আর একটি daemon বানায়, আর পরে mypyc — যা type hint ব্যবহার করে Python কোডকে C extension-এ compile করে; mypy নিজেই mypyc দিয়ে compile করা, ফলে কয়েক গুণ দ্রুত। তৃতীয়ত, প্রকৌশলীরা জানান যে hint-এর সবচেয়ে বড় লাভ বাগ ধরা নয়, বরং কোড বোঝা — একটি অচেনা ফাংশনের def লাইনই অর্ধেক documentation.

Dropbox একা ছিল না। একই সময়ে Facebook (পরে Meta) বানায় Pyre, Google বানায় pytype, Microsoft বানায় pyright — বড় কোম্পানিগুলো প্রায় একই সমস্যায় প্রায় একই সমাধানে পৌঁছেছিল।

মূল উপলব্ধি: mypy-র সাফল্যের চাবি ছিল একটি নকশা-সিদ্ধান্ত — আলাদা ভাষা নয়, সাধারণ Python-এর ভেতরে ঐচ্ছিক hint. এতে বড় পুরোনো কোডবেস কোনো বিপ্লব ছাড়াই ধাপে ধাপে নিরাপদ হতে পেরেছে।

প্র ০৩“Gradual typing” একটি গবেষণা-তত্ত্বের নাম। Siek আর Taha ২০০৬ সালে কী প্রস্তাব করেছিলেন, আর Python-এর Any সেই তত্ত্বে কোথায় বসে?

প্রোগ্রামিং ভাষার জগতে দীর্ঘদিন একটি বিভাজন ছিল: static typing (C, Java, Haskell — compile-এর সময় যাচাই, নিরাপদ কিন্তু কঠোর) বনাম dynamic typing (Lisp, Smalltalk, Python — নমনীয় কিন্তু ভুল ধরা পড়ে দেরিতে)। দুটির মাঝামাঝি চেষ্টা আগেও ছিল, কিন্তু একটি পরিষ্কার তাত্ত্বিক কাঠামো দেন Jeremy Siek আর Walid Taha, ২০০৬ সালের Scheme and Functional Programming Workshop-এ তাঁদের গবেষণাপত্র “Gradual Typing for Functional Languages”-এ।

তাঁদের মূল ধারণা একটি বিশেষ “অজানা” type, যা তাঁরা ? চিহ্নে লিখতেন। এর সাথে আসে একটি নতুন সম্পর্ক — consistency (সামঞ্জস্য)। সাধারণ type system জিজ্ঞেস করে “A কি B-এর সমান বা subtype?” Gradual system জিজ্ঞেস করে “A আর B কি সামঞ্জস্যপূর্ণ?” — আর ? সবকিছুর সাথে সামঞ্জস্যপূর্ণ, দুই দিকেই। তবে সম্পর্কটি transitive নয়: int ~ ? আর ? ~ str, কিন্তু তার মানে int ~ str নয়। এই ছোট কৌশলেই একটি প্রোগ্রামের কিছু অংশ পুরো type-ওয়ালা আর কিছু অংশ type-হীন হয়ে একসাথে থাকতে পারে।

Python-এর Any হুবহু এই ?. PEP 483 আর PEP 484 সরাসরি এই তত্ত্বের উল্লেখ করে। তাই অংশ ৩-এ দেখেছেন, Any যেকোনো type-এ যায় আর যেকোনো type থেকে আসে, অথচ object শুধু এক দিকে — object হলো সাধারণ subtyping-এর চূড়া, Any হলো consistency-র জোকার।

কিন্তু তত্ত্ব আর Python-এর মধ্যে একটি বড় পার্থক্য আছে। Siek-Taha-র মূল নকশায়, typed আর untyped অংশের সীমানায় চালানোর সময় পরীক্ষা (cast) বসানো হয় — ? থেকে int-এ যাওয়া মানটি সত্যিই int কি না যাচাই হয়, ফলে typed অংশের প্রতিশ্রুতি নিশ্চিত থাকে। Typed Racket (Sam Tobin-Hochstadt ও Matthias Felleisen) এই পথে গেছে, সীমানায় contract বসিয়ে। কিন্তু ২০১৬ সালে Takikawa ও সহকর্মীদের একটি গবেষণাপত্র (“Is Sound Gradual Typing Dead?”) দেখায়, এই পরীক্ষার খরচ কিছু ক্ষেত্রে প্রোগ্রামকে বহু গুণ ধীর করে দিতে পারে। Python (আর TypeScript) উল্টো পথ নিয়েছে: চালানোর সময় কোনো পরীক্ষা নেই, hint মুছে যায় না কিন্তু কেউ মানায়ও না। একে কখনো “optional typing” বলা হয়। দাম হলো soundness: mypy “Success” বললেও Any-র সীমানা দিয়ে ভুল type ঢুকে পড়তে পারে — অংশ ৭-এর json.loads উদাহরণে ঠিক এটাই দেখেছেন।

মূল উপলব্ধি: Gradual typing-এর হৃদয় একটি “অজানা” type, যা সবকিছুর সাথে সামঞ্জস্যপূর্ণ — Python-এ তার নাম Any. Python গতি আর সরলতার জন্য সীমানার runtime পরীক্ষা বাদ দিয়েছে; তাই সীমানায় যাচাইয়ের দায়িত্ব আপনার।

প্র ০৪Microsoft কেন Python-এর জন্য নিজের type checker (pyright) বানাল? TypeScript-এর অভিজ্ঞতা Python-এর type hint-কে কীভাবে প্রভাবিত করেছে?

২০১২ সালে Microsoft প্রকাশ করে TypeScript — C# আর Turbo Pascal-এর স্থপতি Anders Hejlsberg-এর নেতৃত্বে। সমস্যাটি ছিল Python-এর মতোই: JavaScript dynamic, আর বড় ওয়েব অ্যাপে তা সামলানো কঠিন। TypeScript-এর সমাধান Python-এর পরবর্তী পথের প্রায় হুবহু পূর্বাভাস: ঐচ্ছিক type, structural typing, একটি any type, আর type চালানোর সময় মুছে যায় — যাচাই হয় শুধু আগে। TypeScript নিজেই স্বীকার করে যে তার type system ইচ্ছাকৃতভাবে পুরোপুরি sound নয়; লক্ষ্য গাণিতিক নিখুঁততা নয়, প্রোগ্রামারের উৎপাদনশীলতা। কয়েক বছরের মধ্যে TypeScript ওয়েব জগতে বিপুল জনপ্রিয় হয়, আর প্রমাণ করে যে একটি dynamic ভাষায় পরে যোগ করা type system বাস্তবে বিশাল লাভ দিতে পারে।

Microsoft-এর প্রকৌশলী Eric Traut ২০১৯ সালে প্রকাশ করেন pyright — Python-এর type checker, কিন্তু লেখা TypeScript-এ, চলে Node.js-এ। কেন আরেকটি checker, যখন mypy আছে? মূল কারণ editor. mypy নকশা করা হয়েছিল কমান্ড লাইন থেকে পুরো প্রজেক্ট যাচাইয়ের জন্য। কিন্তু editor-এ দরকার প্রতিটি অক্ষর টাইপ করার সাথে সাথে মিলিসেকেন্ডে উত্তর — অর্ধেক লেখা, ভাঙা কোডেও। pyright শুরু থেকেই এই “language server” কাজের জন্য বানানো: দরকারি অংশটুকুই অলসভাবে বিশ্লেষণ করে। ২০২০ সালে Microsoft VS Code-এর জন্য Pylance extension আনে, যার ভেতরে pyright — ফলে কোটি কোটি Python প্রোগ্রামার না জেনেই প্রতিদিন একটি type checker ব্যবহার করছেন। অংশ ২-এর শেষ ধাপের লাল দাগটি এখান থেকেই আসে।

প্রভাব শুধু টুলে নয়, ভাষাতেও। Eric Traut নিজেই PEP 695-এর লেখক — যা এনেছে def first[T] আর type Taka = int; এই syntax TypeScript, C#, Rust-এর মতো ভাষার পরিচিত ধরনের কাছাকাছি। X | None লেখার ধরনও TypeScript-এর union-এর সাথে মিলে যায়। দুটি আলাদা checker থাকায় একটি নতুন চ্যালেঞ্জও এল: একই কোডে mypy আর pyright কখনো ভিন্ন রায় দেয়। এই কারণে ২০২৩ সালে PEP 729 দিয়ে একটি Typing Council গঠিত হয়, যারা type system-এর একটি একক লিখিত specification রক্ষণাবেক্ষণ করে। আর ২০২৫ সালে Rust-এ লেখা আরও দ্রুত নতুন checker — Astral-এর ty আর Meta-র Pyrefly — পরীক্ষামূলক অবস্থায় প্রকাশ্যে এসেছে।

মূল উপলব্ধি: TypeScript দেখিয়েছিল যে dynamic ভাষায় ঐচ্ছিক, unsound কিন্তু ব্যবহারিক type system বিশাল সফল হতে পারে; Python একই পথে হেঁটেছে। আর pyright দেখিয়েছে, type hint-এর সবচেয়ে বড় ভোক্তা হয়তো CI নয় — editor, প্রতিটি কি-চাপে।

প্র ০৫PEP 563 সব annotation-কে string বানাতে চেয়েছিল, PEP 649 চেয়েছিল অলস হিসাব। এই বিতর্ক প্রায় পাঁচ বছর কেন চলল, আর Python 3.14 কী বেছে নিল?

Python 3.13 পর্যন্ত annotation হিসাব হতো def চলার মুহূর্তেই। এর দুটি সমস্যা। প্রথমত, forward reference: যে class এখনো তৈরি হয়নি (বা যে class নিজেকেই উল্লেখ করে, যেমন একটি linked list-এর next: Node), তার নাম লেখা যেত না, quote দিয়ে string বানাতে হতো। দ্বিতীয়ত, খরচ: বড় প্রজেক্টে হাজারো জটিল annotation প্রতিবার import-এর সময় হিসাব হতো, অথচ চালানোর সময় প্রায় কেউ এগুলো পড়েই না।

২০১৭ সালে Łukasz Langa লেখেন PEP 563 — Postponed Evaluation of Annotations: সব annotation কখনো হিসাব না করে শুধু string হিসেবে জমা রাখা হবে। এটি Python 3.7-এ from __future__ import annotations দিয়ে ঐচ্ছিকভাবে আসে, আর পরিকল্পনা ছিল Python 3.10-এ এটিই হবে সবার জন্য স্বাভাবিক নিয়ম। type checker-এর জন্য এটি নিখুঁত — তারা কোড পড়ে, string-এ কিছু যায় আসে না।

সমস্যা দেখা দেয় যারা চালানোর সময় annotation পড়ে — pydantic, FastAPI, dataclass-এর কিছু ব্যবহার। তাদের দরকার আসল type object, string নয়। string থেকে আবার type বানাতে get_type_hints string-টিকে eval করে, কিন্তু কোন namespace-এ? কোনো ফাংশনের ভেতরে সংজ্ঞায়িত local class-এর নাম তখন আর খুঁজে পাওয়া যায় না। ২০২১ সালের এপ্রিলে, Python 3.10-এর feature freeze-এর (প্রথম beta) কয়েক সপ্তাহ আগে, pydantic আর FastAPI-র রক্ষণাবেক্ষণকারীরা সতর্ক করেন যে এই পরিবর্তন তাদের ভেঙে দেবে। Python Steering Council শেষ মুহূর্তে PEP 563-কে স্বাভাবিক নিয়ম বানানোর সিদ্ধান্ত স্থগিত করে।

বিকল্পটি আগেই প্রস্তাবিত ছিল: Larry Hastings-এর PEP 649 — Deferred Evaluation of Annotations. এর ধারণা — annotation-কে string নয়, একটি ছোট ফাংশনে মুড়ে রাখা, আর প্রথমবার কেউ __annotations__ চাইলে তখন সেটি চালানো। এতে type checker-এর সুবিধা (forward reference চলে, শুরুতে খরচ নেই) আর runtime লাইব্রেরির সুবিধা (আসল type object, ঠিক scope-এ) দুটোই মেলে। দীর্ঘ আলোচনার পর ২০২৩ সালে Steering Council PEP 649 গ্রহণ করে; বাস্তবায়নের খুঁটিনাটি আর নতুন annotationlib module (যা দিয়ে annotation আসল মান, ForwardRef বা string — তিন রূপে পড়া যায়) ঠিক করে Jelle Zijlstra-র PEP 749. দুটি মিলে আসে Python 3.14-এ (অক্টোবর ২০২৫)। from __future__ import annotations এখনো কাজ করে; PEP 749 অনুযায়ী ভবিষ্যতে একসময় এটি deprecated করার পরিকল্পনা আছে, তবে তাড়াহুড়ো নয়।

মূল উপলব্ধি: একটি ভাষার ফিচারের অনেক ধরনের ব্যবহারকারী থাকে — এখানে static checker আর runtime লাইব্রেরি — আর একজনের জন্য নিখুঁত সমাধান অন্যজনকে ভাঙতে পারে। Python পাঁচ বছর অপেক্ষা করে এমন সমাধান বেছে নিয়েছে, যা দুই পক্ষকেই রক্ষা করে।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. চলবে, না ভাঙবে — আর mypy কী বলবে? def area(w: int, h: int) -> int: return w * h. তিনটি call: (ক) area(3, 4), (খ) area("ab", 3), (গ) area(2.5, 2). প্রতিটির জন্য বলুন Python চালালে কী হবে, আর mypy আপত্তি করবে কি না।

    (ক) চলে, ফল 12; mypy খুশি। (খ) Python চালায় আর ফল দেয় "ababab" — string-কে int দিয়ে গুণ করা বৈধ! কোনো error নেই, চুপচাপ ভুল ফল। mypy আটকাবে: Argument 1 to "area" has incompatible type "str"; expected "int". (গ) Python চালায়, ফল 5.0 — অথচ hint বলেছিল int ফেরত আসবে। mypy আটকাবে, কারণ float চাওয়া জায়গায় int চলে, কিন্তু উল্টোটা নয়: int চাওয়া জায়গায় float চলে না। শিক্ষা: Python hint দেখে না; ভুল type কখনো ক্র্যাশ নয়, নিঃশব্দে ভুল ফল দেয়।

  2. hint দেখে বাগ খুঁজুন: def find_phone(name: str, book: dict[str, str]) -> str | None: return book.get(name). একজন লিখেছেন print(find_phone("রিনা", book).replace("-", "")). mypy কী বলবে, চালানোর সময় কখন ভাঙবে, আর দুই রকমভাবে কীভাবে ঠিক করবেন?

    mypy: Item "None" of "str | None" has no attribute "replace" [union-attr]. চালানোর সময় শুধু তখন ভাঙবে, যখন বইয়ে “রিনা” নেই — AttributeError: 'NoneType' object has no attribute 'replace'. ঠিক করার এক উপায়: phone = find_phone(...), তারপর if phone is None: print("নম্বর নেই"), else-এ replace — narrowing. আরেক উপায়: ফাংশনের নকশা বদলানো — না পেলে None না দিয়ে KeyError বা নিজের exception তোলা, তখন ফেরত type শুধু str. কোনটি ভালো, তা নির্ভর করে “না পাওয়া” স্বাভাবিক ঘটনা নাকি ব্যতিক্রম, তার ওপর।

  3. পুরোনো hint আধুনিক করুন: AI এই লাইনটি দিয়েছে: from typing import List, Dict, Optional, Any আর def summary(rows: List[Dict[str, Any]], limit: Optional[int] = None) -> Dict[str, float]:. Python 3.12-এর ধরনে আবার লিখুন। rows-এর প্রতিটি dict-এ যদি সবসময় name (str) আর amount (float) থাকে, তাহলে Any কীভাবে সরাবেন? ফাংশনটি rows শুধু পড়ে।

    প্রথম ধাপ: def summary(rows: list[dict[str, Any]], limit: int | None = None) -> dict[str, float]: — typing থেকে শুধু Any লাগে। দ্বিতীয় ধাপ: class Row(TypedDict): name: str; amount: float (আলাদা লাইনে), আর parameter rows: Sequence[Row] — Sequence কারণ ফাংশন শুধু পড়ে, তাই tuple-ও পাঠানো যাবে আর invariance-এর ঝামেলা নেই। এখন row["amount"] mypy-র চোখে float, আর row["amout"] বানান ভুল সঙ্গে সঙ্গে ধরা পড়বে। Any সরানো মানে mypy-কে চোখ ফিরিয়ে দেওয়া।

  4. একটি generic helper লিখুন: last_or_default নামে একটি ফাংশন লিখুন — একটি list আর একটি default মান নেবে; list খালি না হলে শেষ উপাদান, খালি হলে default ফেরত দেবে। Python 3.12-এর generic syntax ব্যবহার করুন, তারপর ভাবুন: last_or_default([], None)-এর ফেরত type কী হওয়া উচিত?

    একটি সম্ভাব্য সমাধান:

    def last_or_default[T, D](items: list[T], default: D) -> T | D:
        return items[-1] if items else default
    
    prices: list[float] = [120.0, 95.5]
    p = last_or_default(prices, None)     # mypy: float | None
    print(p)                              # 95.5

    দুটি type parameter লাগল: T (list-এর উপাদান) আর D (default-এর type), কারণ default-এর type list-এর type থেকে আলাদা হতে পারে — যেমন None. ফেরত type T | D, তাই last_or_default(prices, None)-এর ফল float | None, আর mypy আপনাকে None পরীক্ষা করতে বাধ্য করবে। শুধু [T] দিয়ে default: T লিখলে None পাঠানো যেত না (বা T অপ্রত্যাশিতভাবে চওড়া হয়ে যেত)। reveal_type(p) লিখে mypy চালিয়ে নিজের উত্তর মিলিয়ে নিন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পুরোনো, hint-হীন Python প্রজেক্টে ধাপে ধাপে mypy চালু করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📦 virtual environment-এ mypy install করা
  2. 🔍 সাধারণ অবস্থায় mypy চালিয়ে প্রথম error-গুলো দেখা
  3. 🏷️ সবচেয়ে বেশি ব্যবহৃত public ফাংশনে hint যোগ করা
  4. 🔒 পুরো hint পাওয়া module-কে strict করা
  5. 🤖 প্রতিটি pull request-এ CI-তে mypy চালানো

Python-এ def add_vat(price: float) -> float: return price * 1.15 সংজ্ঞায়িত। কোনো type checker ছাড়া সাধারণভাবে add_vat("500") চালালে কী ঘটে?

  • ফাংশন ডাকার মুহূর্তেই hint না মেলায় TypeError আসে
  • ফাংশন চলতে শুরু করে, আর গুণ করার লাইনে TypeError আসে
  • Python "500"-কে নিজে float-এ বদলে 575.0 দেয়
  • প্রোগ্রাম শুরুর আগেই SyntaxError আসে

Python 3.10 বা পরের version-এ “একটি Student object, অথবা None” বোঝাতে কোন type hint সবচেয়ে আধুনিক ও সঠিক?

  • tuple[Student, None]
  • Student
  • Any
  • Student | None

Type hint-এ Any আর object-এর মূল পার্থক্য কোনটি?

  • Any-র ওপর যেকোনো operation বৈধ ধরা হয়, object-এর ওপর শুধু সব object-এর সাধারণ operation
  • Any সব মান নেয়, object শুধু নিজের তৈরি class-এর মান নেয়
  • object চালানোর সময় যাচাই হয়, Any হয় না
  • কোনো পার্থক্য নেই, দুটি একই জিনিসের দুই নাম

Python 3.12-এর syntax-এ def first[T](items: list[T]) -> T সংজ্ঞায়িত। first(["ঢাকা", "খুলনা"])-এর ফলের type mypy কী ধরবে?

  • Any
  • list[str]
  • str
  • object

mypy সাধারণ (strict নয়) অবস্থায় এমন একটি ফাংশনের ক্ষেত্রে কী করে, যার কোনো parameter বা return-এ type hint নেই?

  • “hint নেই” বলে error দেয়
  • parameter-গুলোকে Any ধরে, আর ভেতরের কোড সাধারণত যাচাই করে না
  • ফাংশনটি একবার চালিয়ে type অনুমান করে
  • পুরো ফাইলটি যাচাই থেকে বাদ দেয়

Python 3.14-এ একটি ফাংশনের return annotation-এ এমন একটি class-এর নাম লেখা হয়েছে, যা ফাইলে পরে সংজ্ঞায়িত। def লাইন চলার সময় কী ঘটে?

  • NameError আসে, কারণ নামটি তখনো তৈরি হয়নি
  • annotation-টি স্বয়ংক্রিয়ভাবে একটি string-এ বদলে যায়
  • কিছুই হিসাব হয় না; প্রথমবার __annotations__ পড়ার সময় হিসাব হয়
  • annotation-টি নিঃশব্দে মুছে ফেলা হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Hint = লেবেল, পাহারাদার নয়: Python চালানোর সময় hint যাচাই করে না। পড়ে মানুষ, editor (autocomplete, লাল দাগ) আর type checker (mypy, pyright) — কোড না চালিয়েই।
  2. আধুনিক ধরন: list[int], dict[str, float], tuple[int, ...], X | None, Callable[[int], str]. parameter-এ উদার (Sequence, Iterable), return-এ নির্দিষ্ট।
  3. None-এর বাগ: “হয়তো-None” ফেরত দিলে | None লিখুন; caller-এ if x is None পরীক্ষার পর mypy type সংকুচিত করে। Any যাচাই বন্ধ করে — কম ব্যবহার করুন।
  4. নিজের type: type Taka = int, JSON-এর জন্য TypedDict, নির্দিষ্ট মানের জন্য Literal, ধ্রুবকের জন্য Final, আর সম্পর্ক রাখতে generic def first[T](items: list[T]) -> T.
  5. বাস্তবে: reveal_type দিয়ে mypy-র ভাবনা দেখুন; পুরোনো প্রজেক্টে ধাপে ধাপে, পরে strict আর CI. Python 3.14-এ annotation অলসভাবে হিসাব হয়; বাইরের ডেটা চালানোর সময় যাচাই করুন (pydantic).

পরের পর্বে: এই পর্বে Callable আর lambda দিয়ে ফাংশনকে argument হিসেবে পাঠিয়েছেন। এবার সেই ভাবনাকে আরও এগিয়ে নেব — ছোট নামহীন ফাংশন, functools-এর partial, reduce, cache, আর itertools দিয়ে লুপ ছাড়াই ডেটা-প্রবাহ: lambda, functools আর itertools.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান