AssertionError در پایتون وقتی پرتاب می‌شود که یک ادعای assert شکست بخورد؛ یعنی توسعه‌دهنده باور داشته چیزی حتماً درست است ولی واقعیت خلاف آن را نشان داده. این خطا در نگاه اول بی‌خطر به‌نظر می‌رسد، اما در محیط تولید می‌تواند کاملاً خاموش شود و برنامه را در وضعیت خطرناکی رها کند. در این مقاله، تجربه‌ام از ده‌ها پروندهٔ واقعی این خطا را با تمرکز بر تفاوت assert و raise با شما به اشتراک می‌گذارم.

AssertionError چیست و از کجا پرتاب می‌شود؟

AssertionError استثنایی است که پایتون هنگام شکست یک دستور assert پرتاب می‌کند. هر assert دو بخش دارد: یک شرط که باید درست باشد، و یک پیام اختیاری که هنگام شکست نمایش داده می‌شود. اگر شرط برقرار باشد، اجرای برنامه ادامه می‌یابد؛ اگر شکست بخورد، بلافاصله AssertionError پرتاب می‌شود.

ساختار ارث‌بری این استثنا ساده است:

BaseException
 └── Exception
      └── AssertionError

یعنی AssertionError زیرشاخهٔ Exception است، نه BaseException. اگر جایی except Exception بنویسید، این خطا هم گرفته می‌شود. مهم‌تر از این، یک ویژگی منحصربه‌فرد است که این خطا را از تمام خانوادهٔ Exception جدا می‌کند: assert دستوری است که در حالت اجرای بهینه‌شده، کاملاً حذف می‌شود. این ویژگی، منشأ باگ‌های پنهان بسیاری است. برای درک چارچوب گسترده‌تر، مدیریت خطا در پایتون و آموزش پایتون از صفر را پیشنهاد می‌کنم.

نکتهٔ ظریف اینکه assert در پایتون یک دستور (statement) است، نه یک تابع. به همین دلیل، نمی‌توانید آن را داخل یک عبارت قرار دهید. خطای زیر در پایتون ۳ قابل قبول نیست:

# SyntaxError
value = assert check(x)  # این خط اشتباه است

مفهوم «assertion» در علوم کامپیوتر به‌عنوان یک عقد طراحی (design contract) بین تابع و فراخوان شناخته می‌شود و در ویکی‌پدیا ذیل Assertion in software development توضیح داده شده است. این مفهوم ابتدا در زبان‌های سطح پایین مثل C معرفی شد و سپس به زبان‌های سطح بالا راه یافت.

assert یک «قرارداد» است، نه یک «بررسی». اگر می‌خواهید برنامه در برابر ورودی بد مقاوم شود، از raise استفاده کنید؛ assert تنها برای وضعیت‌هایی است که «هرگز نباید اتفاق بیفتند».

تفاوت بنیادی assert و raise

یکی از پرتکرارترین سردرگمی‌ها در پایتون، تفاوت دقیق assert و raise است. از نظر ظاهری، هر دو برنامه را متوقف می‌کنند، ولی از نظر معنایی و رفتاری، تفاوت‌های بنیادی دارند.

ویژگیassertraise
نوعدستور زباندستور زبان
حذف با -Oبله (کاملاً ناپدید می‌شود)خیر
هدفتست فرض‌های توسعه‌دهندهاعلام خطای واقعی
در محیط تولیدنباید استفاده شودمناسب
استثنای پرتابیفقط AssertionErrorهر استثنایی
پیام دلخواهمحدود به یک رشتههر نوع قابل نمایش

این تفاوت در کد زیر شفاف می‌شود:

# اشتباه در کد تولیدی
def divide(a, b):
    assert b != 0, "divisor must not be zero"  # با -O ناپدید می‌شود
    return a / b

# درست در کد تولیدی
def divide(a, b):
    if b == 0:
        raise ValueError("divisor must not be zero")
    return a / b

در نسخهٔ اول، اگر کد با python -O script.py اجرا شود، دستور assert کاملاً حذف می‌شود و تابع با ZeroDivisionError برنامه را متوقف می‌کند — یعنی خطای واقعی، نه خطای قابل‌فهمی که برنامه‌نویس قصد داشته. این دقیقاً همان دامی است که در بسیاری از پروژه‌ها دیده‌ام.

