چند وقت پیش روی اسکریپت پردازش داده‌های فروش کار می‌کردم. کدی که هفته‌ها بدون مشکل اجرا شده بود، ناگهان با یک پیام متوقف شد: TypeError: unsupported operand type(s) for +: 'int' and 'str'. جالب اینکه خط کد مستقیم و ساده بود — یک جمع ساده بین دو مقدار. مسئله جای دیگری بود: یکی از منابع داده، به‌جای عدد، رشته برگردانده بود. آن روز دوباره یادآوری شد که TypeError (خطای نوع)، همیشه از آن جایی که انتظار دارید نمی‌آید. این مقاله، همان مسیری است که در این سال‌ها برای تشخیص و رفع TypeError در پایتون (Python) طی می‌کنم.

TypeError با بقیه خطاهای پایتون چه تفاوتی دارد؟

در پایتون، هر خطا یک دامنهٔ مشخص دارد و این تفکیک، مهم‌ترین کمکی است که برای عیب‌یابی دارید. سه خطای پرتکرار که زیاد با هم اشتباه گرفته می‌شوند، این‌ها هستند:

خطامعنای دقیقمثال
TypeErrorعملیات روی یک نوع دادهٔ نامناسب انجام شده5 + "abc"
ValueErrorنوع داده درست است، اما مقدار معتبر نیستint("abc")
AttributeErrorشیء مورد نظر، آن ویژگی یا متد را نداردNone.upper()

تفاوت TypeError و ValueError، دقیقاً همان چیزی است که مبتدی‌ها همیشه با هم قاطی می‌کنند. در 5 + "abc"، عملگر جمع نمی‌داند با رشته چه کند — این TypeError است. اما در int("abc")، تابع int دقیقاً می‌داند با رشته چه کند، فقط رشتهٔ «abc» قابل تبدیل به عدد نیست — این ValueError است. این تفکیک، هم در پیام خطا اثر می‌گذارد، هم در مسیر عیب‌یابی. اگر روی این تفاوت‌ها عمیق‌تر کار می‌کنید، خطای ValueError در پایتون و راه حل آن و خطای AttributeError در پایتون و راه حل را جداگانه نوشته‌ام.

نکتهٔ کلیدی: TypeError همیشه به این معنا نیست که «کدی که نوشته‌اید غلط است». بعضی وقت‌ها کد کاملاً درست است، اما داده‌ای که از بیرون می‌رسد، انتظار شما را برآورده نمی‌کند. این تمایز، مسیر اصلاح را هم متفاوت می‌کند.

TypeError، خطای نوع داده است؛ نه خطای منطق، نه خطای مقدار. تفکیک همین یک جمله، نصف مسیر رفع خطا را کوتاه می‌کند.

چهار سناریوی تیپیک TypeError با کد واقعی

در تجربه‌ام، بیش از نود درصد TypeErrorها در پایتون، در چهار سناریوی مشخص می‌گنجند. هر سناریو، پیام خطا و راه‌حل متفاوتی دارد:

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

کلاسیک‌ترین حالت. جمع بین عدد و رشته، یا ضرب بین عدد و None:

price = "1200"
tax = 0.09
total = price + (price * tax)  # TypeError: can't multiply sequence by non-int of type 'float'

اینجا price رشته است، ولی کد مثل عدد با آن رفتار می‌کند. منبع مشکل معمولاً جایی است که price مقدارش را گرفته — احتمالاً از فرم کاربر، فایل CSV، یا پاسخ یک API (Application Programming Interface).

سناریوی دوم: فراخوانی متد روی None

وقتی تابعی مقدار None برمی‌گرداند، اما کد فرض می‌کند یک شیء معتبر برگشته:

user = get_user_by_id(user_id)
name = user.get("name")  # TypeError: 'NoneType' object has no attribute 'get'

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

سناریوی سوم: تعداد یا نوع نامناسب آرگومان تابع

تابع، تعداد مشخصی پارامتر می‌گیرد، اما کد به‌صورت دیگری فراخوانی می‌کند:

def compute_discount(amount, percentage):
    return amount * (1 - percentage)

compute_discount(1000)  # TypeError: compute_discount() missing 1 required positional argument: 'percentage'

