前言
新手面对报错时的第一反应,往往是「代码挂了,重写一遍」。但有经验的人会先做另一件事:把报错信息读完。Python 的报错信息里其实已经把「出了什么事、在哪一行、当时是什么类型」写得相当清楚,3.10 之后还进一步加了更精确的位置提示和「你是不是想写……」的猜测。
所以要先把本文的范围划清楚:这一篇只讲「怎么读报错」,不讲「怎么捕获」。try/except那套机制属于另一个话题,本文一次都不涉及。这里的目标是让你看到一个报错时,能在十秒内定位到「哪一行、哪类问题、下一步看什么」。
本文按报错的来源分四组讲:语法层(代码根本没被接受)、名字与属性与类型层(代码在跑,但用错了东西)、查找层(容器里没这个元素)、导入层(模块找不到)。每一组都给出真实的报错文本,逐行拆解。
一、报错信息的三段结构
不管哪类错误,Python 打印的格式都遵循同一套模板:
Traceback (most recent call last):
File "demo.py", line 7, in <module>
total = price + count
~~~~~~^~~~~~~
TypeError: unsupported operand type(s) for +: 'float' and 'str'从上往下读三样东西:
File "demo.py", line 7—— 出事的位置。有多个File块时,最后一个是直接出事的那一行,上面的是调用它的地方。- 中间被引出来的那行源码,以及它下面那串
^或~~~—— 精确指出这一行的哪一段出了问题。3.11 之后,这种「下划波浪线」标注变得很常见,它标出的就是参与运算的那个表达式。 - 最后一行—— 异常类型冒号后面是消息。类型决定「哪一类问题」,消息决定「具体是什么问题」。
记住这个口诀:看最后一行定类型,看倒数第二个 File 块定位置,看波浪线定范围。
二、语法层:SyntaxError 与 IndentationError
这两类错误有个共同特点:代码压根没开始执行。解释器在解析阶段就拒绝了,所以不会有「运行时」的信息,你看到的永远是「文件第几行写得不对」。
File "demo.py", line 1
print "hello"
^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?这是把 Python 2 的老代码拿到 Python 3 里跑的最典型症状。Python 2 里print是语句,可以不写括号;Python 3 里print是函数,括号是必须的。报错消息里那句猜测已经直接给了答案。顺带说,Python 2.7 已于 2020 年 1 月 1 日停止维护,这类改写是接手老项目时的必做项。
File "demo.py", line 3
return x
^^^^^^^^
SyntaxError: 'return' outside functionreturn只能出现在函数体内。看到这个报错,检查缩进层级——很可能上面那个def已经结束了,这一行掉到了模块顶层。
File "demo.py", line 4
total = a +
^
SyntaxError: invalid syntaxinvalid syntax是最笼统的一类,但配上波浪线就很好办:它指向的位置,通常就是「读到这儿读不下去了」。像这个例子,加号后面缺了操作数,所以光标落在行尾。遇到invalid syntax,往指示位置的前面看,而不是往后看——错误往往出在它前一个记号上。
File "demo.py", line 5
print("done")
^
IndentationError: unexpected indentFile "demo.py", line 5
x = 1
IndentationError: expected an indented blockunexpected indent是「不该缩进却缩进了」,常见于从别处复制粘代码、多打了一个空格。expected an indented block是「该缩进却没缩进」,通常出现在if、for、def这类冒号之后。
还有一种是前两者的亲戚:
TabError: inconsistent use of tabs and spaces in indentation同一个文件里混用了制表符和空格。肉眼看缩进是对齐的,但解释器数的是字符。统一用空格(PEP 8 建议 4 个空格)就能根治。
三、名字、属性与类型:NameError、AttributeError、TypeError
这一层是运行时错误,代码真的跑起来了,跑到某一行时发现「这个东西不对劲」。
NameError: name 'totl' is not defined. Did you mean: 'total'?意思是「这个名字我从来没听说过」。常见成因有四种,按出现频率排:
- 拼写错误(如上面这个)。
- 变量在赋值之前就被使用。
- 忘了
import,直接用json.dumps却从没import json。 - 作用域问题:在函数内部读取了一个只在别处定义的名字。
那半句Did you mean:是 Python 3.10 起加入的,解释器会在当前作用域里找相似名字,非常省事。
它的一个变体要单独认:
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value名字确实存在,但它在函数里被赋过值,于是成了局部变量;而在被读取的那一刻,这个局部变量还没有值。典型场景是函数里既读又写同一个名字,却在读的时候还没走到写的那一步。这句话的措辞在不同版本里不一样:较新的版本会直说「不能访问尚未绑定值的局部变量」,更早的版本写的是「局部变量在赋值之前被引用」。两种说法指的是同一件事,看到旧措辞不要以为换了机制。
AttributeError: 'str' object has no attribute 'append'. Did you mean: 'extend'?意思是「str这个类型的对象上没有append这个方法」。读法是:先看类型,再看方法名。'str'说明你手上是一个字符串,而字符串是不可变的,没有追加元素的方法——append是列表的方法。这里的根因几乎总是「变量在半路被赋值成了别的类型」。
TypeError: unsupported operand type(s) for +: 'int' and 'str'TypeError: can only concatenate str (not "int") to strTypeError: 'int' object is not callableunsupported operand type(s) for +是「这两个类型不能做这个运算」;can only concatenate str (not "int")是它针对字符串加法的专门措辞;'int' object is not callable则是「你对一个整数用了调用语法」,最常见的原因是变量名遮蔽了函数名——比如先定义了def list(): ...,后来又list = [1, 2],于是list()就调不动了。
TypeError的读法固定为三步:看运算符或方法名、看两侧(或对象)的类型、回到那一行确认变量到底是什么。
四、查找与导入:KeyError、IndexError、ModuleNotFoundError
KeyError: 'name'字典里没有name这个键。报错消息里被引号包住的那段就是这个键本身,非常直白。如果键是字符串且里面有空格或特殊字符,有时会看到双重引号,把内容按原样读出来即可。排查时通常要回头确认「写进去的时候用的是哪个键」——是"name"还是"Name",是字符串还是数字。
IndexError: list index out of range下标超出了序列范围。注意它不带具体数值,所以要自己回到那一行看用的是哪个下标。常见成因:用len(lst)当下标(最大值应该是len(lst) - 1)、迭代过程中删元素导致长度变了、把空列表当成有内容的列表来取。
ModuleNotFoundError: No module named 'requests'导入的模块找不到。按顺序排查:
- 模块名拼写是否正确(模块名大小写敏感)。
- 标准库还是第三方库——
requests不是标准库,没装就是没有。装法是pip install requests,具体包名以该库官方文档为准。 - 是不是自己写的文件,比如同目录下有
utils.py却import util。若是包,还要看它有没有__init__.py、是否在sys.path里。 - 虚拟环境是否激活,装在了另一个环境里是最常见的「明明装了却说没有」。
ModuleNotFoundError是ImportError的子类,所以在老的英文资料里常看到ImportError: No module named ...这种写法,那是更早版本的措辞。
下面这张表把最常见的一批报错压成速查表:
| 报错 | 一句话含义 | 先看什么 |
|---|
SyntaxError | 语法没通过,代码没执行 | 波浪线位置的前一个记号 |
IndentationError | 缩进层级不对 | 冒号后的缩进、是否混用 Tab |
NameError | 名字不存在 | 拼写、是否import过 |
UnboundLocalError | 局部变量还没赋值就被读 | 函数内的赋值语句位置 |
TypeError | 类型不支持这个操作 | 运算符两侧的类型 |
AttributeError | 对象没这个属性/方法 | 对象实际是什么类型 |
KeyError | 字典没这个键 | 键的拼写与类型 |
IndexError | 下标越界 | 下标表达式与序列长度 |
ModuleNotFoundError | 模块找不到 | 是否安装、环境是否正确 |
常见坑点
1. 只看第一行Traceback就下结论
❌ 一看到Traceback (most recent call last):就以为是第一段File出了问题。 ✅ 先看最后一行确定异常类型,再看最后一个File块确定直接出事的行。
2. 把invalid syntax的位置当成错误所在
❌ 紧盯着波浪线指向的那个字符反复检查。 ✅ 解析器是「读到读不动了」才报错,问题常常在它前面一个记号:少个括号、少个逗号、少个冒号。
3. 以为NameError一定是拼错
❌ 把名字逐字母核对三遍,仍找不到原因。 ✅ 检查三件事:有没有import、是不是在赋值之前使用、是不是在函数里访问了不该访问的作用域。
4. 把AttributeError的成因归到方法不存在
❌ 去查文档确认append到底叫什么。 ✅ 先看报错里的类型('str' object),确认这个变量实际是什么类型,多数时候是类型在半路变了。
5. 用len()当最后一个下标
❌lst[len(lst)],直接越界。 ✅ 最后一个下标是len(lst) - 1;for循环里用enumerate拿下标更稳。
6. 看到ModuleNotFoundError就反复重装
❌ 一遍遍pip install,问题照旧。 ✅ 先确认当前用的是哪个 Python 环境、装在哪个解释器下,虚拟环境没激活是高频原因。
7. 把 Python 2 的老代码直接跑在 Python 3 上
❌print "x"、except X, e、raise X, y、dict.iteritems()、xrange这类写法直接搬过来。 ✅ 分别为:print("x");except X as e;raise X(y);.items();range。Python 2.7 已于 2020 年 1 月 1 日停止维护,老写法只是为了读懂遗留代码。
8. 遇到报错就吞掉,靠猜改代码
❌ 加一句pass让程序继续跑,报错看不到了,问题也还在。 ✅ 先把报错信息读懂、定位到具体那一行,再决定怎么改;报错不是噪音,它是现场记录。
总结
| 层次 | 代表报错 | 读法要点 |
|---|
| 语法层 | SyntaxError、IndentationError、TabError | 代码未执行;看指示位置的前一个记号、看缩进 |
| 名字/属性/类型层 | NameError、UnboundLocalError、AttributeError、TypeError | 看类型、看方法名、看变量在半路被改成了什么 |
| 查找层 | KeyError、IndexError | 键直接写在消息里;下标要自己回查 |
| 导入层 | ModuleNotFoundError | 拼写、是否安装、环境是否正确 |
读报错是一项可以练出来的技能,而且性价比极高:必须先定位到那一行,才谈得上修。最后一行给你类型,倒数第二个File块给你行号,那串波浪线给你范围——把这三样读全再动手,多数问题不必靠猜。