در مقابل، raise در تمام حالت‌ها فعال است و برنامه‌نویس می‌تواند استثنای دقیقی را انتخاب کند که معنایش با موقعیت هم‌خوانی داشته باشد. برای مثال، اگر ورودی از کاربر می‌آید و نامعتبر است، ValueError منطقی‌تر است؛ اگر نوع داده اشتباه است، TypeError؛ اگر وضعیت اجرا با قرارداد ناسازگار است، RuntimeError. این تفکیک در مقالات خطای ValueError در پایتون و خطای TypeError در پایتون به‌تفصیل بررسی شده است.

assert به‌عنوان ابزار دیباگ درونی

با این همه، assert جای خود را در کد دارد؛ ولی فقط در لایه‌های درونی که با وضعیت‌های «قطعاً غلط» سروکار دارند. مثلاً:

def _normalize_weights(weights):
    total = sum(weights)
    normalized = [w / total for w in weights]
    # فرض درونی: مجموع باید ۱ باشد
    assert abs(sum(normalized) - 1.0) < 1e-9
    return normalized

در این مثال، assert برای بررسی صحت یک محاسبهٔ درونی استفاده شده است، نه برای اعتبارسنجی ورودی کاربر. اگر منطق نرمال‌سازی خراب شود، assert آن را لو می‌دهد. این الگو در کد علمی و عددی بسیار رایج است، ولی همچنان جای بحث دارد؛ چرا که در حالت -O، این محافظ هم ناپدید می‌شود.

چرا assert در محیط تولید ناپدید می‌شود؟

این ویژگی پایتون، ریشه در طراحی زبان دارد. assert به‌عنوان یک ابزار «توسعه» طراحی شده، نه یک ابزار «اجرا». در نسخه‌های اولیهٔ پایتون، این تصمیم به‌عنوان یک بهینه‌سازی کارایی گرفته شد: در حالت اجرای بهینه، تمام assertها حذف می‌شوند تا سرعت برنامه بالا برود.

روش‌های فعال‌سازی حالت بهینه:

# اجرا با حذف تمام assertها
python -O script.py

# با بهینه‌سازی تهاجمی‌تر
python -OO script.py

# با متغیر محیطی
PYTHONOPTIMIZE=1 python script.py
PYTHONOPTIMIZE=2 python script.py

سطح -O و -OO تفاوت جزئی دارند: -OO علاوه بر حذف assert، docstringها را هم از بایت‌کد حذف می‌کند. در هر دو حالت، assert ناپدید می‌شود.

این رفتار در محیط‌های تولیدی مختلف، به شکل‌های متفاوتی فعال می‌شود:

  • Docker: اگر در ENTRYPOINT از -O استفاده کرده باشید، تمام assertها حذف می‌شوند. مخصوصاً اگر ایمیج‌های سبک مثل python:slim را با دستورات اختصاصی اجرا کنید.
  • Gunicorn: در برخی تنظیمات، گزینه‌های PYTHONOPTIMIZE به‌صورت پیش‌فرض در gunicorn.conf.py فعال می‌شود.
  • uWSGI: پارامتر optimize در فایل تنظیمات، همین اثر را دارد.
  • Heroku و سرویس‌های مشابه: برخی از این سرویس‌ها به‌صورت پیش‌فرض PYTHONOPTIMIZE=1 را ست می‌کنند.

در یکی از پروژه‌های من، یک API که روی Heroku اجرا می‌شد، به‌طور ناگهانی رفتار متفاوتی نشان داد. ریشه این بود که Heroku در دیپلوی جدید، PYTHONOPTIMIZE=1 را فعال کرده بود و یک assert مهم در لایهٔ احراز هویت ناپدید شده بود. این نوع باگ‌ها، به‌دلیل اینکه در محیط توسعه ظاهر نمی‌شوند، از سخت‌ترین انواع دیباگ هستند.

assert در محیط توسعه «دوست» شماست و در محیط تولید «غریبه»؛ تفاوت این دو، در یک سوئیچ پنهان به نام PYTHONOPTIMIZE نهفته است.

پیامد جانبی این رفتار، در کتابخانه‌های عمومی هم دیده می‌شود. اگر کتابخانه‌ای که در پروژه استفاده می‌کنید، از assert برای اعتبارسنجی ورودی استفاده کند، در محیط تولید ممکن است همان اعتبارسنجی ناپدید شود و کتابخانه با ورودی نامعتبر، رفتار غیرقابل‌پیش‌بینی داشته باشد. این موضوع در بررسی کد کتابخانه‌های ثالث، یکی از نقاطی است که همیشه با دقت نگاه می‌کنم.

