Type Hints و Static Typing
Type Hints (یا Type Annotations) از پایتون 3.5 معرفی شدند و امکان مشخص کردن نوع متغیرها، پارامترها و خروجی توابع را میدهند. در این فصل با ماژول typing، ابزار mypy، Generics، Protocol و TypedDict آشنا میشویم.
Type Hints (یا Type Annotations) از پایتون 3.5 معرفی شدند و امکان مشخص کردن نوع متغیرها، پارامترها و خروجی توابع را میدهند. در این فصل با ماژول typing، ابزار mypy، Generics، Protocol و TypedDict آشنا میشویم.
چرا Type Hints؟
- کاتچ کردن خطاها قبل از اجرا (با mypy)
- IDE هوشمند: autocomplete و refactoring بهتر
- مستندات خودکار
- کد قابل فهمتر در پروژههای بزرگ
نکته مهم: پایتون بهصورت پیشفرض type hints را در زمان اجرا چک نمیکند. این فقط یک annotation است. برای چک، از ابزارهای static analysis مثل mypy استفاده میشود.
مبانی Type Hints
# متغیرها
name: str = "علی"
age: int = 30
price: float = 99.99
is_active: bool = True
# توابع
def greet(name: str, age: int) -> str:
return f"{name} ({age} ساله)"
def add(a: int, b: int) -> int:
return a + b
# بدون return type
def log(message: str) -> None:
print(message)
# با parameter پیشفرض
def fetch(url: str, timeout: int = 10) -> dict:
pass
Generic Types
از پایتون 3.9 میتوان مستقیم از انواع built-in بهصورت generic استفاده کرد:
# Python 3.9+
def get_users() -> list[str]:
return ["علی", "سارا"]
def get_prices() -> dict[str, float]:
return {"فرش": 5_000_000.0, "گلیم": 1_500_000.0}
scores: list[int] = [85, 92, 78]
config: dict[str, str | int] = {"name": "app", "port": 8080}
# Tuple - با طول ثابت
point: tuple[int, int] = (10, 20)
rgba: tuple[int, int, int, int] = (255, 128, 0, 200)
# Tuple با هر تعداد
log: tuple[str, ...] = ("INFO", "ERROR", "DEBUG") # هر تعداد str
# Set
unique_ids: set[int] = {1, 2, 3}
برای Python 3.8 و قبل
from typing import List, Dict, Tuple, Set
def get_users() -> List[str]: ...
def get_prices() -> Dict[str, float]: ...
point: Tuple[int, int] = (10, 20)
Optional و Union
# Optional - یعنی None هم ممکن است
def find_user(user_id: int) -> str | None:
if user_id == 1:
return "علی"
return None
# معادل قدیمی
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]: ...
# Union - چند نوع ممکن
def parse(value: int | float | str) -> str:
return str(value)
# قدیمی
def parse(value: Union[int, float, str]) -> str: ...
# Literal - فقط مقادیر خاص
from typing import Literal
def set_mode(mode: Literal["read", "write", "append"]) -> None:
pass
set_mode("read") # OK
# set_mode("delete") # mypy خطا
Callable – توابع بهعنوان پارامتر
from typing import Callable
# تابعی که تابع میگیرد
def apply(func: Callable[[int, int], int], x: int, y: int) -> int:
return func(x, y)
def add(a: int, b: int) -> int:
return a + b
result = apply(add, 3, 5) # 8
# هر signature
handler: Callable[..., None] # هر ورودی، خروجی None
# تابع بدون پارامتر
callback: Callable[[], str]
Generics سفارشی – TypeVar
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
"""اولین آیتم - نوع خروجی برابر با نوع آیتم لیست"""
return items[0]
# پایتون متوجه میشود
x: int = first([1, 2, 3])
y: str = first(["a", "b"])
# محدود کردن TypeVar
Number = TypeVar("Number", int, float)
def maximum(a: Number, b: Number) -> Number:
return a if a > b else b
maximum(1, 2) # int
maximum(1.5, 2.7) # float
# maximum("a", "b") # mypy خطا
Generic Classes
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
# استفاده با نوع مشخص
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
x: int = int_stack.pop() # 2
str_stack: Stack[str] = Stack()
str_stack.push("hello")
Python 3.12+ – سینتکس جدید
# Python 3.12+ - بدون TypeVar
def first[T](items: list[T]) -> T:
return items[0]
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
Protocol – Duck Typing تایپمحور
Protocol اجازه میدهد بدون وراثت، نیازمندیها را تعریف کنیم:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("○")
class Square:
def draw(self) -> None:
print("□")
def render(shape: Drawable) -> None:
shape.draw()
# بدون نیاز به ارثبری از Drawable
render(Circle()) # ○
render(Square()) # □
runtime_checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Comparable(Protocol):
def __lt__(self, other) -> bool: ...
# حالا isinstance هم کار میکند
print(isinstance(5, Comparable)) # True
print(isinstance("a", Comparable)) # True
TypedDict – دیکشنری با ساختار
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
is_active: bool
# استفاده
user: User = {
"id": 1,
"name": "علی",
"email": "ali@example.com",
"is_active": True
}
def greet_user(u: User) -> str:
return f"سلام {u['name']}"
greet_user(user)
# کلیدهای اختیاری (Python 3.11+)
class Config(TypedDict):
host: str
port: int
timeout: NotRequired[int] # اختیاری
@dataclass با Type Hints
from dataclasses import dataclass, field
@dataclass
class Product:
id: int
name: str
price: float
tags: list[str] = field(default_factory=list)
discount: float = 0.0
p = Product(id=1, name="فرش", price=5_000_000)
انواع پیشرفته
Final – مقدار غیرقابل تغییر
from typing import Final
MAX_RETRIES: Final = 3
API_URL: Final[str] = "https://api.example.com"
# MAX_RETRIES = 5 # mypy خطا
NewType – نوع مجزا
from typing import NewType
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
def get_user(uid: UserId) -> str:
return "user"
user_id = UserId(123)
product_id = ProductId(456)
get_user(user_id) # OK
# get_user(product_id) # mypy خطا - نوع متفاوت
# get_user(123) # mypy خطا - باید UserId باشد
Annotated – متادیتا اضافه
from typing import Annotated
# با pydantic یا FastAPI
def create_user(
name: Annotated[str, "نام کاربر، حداکثر 50 کاراکتر"],
age: Annotated[int, "سن - بزرگتر از 0"]
) -> None:
pass
mypy – بررسی Type
pip install mypy
# بررسی یک فایل
mypy script.py
# بررسی پروژه
mypy myproject/
# تنظیمات سختگیرانه
mypy --strict script.py
پیکربندی mypy.ini
[mypy]
python_version = 3.12
strict = True
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_unimported = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
[mypy-third_party.*]
ignore_missing_imports = True
نادیده گرفتن خط
x: int = "string" # type: ignore
y = some_legacy_function() # type: ignore[no-any-return]
مثال جامع
from dataclasses import dataclass
from typing import Protocol, TypeVar, Generic
T = TypeVar("T")
class Repository(Protocol, Generic[T]):
def get(self, id: int) -> T | None: ...
def save(self, item: T) -> None: ...
def list_all(self) -> list[T]: ...
def delete(self, id: int) -> bool: ...
@dataclass
class User:
id: int
name: str
email: str
class UserRepository:
def __init__(self) -> None:
self._users: dict[int, User] = {}
def get(self, id: int) -> User | None:
return self._users.get(id)
def save(self, user: User) -> None:
self._users[user.id] = user
def list_all(self) -> list[User]:
return list(self._users.values())
def delete(self, id: int) -> bool:
return self._users.pop(id, None) is not None
# تست
repo: Repository[User] = UserRepository()
repo.save(User(1, "علی", "ali@example.com"))
user = repo.get(1)
if user:
print(user.name)
بهترین شیوهها
- برای پروژههای جدید، از روز اول type hints بنویسید
- برای پروژههای قدیمی، تدریجی اضافه کنید (gradual typing)
- mypy را در CI اجرا کنید
- از سینتکس مدرن (Python 3.10+):
X | NoneبهجایOptional[X] - برای interface، Protocol بهتر از ABC است
- TypedDict برای dictهای با ساختار مشخص
- Final برای ثابتها
جمعبندی
- Type Hints کیفیت کد را بهشدت بالا میبرند
- پایتون در زمان اجرا چک نمیکند، باید از mypy استفاده کرد
- سینتکس مدرن (3.9+):
list[int]،X | None - TypeVar و Generic برای کد قابل استفاده مجدد
- Protocol برای duck typing تایپمحور
- TypedDict برای dictهای ساختارمند