سناریوی چهارم: تکرار روی شیء غیرقابل‌تکرار

تلاش برای for روی چیزی که iterable نیست (یا یک عدد، یا None):

items = None
for item in items:  # TypeError: 'NoneType' object is not iterable
    print(item)

این حالت معمولاً از توابعی می‌آید که در حالت موفق، یک لیست برمی‌گردانند، و در حالت خطا، None یا یک مقدار دیگر. مدیریت این الگو، تفاوت بین کدِ مبتدی و کدِ حرفه‌ای است.

ریشه‌یابی سریع: سه ابزاری که همیشه استفاده می‌کنم

وقتی TypeError می‌گیرید، اولین کاری که باید بکنید، نه حدس زدن است، نه عوض کردن کد. باید وضعیت داده را بفهمید. سه ابزار ساده که در هر عیب‌یابی استفاده می‌کنم:

ابزار اول: خواندن درست Traceback

پیام TypeError، سه اطلاع کلیدی می‌دهد: فایل، شماره خط، و نام نوع داده‌ای که دریافت شده. مثال TypeError: unsupported operand type(s) for +: 'int' and 'str' می‌گوید عملگر + یک عدد و یک رشته گرفته. اگر فقط پیام انتهای خطا را بخوانید، نصف اطلاعاتی که پایتون داده را از دست می‌دهید — همیشه از بالای traceback شروع کنید.

ابزار دوم: تابع type() و repr()

پیش از خط مشکل‌ساز، یک خط ساده اضافه کنید:

print(type(price), repr(price))

این یک خط، معمولاً تمام مسئله را روشن می‌کند. type می‌گوید نوع دقیق چیست و repr می‌گوید محتوای واقعی (با فاصله‌های اضافه و کاراکترهای نامرئی) چه است. در یکی از پروژه‌هایم، repr نشان داد رشته‌ای که فکر می‌کردم «1200» است، در واقع ترکیبی با یک نیم‌فاصلهٔ پنهان بود که تبدیل به عدد را از کار می‌انداخت.

ابزار سوم: IDE یا REPL برای کاوش تعاملی

IDE (Integrated Development Environment) یا REPL (Read-Eval-Print Loop) به شما اجازه می‌دهد در حین اجرا، وضعیت متغیرها را بررسی کنید. اگر با pdb یا دیباگر VS Code آشنا نیستید، پیشنهاد می‌کنم روی پروژه‌ای کوچک تمرین کنید. برای توضیح مفاهیم پایهٔ کار با این محیط‌ها، آموزش پایتون از صفر و بهترین منابع یادگیری پایتون را ببینید.

پیام TypeError، مقصد را نشان می‌دهد؛ منبع اغلب جایی دیگر است. برای رسیدن به منبع، باید وضعیت داده را ببینید، نه فقط خط خطا را.

روش اصلاح گام‌به‌گام برای هر سناریو

برای هر یک از چهار سناریوی بالا، روش اصلاح مشخص است. اصل مشترک همه، یک جمله است: نوع داده‌ای که واقعاً دارید را به نوع داده‌ای که کد انتظار دارد تبدیل کنید — یا کد را طوری بنویسید که هر دو نوع را بپذیرد.

اصلاح سناریوی اول: تبدیل صریح نوع

price = "1200"
tax = 0.09
price_float = float(price)  # تبدیل صریح قبل از عملیات
total = price_float + (price_float * tax)

اما تبدیل صریح، همیشه امن نیست. اگر ورودی کاربر «abc» باشد، float("abc") خودش ValueError می‌دهد. پس تبدیل باید در یک بلوک try/except قرار بگیرد یا با یک تابع اعتبارسنجی همراه باشد. روش درست‌تر:

def safe_float(value, default=0.0):
    try:
        return float(value)
    except (ValueError, TypeError):
        return default

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

اصلاح سناریوی دوم: بررسی None

پیش از استفاده از متغیری که ممکن است None باشد، صریح بررسی کنید:

user = get_user_by_id(user_id)
if user is None:
    return "کاربر یافت نشد"
name = user.get("name")

استفاده از is None به‌جای == None، هم توصیهٔ رسمی پایتون است، هم سریع‌تر.