سناریوهای واقعی که این خطا را می‌سازند

در طول سال‌ها کار با پایتون، AssertionError را در این شش الگو دیده‌ام. شناختن هر الگو، تشخیص را چند برابر سریع‌تر می‌کند.

سناریوی اول: اعتبارسنجی ورودی کاربر با assert

رایج‌ترین الگوی اشتباه، استفاده از assert برای اعتبارسنجی ورودی کاربر است:

def create_user(username):
    assert len(username) >= 3, "username too short"
    # ...

در محیط توسعه، این تابع خطا می‌دهد اگر نام کاربری کوتاه باشد. در محیط تولید با -O، همین تابع بدون هیچ خطایی نام کاربری نامعتبر را می‌پذیرد و به دیتابیس می‌فرستد. راه‌حل درست:

def create_user(username):
    if len(username) < 3:
        raise ValueError("username must be at least 3 characters")
    # ...

سناریوی دوم: assert برای بررسی خروجی تابع کتابخانه

گاهی توسعه‌دهندگان فرض می‌کنند که یک کتابخانهٔ ثالث همیشه خروجی مورد انتظار را برمی‌گرداند و از assert برای بررسی استفاده می‌کنند:

result = external_api_call()
assert "data" in result, "api missing data field"

در محیط تولید، این assert ناپدید می‌شود و کد بعدی با KeyError مواجه می‌شود که پیام گمراه‌کننده‌ای می‌دهد. راه‌حل درست:

result = external_api_call()
if "data" not in result:
    raise RuntimeError(f"unexpected api response: {result}")

این الگو مخصوصاً در پروژه‌های وب اسکرپینگ با پایتون شایع است، چون کتابخانه‌های scraping گاهی ساختار HTML را تغییر می‌دهند.

سناریوی سوم: assert در کد عددی و scientific

در پروژه‌های scientific computing، توسعه‌دهندگان مکرراً از assert برای بررسی فرض‌های ریاضی استفاده می‌کنند:

assert X.shape[0] == y.shape[0], "samples and labels count mismatch"

مشکل اینجاست که در محیط تولید با -O، این بررسی‌ها ناپدید می‌شوند و در صورت ناسازگاری، رفتار کتابخانه‌های عددی مثل numpy ممکن است ناخواسته و غیرقابل‌پیش‌بینی باشد. راه‌حل درست:

if X.shape[0] != y.shape[0]:
    raise ValueError(
        f"samples and labels count mismatch: {X.shape[0]} vs {y.shape[0]}"
    )

سناریوی چهارم: assert برای بررسی state machine

در کدهایی که state machine دارند، assert گاهی برای بررسی stateهای ممکن استفاده می‌شود:

def transition(state, action):
    assert state in ("idle", "running", "stopped"), f"unknown state: {state}"
    # ...

در محیط تولید، این assert ناپدید می‌شود و state نامعتبر باعث رفتارهای عجیب در لایه‌های بعدی می‌شود. الگوی درست، استفاده از enum و بررسی صریح است:

from enum import Enum

class State(Enum):
    IDLE = "idle"
    RUNNING = "running"
    STOPPED = "stopped"

def transition(state, action):
    if not isinstance(state, State):
        raise TypeError(f"state must be State, got {type(state).__name__}")
    # ...

سناریوی پنجم: assert در unittest و nose

قبل از رواج pytest، فریم‌ورک unittest از assert و self.assertEqual استفاده می‌کرد. در کد تولیدی، این assertها ناپدید می‌شوند ولی در تست، رفتار متفاوتی دارند. برای اطلاعات بیشتر دربارهٔ رفتار مدرن تست، راهنمای multiprocessing در پایتون و ساختار تست‌های موازی را ببینید.

سناریوی ششم: assert در بازنویسی کد (refactoring)

گاهی توسعه‌دهندگان در مرحلهٔ بازنویسی، assertهای موقتی برای بررسی سازگاری رفتار قدیم و جدید می‌گذارند و فراموش می‌کنند آن‌ها را حذف کنند. نتیجه: کدی که در محیط توسعه کار می‌کند و در محیط تولید، بدون اطلاع، این بررسی‌ها را رد می‌کند. راه‌حل: در بازنویسی، هرگز از assert برای «مقایسهٔ رفتار» استفاده نکنید؛ از یک ماژول دیباگ جداگانه بهره ببرید.

سناریوهای مشابه در خانوادهٔ خطاهای منطقی هم دیده می‌شود. برای نمونه، خطای NameError در پایتون نشان می‌دهد که چطور یک فرض ساده دربارهٔ تعریف متغیر، می‌تواند به باگ‌های پنهان منجر شود.

روش تشخیص در پنج گام

در برخورد با AssertionError، پروتکل زیر را در پروژه‌های خودم اجرا می‌کنم. این پروتکل، هم برای حالت‌های آشکار و هم برای باگ‌های پنهان محیط تولید مفید است.

گام اول: خواندن دقیق پیام

پایتون پیام assert را با پیام استثنا ترکیب می‌کند:

AssertionError: divisor must not be zero

اگر پیام خالی باشد، یعنی assert بدون پیام نوشته شده است. این یکی از بدترین حالت‌هاست، چون هیچ زمینه‌ای برای دیباگ ندارد. قاعده‌ی من: هر assert باید پیام داشته باشد؛ حتی اگر پیام کوتاه باشد.

گام دوم: بررسی PYTHONOPTIMIZE

اولین سؤالی که می‌پرسم این است: «آیا محیط تولید با -O اجرا می‌شود؟». این سؤال را با بررسی مستقیم محیط اجرا پاسخ می‌دهم:

import sys
print("optimize:", sys.flags.optimize)
print("PYTHONOPTIMIZE:", __import__("os").environ.get("PYTHONOPTIMIZE"))

اگر sys.flags.optimize مقدار ۱ یا ۲ داشته باشد، تمام assertها ناپدید شده‌اند و برنامه در وضعیت «بی‌محافظ» اجرا می‌شود. این گام، نیمی از پرونده‌های «assert در محیط توسعه کار می‌کند ولی در محیط تولید نه» را حل می‌کند.

گام سوم: بررسی traceback کامل

در traceback، نقطهٔ پرتاب استثنا در آخرین فریم نمایش داده می‌شود. اگر assert در یک تابع کتابخانه‌ای باشد، زنجیرهٔ traceback طولانی‌تر خواهد بود. همیشه با chain=True لاگ کنید تا کل مسیر مشخص شود:

import traceback

try:
    risky_operation()
except AssertionError:
    traceback.print_exc(limit=None, chain=True)

گام چهارم: بازتولید در حالت بهینه

اگر مطمئن نیستید محیط تولید با -O اجرا می‌شود، خطا را در محیط توسعه با -O بازتولید کنید:

python -O script.py

اگر خطا در این حالت ناپدید شد ولی در حالت عادی ظاهر شد، تأیید می‌شود که assert در محیط تولید ناپدید شده است. این تشخیص، مخصوصاً در دیباگ سرویس‌های Docker و serverless مفید است.

گام پنجم: جستجوی assertهای پنهان در کتابخانه‌ها

گاهی خطا از کتابخانهٔ ثالث می‌آید. برای بررسی:

import numpy
import inspect
import re

source = inspect.getsource(numpy)
matches = re.findall(r"\bassert\b.*", source, re.MULTILINE)
print(f"found {len(matches)} assert statements in numpy")

این روش، نگاهی سریع به رفتار کتابخانهٔ مورد اعتماد شما می‌دهد. در پروژه‌های حساس، گاهی لازم است نسخه‌ای از کتابخانه با assertهای جایگزین‌شده توسط raise استفاده شود. برای مطالعه دربارهٔ ابزارهای مشابه در دیباگ، ابزارهای دیباگ پایتون را بررسی کنید.

اگر خطا با خطاهای لایه‌های پایین‌تر سیستمعامل قاطی شود، خطای OSError در پایتون تفکیک این دو را روشن می‌کند.

الگوهای درست اعتبارسنجی در کد تولیدی

راه‌حل تمام مسائل مربوط به AssertionError در یک جمله خلاصه می‌شود: «در کد تولیدی، از assert استفاده نکنید». در ادامه، الگوهای عملی برای جایگزینی assert با بررسی‌های امن را مرور می‌کنیم.

الگوی اول: raise با نوع استثنای دقیق

def process(data):
    if not isinstance(data, dict):
        raise TypeError(f"data must be dict, got {type(data).__name__}")
    if "id" not in data:
        raise ValueError("data must contain 'id' key")
    # ...