اصلاح سناریوی سوم: بازبینی امضای تابع

اگر خطا می‌گوید آرگومان کم یا زیاد داده شده، دو راه دارید: فراخوانی را اصلاح کنید یا پارامتر را با مقدار پیش‌فرض تعریف کنید:

def compute_discount(amount, percentage=0.0):
    return amount * (1 - percentage)

در APIهای عمومی، مقدار پیش‌فرض یک راه استاندارد برای کاهش TypeError است؛ اما در تابع‌های داخلی، بازبینی فراخوانی معمولاً تمیزتر است.

اصلاح سناریوی چهارم: نرمال‌سازی iterable

items = items or []
for item in items:
    print(item)

عبارت items or [] اگر items مقدارش None یا هر مقدار falsy باشد، یک لیست خالی جایگزین می‌کند. این الگو در پردازش داده بسیار کاربردی است — به‌شرطی که لیست خالی، رفتار درستی برای منطق شما داشته باشد.

TypeError در پروژه‌های واقعی: سه کیس میدانی

سناریوهای آموزشی، همیشه به پروژه‌های واقعی شبیه نیستند. سه کیس واقعی که در پروژه‌ها دیده‌ام:

کیس اول: داده از JSON با نوع اشتباه

در پایتون، وقتی از یک API پاسخ JSON (JavaScript Object Notation) می‌گیرید، اعداد ممکن است در برخی APIها به‌صورت رشته بازگردند. اگر همان عدد را در محاسبات بعدی استفاده کنید، TypeError می‌گیرید. راه‌حل رایج در پروژه‌های من: تعریف یک لایهٔ «نرمال‌سازی» که پیش از هر محاسبه، نوع دادهٔ ورودی را تأیید می‌کند. کار با JSON را در کار با JSON در پروژه‌های واقعی با جزئیات باز کرده‌ام.

کیس دوم: متدها روی دیتافریم Pandas

وقتی روی یک ستون Pandas عملیات انجام می‌دهید، اگر ستون نوع mixed داشته باشد (مثلاً چند رشته بین اعداد)، عملیات عددی، TypeError می‌دهد. راه‌حل استاندارد: pd.to_numeric با پارامتر errors='coerce' که مقادیر غیرقابل تبدیل را به NaN تبدیل می‌کند. کاوش بیشتر در Pandas را در کتابخانه pandas در پایتون آورده‌ام.

کیس سوم: مقدار از دیتابیس با نوع اشتباه

اگر مقدار از دیتابیس MySQL (My Structured Query Language) می‌آید و ستون از نوع VARCHAR است، پایتون همان مقدار را به‌عنوان رشته برمی‌گرداند، حتی اگر محتوایش عدد باشد. این نکته، وقتی پروژه‌ای با دیتابیس قدیمی و بدون تمیزکاری نوع دارد، چند ساعت عیب‌یابی می‌طلبد. مسیر اتصال به MySQL را در اتصال پایتون به MySQL جداگانه توضیح داده‌ام.

اشتباهاتی که حل مشکل را سخت‌تر می‌کنند

چهار اشتباه رایج که در عیب‌یابی TypeError زیاد دیده‌ام:

  • گرفتن خطا با except Exception بدون بررسی نوع: وقتی همهٔ خطاها را می‌گیرید، TypeError هم پنهان می‌شود و به‌جای رفع، سرکوب می‌شود. کد در نگاه اول کار می‌کند، اما در جایی دیگر، با پیام گمراه‌کننده‌ای می‌شکند.
  • تبدیل نوع بدون بررسی امکان تبدیل: int("abc") خودش ValueError می‌دهد و تبدیل TypeError را به مشکل بدتری تبدیل می‌کند. تبدیل باید در بلوک try/except یا با تابع امن انجام شود.
  • فریب دادن خود با str(): وقتی خطای int + str می‌گیرید، یک واکنش سریع این است که هر دو طرف را با str() به رشته تبدیل کنید. اما اگر هدف، محاسبهٔ عددی است، این کار فقط نتیجه را خراب می‌کند («10» + «5» می‌شود «105»). تبدیل باید هم‌راستا با نیت باشد.
  • بی‌توجهی به traceback کامل: معمولاً پیام TypeError در خط آخر داده می‌شود، اما منبع در بالای آن. اگر فقط خط آخر را بخوانید، ممکن است متغیری که در خط بی‌ربطی مقدار گرفته را مقصر بدانید و ساعت‌ها در جای اشتباه وقت بگذارید.

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