این الگو، سه مزیت دارد: در محیط تولید ناپدید نمی‌شود، نوع استثنا گویای مشکل است، و پیام شامل اطلاعات دقیق است.

الگوی دوم: تابع کمکی برای اعتبارسنجی

def _require(condition, message, exception_class=ValueError):
    if not condition:
        raise exception_class(message)

# استفاده
def create_user(username):
    _require(len(username) >= 3, "username too short")
    _require(username.isalnum(), "username must be alphanumeric")
    # ...

این الگو، خوانایی کد را بالا می‌برد و از تکرار الگوهای مشابه جلوگیری می‌کند. یک نکتهٔ ظریف: هرگز از نام assert_that یا مشابه آن استفاده نکنید، چون ممکن است خواننده به‌اشتباه فکر کند با assert استاندارد پایتون طرف است.

الگوی سوم: enum برای مقادیر محدود

from enum import Enum

class Environment(Enum):
    DEV = "dev"
    STAGING = "staging"
    PROD = "prod"

def deploy(env):
    if not isinstance(env, Environment):
        raise TypeError(f"env must be Environment, got {type(env).__name__}")
    # ...

این الگو در سطح static type checking هم مزیت دارد: ابزارهایی مثل mypy می‌توانند خطاهای نوع را قبل از اجرا شناسایی کنند.

الگوی چهارم: استفاده از Pydantic برای اعتبارسنجی

from pydantic import BaseModel, Field, ValidationError

class UserInput(BaseModel):
    username: str = Field(min_length=3, pattern=r"^[a-zA-Z0-9_]+$")
    age: int = Field(ge=0, le=150)

def create_user(raw):
    try:
        data = UserInput(**raw)
    except ValidationError as e:
        raise ValueError(f"invalid user input: {e}") from e
    # ...

این الگو، اعتبارسنجی را به لایهٔ schema منتقل می‌کند و منطق تجاری را از بررسی‌های تکراری آزاد می‌کند. اگر با FastAPI کار می‌کنید، این الگو عملاً استاندارد است.

الگوی پنجم: contract در سطح ماژول

from typing import Protocol

class DataSource(Protocol):
    def fetch(self) -> dict: ...

def process(source: DataSource) -> dict:
    data = source.fetch()
    if not isinstance(data, dict):
        raise RuntimeError(
            f"source.fetch() must return dict, got {type(data).__name__}"
        )
    return data

این الگو، قرارداد ماژول را صریح می‌کند و ابزارهای type checker می‌توانند آن را بررسی کنند. در پروژه‌های بزرگ، این رویکرد از انبوهی از assertهای پراکنده بهتر است.

الگوی ششم: fail-fast در راه‌اندازی، نه در هر درخواست

یکی از راه‌های تمیز برای جایگزینی assert، انجام بررسی‌ها در زمان راه‌اندازی برنامه است، نه در هر درخواست:

# در فایل config.py
import os

required_env_vars = ["DATABASE_URL", "SECRET_KEY", "API_KEY"]
missing = [v for v in required_env_vars if not os.environ.get(v)]
if missing:
    raise RuntimeError(
        f"missing required environment variables: {missing}"
    )

این الگو، برنامه را در لحظهٔ راه‌اندازی متوقف می‌کند اگر پیش‌نیازها موجود نباشند. مزیت آن نسبت به assert: در همهٔ حالت‌ها فعال است و پیام خطا کامل است.

اگر در پروژه‌ای با تست و اعتبارسنجی سر و کار دارید، الگوهای مشابه در مقالات خطای TypeError در پایتون و خطای KeyError در پایتون نیز بررسی شده است.

AssertionError در pytest و ابزارهای تست

در pytest، برخلاف نگرانی‌های بالا، assert ابزار اصلی تست است. pytest با استفاده از بازنویسی بایت‌کد (bytecode rewriting)، پیام‌های بسیار دقیقی از assertها تولید می‌کند و رفتار آن با assert در کد معمولی متفاوت است.

رفتار بازنویسی assert در pytest

def test_addition():
    result = 2 + 2
    assert result == 5

خروجی pytest:

assert 4 == 5
 +  where 4 = 2 + 2

این پیام فوق‌العاده مفید است، چون مقادیر واقعی متغیرها را نشان می‌دهد. pytest این کار را با بازنویسی assertها در زمان import فایل تست انجام می‌دهد. در کدهای معمولی (خارج از تست)، این بازنویسی رخ نمی‌دهد و اگر assertها هم با -O حذف شوند، ابزار تست دیگر نمی‌تواند اطلاعاتی نمایش دهد.