پیشگیری: نوشتن کدی که کم‌تر TypeError بدهد

بهترین عیب‌یابی، خطایی است که رخ نمی‌دهد. سه ابزار پیشگیرانه که در پروژه‌های خودم استاندارد کرده‌ام:

ابزار اول: Type Hints (نوع‌نویسی صریح)

پایتون از نسخهٔ 3.5 به بعد، امکان نوع‌نویسی را در امضای توابع فراهم کرده. اگر تابع شما انتظار float دارد، همان را بنویسید:

def compute_discount(amount: float, percentage: float) -> float:
    return amount * (1 - percentage)

Type Hint به‌خودی‌خود در زمان اجرا خطا نمی‌دهد، اما دو مزیت بزرگ دارد: کد خواناتر می‌شود، و ابزارهای تحلیل استاتیک می‌توانند پیش از اجرا، خطاهای احتمالی را بگیرند. اگر با شیء‌گرایی در پایتون آشنا نیستید، شی گرایی در پایتون را ببینید.

ابزار دوم: تحلیل استاتیک با mypy

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

ابزار سوم: Validation در مرزها

در مرزهای پروژه — جایی که داده از بیرون وارد می‌شود — همیشه اعتبارسنجی کنید. این مرزها شامل ورودی کاربر، پاسخ API، خواندن فایل، و دیتابیس هستند. تکنیک dataclass یا کتابخانه‌های مشابه مثل pydantic، این اعتبارسنجی را ساده و صریح می‌کنند.

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

تفاوت پیام‌های TypeError در نسخه‌های مختلف پایتون

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

پایتون 3.8پایتون 3.10 و بعدتر
unsupported operand type(s) for +: 'int' and 'str'پیام خطا با نشان دادن خود مقدار خطادار، سرنخ مستقیم‌تری می‌دهد
'NoneType' object is not subscriptableپیام دقیق‌تر با اشاره به متغیری که None است

در پایتون 3.10 و نسخه‌های بعدی، پیام‌های خطا بسیار گویاتر شده‌اند. اگر روی نسخهٔ قدیمی‌تری کار می‌کنید، ارتقا به نسخهٔ جدید (حداقل 3.10) یکی از ساده‌ترین راه‌ها برای کاهش زمان عیب‌یابی است. توجه داشته باشید که اگر با کتابخانه‌های قدیمی کار می‌کنید، این ارتقا همیشه بی‌دردسر نیست و نیاز به تست دارد.

خط پایان: خطا به‌عنوان سرنخ، نه مانع

TypeError در پایتون، یکی از خوش‌رفتارترین خطاهاست. جای خطا را دقیقاً می‌گوید، نوع دادهٔ نامناسب را در پیام مشخص می‌کند، و در بیشتر موارد، راه‌حل را پیشنهاد می‌دهد. اگر این خطا را نه به‌عنوان مانع، بلکه به‌عنوان سرنخ ببینید، مسیر حل به‌طور چشمگیری کوتاه‌تر می‌شود. سه عادت ساده در پروژه‌های من، اکثر TypeErrorها را از پیش می‌گیرند: Type Hints در امضای توابع، اعتبارسنجی در مرزهای پروژه، و نگاه دقیق به traceback کامل. اگر این سه را در کد خودتان رعایت کنید، TypeError از یک بحران به یک یادآوری ساده تبدیل می‌شود.

اگر تجربه‌ای از یک TypeError دارید که ساعت‌ها وقت شما را گرفته — به‌خصوص مواردی که پیام خطا گمراه‌کننده بود — خوشحال می‌شوم در دیدگاه‌ها بخوانم. چه چیزی باعث آن شده بود و چطور حلش کردید؟ همان تجربه، برای خوانندهٔ بعدی که در وضعیت مشابه قرار دارد، از هر مقالهٔ مرجع مفیدتر است. 🐍