توصیه در تست‌ها

برای تست‌های pytest:

  • هرگز pytest را با -O اجرا نکنید. این کار تمام assertها را ناپدید می‌کند و تست‌ها به‌ظاهر موفق می‌شوند ولی در واقع هیچ بررسی‌ای انجام نشده است.
  • در CI/CD، مطمئن شوید PYTHONOPTIMIZE ست نشده است.
  • در Dockerfile تست، دستور python -m pytest را بدون -O اجرا کنید.

استفادهٔ درست از pytest.raises

import pytest

def test_divisor_zero():
    with pytest.raises(ValueError, match="divisor must not be zero"):
        divide(1, 0)

این الگو، دقیقاً نوع استثنا و پیام آن را بررسی می‌کند. برخلاف assert ساده، این الگو در همهٔ حالت‌ها فعال است و به رفتار -O حساس نیست (چون pytest.raises یک context manager است، نه assert).

اگر در پروژهٔ خود از ابزارهای تست موازی استفاده می‌کنید، مقالهٔ راهنمای multiprocessing در پایتون نکات مکمل را پوشش می‌دهد.

AssertionError در Django، numpy و کتابخانه‌ها

هر چارچوب و کتابخانه، رفتار خاص خود را با assert و AssertionError دارد. شناخت این رفتارها، دیباگ در محیط‌های واقعی را سریع‌تر می‌کند.

Django و assert در مدل‌ها

در Django، assert در مدل‌ها و فرم‌ها استفادهٔ محدودی دارد. برخی متدهای داخلی Django از assert برای بررسی سازگاری استفاده می‌کنند و اگر با تنظیمات نامناسب اجرا شوند، AssertionError می‌دهند:

# نمونه‌ای از خطای شایع در Django
AssertionError: AUTH_USER_MODEL refers to model 'myapp.User' that has not been installed

این خطا معمولاً در زمان راه‌اندازی رخ می‌دهد و نشانهٔ تنظیمات نادرست است. برای مطالعهٔ تفصیلی دربارهٔ ساختار Django، آموزش Django برای مبتدیان را ببینید.

numpy و بررسی شکل آرایه‌ها

کتابخانهٔ numpy در برخی از توابع، از assert داخلی استفاده می‌کند برای بررسی سازگاری شکل آرایه‌ها. اگر آرایه‌ها نامتناسب باشند و کتابخانه با -O اجرا شود، به‌جای AssertionError واضح، ممکن است خطای مبهم در محاسبات ظاهر شود:

import numpy as np

a = np.array([[1, 2], [3, 4]])
b = np.array([1, 2, 3])

# در حالت عادی: ValueError با پیام واضح
# در حالت -O: ممکن است خطای محاسباتی غیرقابل‌فهم بدهد
try:
    result = np.dot(a, b)
except Exception as e:
    print(type(e).__name__, e)

راه‌حل: همیشه پیش از عملیات عددی، شکل آرایه‌ها را با .shape و بررسی صریح چک کنید. این رویکرد، از وابستگی به assertهای داخلی کتابخانه جلوگیری می‌کند.

pandas و بررسی schema

کتابخانهٔ pandas در برخی از متدها مثل merge و concat، assertهای داخلی دارد که اگر ستون‌ها یا ایندکس‌ها نامتناسب باشند، فعال می‌شوند. در محیط تولید با -O، این بررسی‌ها ناپدید می‌شوند. توصیه: پیش از merge، ستون‌های کلیدی را صریحاً بررسی کنید:

if not set(left.columns) & set(right.columns):
    raise ValueError("no common column for merge")

TensorFlow و PyTorch

در کتابخانه‌های یادگیری عمیق، assertهای داخلی زیادی برای بررسی shape تنسورها وجود دارد. اگر با -O اجرا شوند، این بررسی‌ها ناپدید می‌شوند و اشتباهات shape ممکن است در لایه‌های بعدی به خطاهای مبهم منجر شوند. توصیه: همیشه در محیط تولید، از اجرای بدون -O برای این کتابخانه‌ها استفاده کنید یا بررسی‌های shape را در کد خود اضافه کنید.

myPy و type checking

ابزار mypy و سایر type checkerها، روی نوع‌های static کار می‌کنند، نه روی assertهای زمان اجرا. اگر می‌خواهید اعتبارسنجی‌های خود را در فاز تحلیل (و نه اجرا) بررسی کنید، از type hintها بهره ببرید:

from typing import Literal

def set_env(env: Literal["dev", "staging", "prod"]) -> None:
    ...

این رویکرد، جایگزینی عالی برای assertهای runtime در سطح API است.

پرسش‌های پرتکرار درباره AssertionError

این بخش، پرسش‌هایی را پوشش می‌دهد که در جلسات مشاوره و انجمن‌های فنی بیشترین تکرار را داشته‌اند. پاسخ‌ها به‌شکلی نوشته شده‌اند که برای جستجوهای مستقیم و دستیارهای هوش مصنوعی به‌عنوان پاسخ معتبر قابل استخراج باشند.

AssertionError با ValueError چه تفاوتی دارد؟

AssertionError وقتی پرتاب می‌شود که یک assert شکست بخورد و تنها در حالت‌های خاصی فعال است (نه در حالت -O). ValueError وقتی پرتاب می‌شود که یک مقدار از نظر محتوا نامعتبر باشد و در همهٔ حالت‌ها فعال است. برای مطالعهٔ تفصیلی ValueError، خطای ValueError در پایتون را ببینید.

چرا assert در محیط تولید کار نمی‌کند؟

چون پایتون با فلگ -O یا متغیر محیطی PYTHONOPTIMIZE، تمام دستورات assert را حذف می‌کند. این رفتار عمدی است: assert برای دیباگ طراحی شده، نه برای اعتبارسنجی تولید. راه‌حل: در کد تولیدی، از if ... raise استفاده کنید.

آیا assert در unittest هم ناپدید می‌شود؟

بله. اگر تست‌ها را با python -O -m unittest اجرا کنید، assertهای داخل تست‌ها ناپدید می‌شوند و تست‌ها به‌ظاهر موفق می‌شوند ولی در واقع هیچ بررسی‌ای انجام نشده است. این یک دام رایج در pipelineهای CI است. راه‌حل: هرگز تست‌ها را با -O اجرا نکنید.

چرا pytest با assert کار می‌کند و مشکل ندارد؟

چون pytest با بازنویسی بایت‌کد، assertها را به‌شکل خاصی در فایل‌های تست پردازش می‌کند. این رفتار فقط برای فایل‌های تست فعال است، نه برای کد معمولی. همچنین، pytest توصیه می‌کند که هرگز با -O اجرا نشود، چون در این حالت بازنویسی هم بی‌اثر می‌شود.

چطور بفهمم کد من با -O اجرا می‌شود یا نه؟

با کد زیر می‌توانید بررسی کنید:

import sys
print("optimize level:", sys.flags.optimize)
# 0: عادی، 1: -O، 2: -OO

همچنین os.environ.get("PYTHONOPTIMIZE") را بررسی کنید. اگر مقدار 1 یا 2 یا "" باشد، assertها ناپدید شده‌اند.

آیا assert پیام دلخواه را پشتیبانی می‌کند؟

بله، ولی فقط یک پیام رشته‌ای:

assert condition, "message here"

اگر نیاز به ساختار پیچیده‌تر دارید (مثل dict یا لیست)، باید پیام را به رشته تبدیل کنید. این محدودیت، یکی از دلایل ضعف assert در مقایسه با raise است که هر نوع آبجکتی را می‌پذیرد.

آیا می‌توان assert را با محافظ فعال نگه داشت؟

فنی بله، ولی توصیه نمی‌شود. راه‌حل‌هایی مثل if __debug__: assert ... وجود دارند ولی در واقع این هم در حالت -O ناپدید می‌شود، چون __debug__ هم توسط پایتون ست می‌شود. راه‌حل درست، جایگزینی با if ... raise است.

assert در زبان‌های دیگر چطور است؟

در C و C++، assert با ماکرو پیاده‌سازی می‌شود و با NDEBUG غیرفعال می‌شود. در Java، assert با -ea فعال و با -da غیرفعال می‌شود. مفهوم مشترک در همهٔ این زبان‌ها این است: assert برای دیباگ است، نه اجرا. این درسی است که همهٔ توسعه‌دهندگان باید در ذهن داشته باشند.

آیا در FastAPI، assert مشکل‌ساز است؟

در FastAPI، به‌دلیل استفادهٔ گسترده از Pydantic، اعتبارسنجی معمولاً خودکار است. اگر در endpointها از assert استفاده کنید، در محیط تولید با -O این بررسی‌ها ناپدید می‌شوند و ممکن است دادهٔ نامعتبر وارد لایه‌های بعدی شود. راه‌حل: از HTTPException با کد وضعیت مناسب استفاده کنید:

from fastapi import HTTPException

if not condition:
    raise HTTPException(status_code=400, detail="invalid input")

چطور می‌توان کد را بازرسی کرد و assertهای خطرناک را پیدا کرد؟

با ابزار flake8-assertive یا جستجوی دستی:

$ grep -rn "^\s*assert " --include="*.py" ./src
$ python -m pyflakes ./src

در CI، می‌توانید یک rule اضافه کنید که مانع اضافه‌شدن assert به کد تولیدی شود. برای نمونه، با ruff:

# در pyproject.toml
[tool.ruff.lint]
select = ["S101"]  # ممانعت از assert

این ابزار مخصوصاً در پروژه‌های تیمی مفید است و از اضافه‌شدن assertهای خطرناک جلوگیری می‌کند.

آیا assert در unit test مجاز است؟

در frameworkهایی مثل pytest، بله. ولی در unittest، پیشنهاد می‌کنم از متدهای تخصصی مثل self.assertEqual، self.assertTrue و مشابه استفاده کنید، چون در آن‌ها هم اطلاعات تشخیصی بهتری وجود دارد و هم رفتار predictable تری دارند.

چرا کتابخانه‌های محبوب از assert استفاده می‌کنند؟

دو دلیل اصلی: اول، بعضی از assertها برای بررسی‌های درونی طراحی شده‌اند که در حالت عادی هرگز نباید شکست بخورند (defensive programming). دوم، در کد علمی و عددی، assertها برای جلوگیری از محاسبات پرهزینه روی ورودی نامعتبر استفاده می‌شوند. در کتابخانه‌های بالغ، معمولاً این assertها با بررسی‌های صریح جایگزین شده‌اند، ولی همچنان در برخی نسخه‌های قدیمی دیده می‌شوند.

برای مطالعات مکمل در همین خانواده، خطای AttributeError در پایتون، خطای ValueError در پایتون و خطای KeyError در پایتون نکات کلیدی مشابهی دارند که در مدیریت کد تولیدی کمک‌کننده است.

آنچه ادعاهای شکسته به معماری کد من آموختند

AssertionError بیش از آنکه یک خطای فنی باشد، یک «درس طراحی» است. سه اصلی که پس از سال‌ها کار با آن، در معماری کد خودم رعایت می‌کنم:

نخست، مرز بین دیباگ و تولید را روشن کنید. assert ابزاری است که برای «برنامه‌نویس در فاز توسعه» طراحی شده و در فاز اجرا، به‌درستی ناپدید می‌شود. اگر می‌خواهید برنامه در تولید مقاوم باشد، از if ... raise استفاده کنید. این تفکیک، نیمی از باگ‌های پنهان مربوط به assert را از ابتدا حذف می‌کند.

دوم، نوع استثنا را معنادار انتخاب کنید. AssertionError برای همه‌چیز مناسب نیست. برای ورودی نامعتبر ValueError، برای نوع اشتباه TypeError، برای وضعیت اجرای نامناسب RuntimeError. این تفکیک، دیباگ را چند برابر سریع‌تر می‌کند و در سطح ابزارهای پایش (monitoring)، طبقه‌بندی دقیق‌تری به شما می‌دهد.

سوم، اعتبارسنجی را در لایه‌های درست انجام دهید. در APIهای مدرن، اعتبارسنجی ورودی می‌تواند در سه لایه انجام شود: در نوع‌دهی static (type hints)، در schema (Pydantic)، و در منطق تجاری. این سه لایه، جای assertهای پراکنده را می‌گیرند و برنامه را در تمام محیط‌ها یکسان نگه می‌دارند.

در پایان، اگر در پروژه‌ای با حالت خاصی از AssertionError برخورد کردید که اینجا پوشش داده نشده — مثلاً در ترکیب با TensorFlow، aiohttp، یا کتابخانه‌های C-extension که پیام‌های اختصاصی دارند — تجربه‌تان را در دیدگاه‌ها بنویسید. به‌ویژه اگر راه‌حلی پیدا کرده‌اید که با رویکردهای معمول متفاوت است و می‌تواند برای خوانندهٔ بعدی ارزشمند باشد. 🧩