写Python这些年谁还没被几个报错折磨过。不少刚入门的朋友一看到红字就紧张其实换个角度想报错是编辑器在帮你把关它用最直接的方式告诉你这里有问题改掉就好了。Python的错误类型虽然多但日常开发里翻来覆去就是那十几个踩坑规律也很明显。这篇我整理了实际开发中最高频的十类错误每一类都配了触发场景、原因分析和修复方法你照着排查基本都能解决。我尽量用平时和朋友交流的口吻来讲不整那些虚的读完你至少能把一半报错秒杀掉。1. 属性访问类错误AttributeError 和 TypeError 的纠缠1.1 最常见的 NoneType has no attribute这个报错我是真的见太多了几乎每个写爬虫或者调接口的朋友都遇到过。报错信息长这样AttributeError: NoneType object has no attribute text原因很直接你前面的调用返回了None然后你又对None调用了属性或方法。比如用某个库解析网页定位元素没找到返回的是None你直接.text就炸了。还有调接口拿JSON某个字段不存在解析出来是None你接下来一长串链式调用全部踩空。解决思路分三层考虑第一层确定这个值到底允不允许是None。如果业务上不允许你可以在获取之后加断言result api.get_data() assert result is not None, 接口返回为空请检查参数或网络第二层如果确实可能为空那就做空值保护用条件判断或者设置默认值title element.find(h1).text if element.find(h1) else 默认标题第三层养成链式调用前就断层的习惯别一个表达式写十层调用拆开写更能看清是哪一步返回了None。1.2 ValueError: not enough values to unpack (expected 2, got 1)解包错误也是入门高频通常长这样ValueError: not enough values to unpack (expected 2, got 1)核心原因左边接收变量数多于右边可解包元素数。比如你写了key, value item但item实际上只有一个元素。常见发生在用split()拆字符串时某一行少了分隔符导致只拆出一个片段。推荐的做法是把严格解包改成容错解包parts line.split(:, 1) key parts[0] if len(parts) 0 else value parts[1] if len(parts) 1 else 还有一个容易被忽视的点for key, value in dict.items()这种写法本身没问题但如果你在循环体里修改了字典结构或者迭代了一个已经被赋值为其他类型的变量也会出现这类报错。处理数据前先打印一行看看结构能省下大量排查时间。2. 键与索引访问KeyError 和 IndexError2.1 KeyError: xxx 键不存在KeyError发生在你用dict[key]方式访问不存在的键时比如从别人接口返回的JSON里取字段数据结构变了一点就炸了。很多人在写爬虫时深受其害——昨天还能跑的代码今天报KeyError大都是接口加字段了。解决方案就四个字别用中括号。改成.get()方法user_name data.get(user, {}).get(name, 未知用户)get()第一个参数是键名第二个是默认值键不存在时返回默认值而不是抛异常。更讲究一点就用collections.defaultdict给整个字典设置默认工厂from collections import defaultdict data defaultdict(lambda: None)在深嵌套的场景里一级一级get()写起来确实啰嗦。我一般会写个小工具函数像剥洋葱一样取嵌套数据def deep_get(mapping, path, defaultNone): for key in path.split(.): if isinstance(mapping, dict): mapping mapping.get(key) if mapping is None: return default else: return default return mapping平时取配置、取接口数据都用这个报错率直线下降。2.2 IndexError: list index out of rangeIndexError就是列表越界这个错误在数据处理和循环遍历场景里很常见。比如你从一个列表里取前三个元素但列表只有两个items [10, 20] first, second, third items[0], items[1], items[2] # 报错处理办法是在取索引前先判断长度if len(items) 3: third items[2] else: third None另外要注意循环里动态删除列表元素会导致索引错乱。正确姿势是把要删除的元素收集到新列表最后统一删to_remove [] for item in items: if item bad: to_remove.append(item) for item in to_remove: items.remove(item)更好的思路是直接列表推导式生成新列表压根不用原地删items [item for item in items if item ! bad]这个坑我在处理日志数据时踩过不止一次修改正在遍历的容器不仅可能IndexError还可能漏掉数据结果非常隐蔽。3. 类型转换与运算ValueError 和 TypeError3.1 int() 和 float() 转换失败把字符串转整数是Python里再普通不过的操作但用户输入千奇百怪比如int(abc)直接抛ValueError: invalid literal for int()。这类错误的本质是Python没法从你给的值里提取出数字。防御性写法是用try-except包裹或者先判断是不是数字def safe_int(value, default0): try: return int(value) except (ValueError, TypeError): return default注意除了ValueError还要捕获TypeError因为int(None)同样会报错。在表单验证、命令行参数解析这些场景你永远不知道用户会传什么进来防御函数是必需品。3.2 TypeError: 参数类型不对TypeError种类很多常见的有这么几种参数个数不对function takes 1 positional argument but 2 were given参数类型不对比如字符串和整数直接相加对象不可调用int object is not callable字符串和整数相加是最经典的age 25 message 年龄是 age # 报错改成message f年龄是{age}f-string不仅更安全代码也更干净。对于函数参数个数问题多半是定义和调用不一致检查函数签名就好了。还有一种TypeError比较阴你定义了一个变量叫list或者dict把内置函数覆盖了后面调用list(abc)就变成调用一个字符串或者None报错object is not callable。这个我在老代码里见过变量命名千万别用内置关键字和常用类型名。4. 语法与缩进SyntaxError 和 IndentationError4.1 中文标点引起的SyntaxError新手最冤的错误就是SyntaxError尤其是下面这种SyntaxError: invalid character (UFF1A)说白了就是代码里混入了中文标点。全角冒号、全角括号、中文引号在Python解释器眼里都是非法字符。排查方法很简单看到invalid character优先检查当前行和上一行末尾有没有中文标点。编辑器里开启显示空白字符的功能全角字符很容易暴露出来。4.2 IndentationError: unexpected indent缩进错误是Python特色因为Python用缩进表示代码块。常见报错IndentationError: unexpected indent以及混用Tab和空格时编辑器看着对齐了解释器却认为层级混乱TabError: inconsistent use of tabs and spaces in indentation解决方案很统一全部用四个空格缩进不要用Tab。编辑器里统一配置或者干脆打开convert tabs to spaces。为什么Python不让你混用因为代码块边界完全靠缩进定义混用等于边界模糊解释器没法判断你的意图。这个设计虽然让新手难受但也逼着你写出整齐的代码从这点看算是善意的强制。4.3 括号不匹配的报错写复杂表达式时括号少写一个或者多写一个Python会报SyntaxError: unexpected EOF while parsing这种错误在长链式调用或嵌套函数里特别常见。我的习惯是表达式超过一层括号就拆开写或者每写完一个括号就数一遍。编辑器里的括号高亮功能一定要开光标移到括号上配对的那个会高亮少写了立刻就能看出来。5. 变量作用域NameError 和 UnboundLocalError5.1 NameError: name xxx is not definedNameError看起来是变量没定义实际原因五花八门变量拼写错误变量在当前作用域确实没定义引用了别的模块里的变量但没导入变量在函数内部使用但定义在函数外部且没有声明global排查时先看拼写再看作用域。Python查找变量的顺序是局部作用域、闭包作用域、全局作用域、内置作用域也就是所谓的LEGB规则。报NameError基本可以断定这四级作用域里都没有这个名字。5.2 UnboundLocalError: local variable referenced before assignment这个错误比NameError更有迷惑性示例count 0 def increment(): count 1 # 报错 increment()看代码你可能觉得没问题全局有个count函数里直接加1为什么报错因为Python规定函数里如果有对某个变量赋值的操作这个变量在整个函数体里都被视为局部变量。count 1对count做了赋值所以count被当作局部变量但你执行时又要先读取count的值局部count此时还没初始化就报referenced before assignment。解决办法是在函数内部先声明global countcount 0 def increment(): global count count 1更推荐的做法是别用全局变量用参数传进传出或者放在类里作为实例属性。在闭包场景中对应的关键字是nonlocaldef outer(): value 0 def inner(): nonlocal value value 1 return inner这种错误在写GUI程序、写游戏循环计数的功能里非常常见理解了赋值即局部变量的规则基本不会再犯。6. 导入与依赖ModuleNotFoundError 和 ImportError6.1 ModuleNotFoundError: No module named xxx这个报错说明了两个问题要么这个模块没安装要么安装了但当前Python环境找不到。很多朋友处理这个错误时会陷入死循环先pip install xxx发现还是报错再装一遍还是报错。关键要明白pip装到哪个环境和你的脚本用哪个Python解释器必须是同一个。否则你在系统Python里装了包但项目用的是虚拟环境解释器自然找不到。检查思路是这样的which python # 或 where python which pip # 或 where pip这两个路径应该在同一个目录下。建议所有项目都创建虚拟环境python -m venv venv source venv/bin/activate # Windows上执行 venv\Scripts\activate pip install 需要安装的包这样环境隔离得干干净净不会再出现装了这个环境却用到那个环境的诡异问题。6.2 循环导入ImportError: cannot import name还有一种ImportError特别磨人就是循环导入。模块A导入了模块B模块B又导入了模块APython在导入过程中绕圈圈最后告诉你cannot import name xxx from partially initialized module。打破循环导入的方法有几种。最推荐的是重构模块结构把公共部分抽到一个独立模块里。其次是延迟导入——把导入语句写在函数内部而不是模块顶层def get_helper(): from utils import helper # 用到时才导入 return helper顶层导入在模块加载时就要完成函数内导入则等到函数执行时才触发此时两个模块都已经加载完毕就不会撞车了。这个问题在设计业务层和工具层时容易遇到注意保持依赖方向单向别让模块交叉引用。7. 文件与路径FileNotFoundError 和 UnicodeDecodeError7.1 FileNotFoundError: 找不到文件明明文件就在项目目录里一运行就报FileNotFoundError: [Errno 2] No such file or directory这是路径问题最常见的原因有相对路径基准不对。程序运行时的工作目录不一定是项目根目录如果你在别处启动了脚本相对路径就失效Windows下反斜杠转义问题C:\Users\name里的\U会被当成Unicode转义解决方案就是绝对路径或者用pathlib处理from pathlib import Path BASE_DIR Path(__file__).resolve().parent # 脚本所在目录 file_path BASE_DIR / data / config.json with open(file_path, encodingutf-8) as f: data f.read()Path(__file__).resolve().parent拿到的是当前文件所在目录整个项目移动位置也不影响。这是我最推荐的文件路径写法没有之一。7.2 UnicodeDecodeError: gbk codec cant decode在Windows上读取文本文件最容易报这个错因为Windows默认编码是GBK而文件本身是UTF-8保存的UnicodeDecodeError: gbk codec cant decode byte 0x... in position ...解决方案就是打开文件时显式指定编码with open(data.txt, encodingutf-8) as f: content f.read()写入时同样指定encodingutf-8从根本上避免混乱。还有个细节是不同平台默认编码不一致代码里凡是涉及open()的地方都写上encodingutf-8会少踩很多坑。另外如果你用requests拿到网页内容注意响应头里的charset有些老网站是GB2312或ISO-8859-1编码做解码时也得先确认实际编码别一律按UTF-8硬解。8. 隐蔽陷阱可变默认参数与深浅拷贝8.1 可变默认参数的值被共享这个错误不会报错但结果完全不对属于最阴的一类。看例子def add_item(item, container[]): container.append(item) return container第一次调用返回[1]第二次调用返回[1, 2]看起来还行但如果你在不同上下文里调用这个函数会发现所有调用都在往同一个列表里塞东西。这是因为默认参数在函数定义时创建一次之后所有调用共用它。正确做法是用None作为默认值在函数内部新建def add_item(item, containerNone): if container is None: container [] container.append(item) return container这种坑在老代码里特别隐蔽不仔细看根本发现不了我建议写函数时坚持一个原则默认参数只用不可变类型int、str、tuple、None可变类型一律用None占位。8.2 浅拷贝带来的数据联动修改列表、字典这类可变对象直接赋值只是拷贝引用list_a [1, 2, 3] list_b list_a list_b.append(4) # list_a 也变成 [1, 2, 3, 4]很多人在这里翻车。需要真正独立副本时使用copy模块import copy list_b list_a.copy() # 浅拷贝适合一维列表 dict_b copy.deepcopy(dict_a) # 深拷贝适合嵌套结构浅拷贝只复制最外层嵌套的子列表仍然是共享的。处理二维列表或嵌套字典时务必用deepcopy不然改内部元素会神奇地影响原数据。我处理配置数据时凡是涉及嵌套结构的复制一律deepcopy绝不图省事用等号赋值。8.3 遍历字典时修改字典的RuntimeError运行中遇到这个错RuntimeError: dictionary changed size during iteration原因是你在for循环遍历字典的同时删除了或添加了键。Python的字典迭代器要求迭代期间字典结构不能变否则直接抛错。安全删除方式是收集键再统一删to_delete [key for key in data if data[key] 0] for key in to_delete: del data[key]或者用字典推导式生成新字典更Pythonicdata {key: value for key, value in data.items() if value 0}9. 排查报错的通用方法论9.1 从最后一行开始看遇到任何报错不要从头开始读那一大段红色的堆栈信息直接拖到最底部看最后一行的异常类型和消息。往往一句话就能定位问题。然后再往上看Traceback中间的部分它会标出调用链和具体出错的文件、行号。我平时排查顺序就是什么类型的异常在哪个文件的第几行然后跳到那一行看代码。95%的问题在这一步就解决了。9.2 用print打点定位有人觉得用print调试不够高级但它是排查问题最快速的手段尤其是在脚本执行流程复杂的时候。在可疑函数入口打一个在关键计算步骤打一个看变量实际值是不是符合预期很快就能锁定是哪一步产生的异常。注意排查完记得删掉这些调试语句别留在正式代码里。9.3 善用异常捕获但别滥用try-except的合理用法是包裹那些确实有可能出异常的操作比如文件读取、网络请求、用户输入转换。但不要为了图省事把整个函数体包在一个超大try里然后except Exception: pass——这会把真正的逻辑错误也吞掉程序不报错但结果全错排查成本更高。写异常处理时尽量精确捕获异常类型比如try: with open(path, encodingutf-8) as f: data json.load(f) except FileNotFoundError: logger.error(配置文件不存在: %s, path) raise except json.JSONDecodeError: logger.error(配置文件不是合法JSON: %s, path) raise9.4 看懂Traceback里的调用链Traceback是一层一层向上展开的最底部是异常真正抛出的位置往上是调用它的函数再往上是更外层的调用者。我习惯从最底部往上反着看先看哪里抛的再看是谁调的这样理清调用关系比乱猜要快得多。10. 常见错误速查表与我的调试习惯我整理了一个平时用的速查表按错误类型归类方便快速对照报错信息核心原因快速修复AttributeError: NoneType object has no attribute上一步返回了None检查前一步返回值做空值保护KeyError: xxx字典里没有这个键改用.get(key, default)IndexError: list index out of range索引超出列表长度先判断len(list)再取元素ValueError: invalid literal for int()字符串无法转数字用try/except封装转换TypeError: 参数数量或类型不对调用签名不匹配或类型不能混合使用检查函数定义和传参SyntaxError: invalid character中文标点混入代码检查全角冒号、括号、引号IndentationError / TabError缩进不一致或Tab/空格混用统一用四个空格NameError: name xxx is not defined变量未定义或拼写错误检查拼写和作用域UnboundLocalError函数内赋值导致变量被视为局部需要时用global或nonlocalModuleNotFoundError模块未安装或环境不对检查pip和python是否同一环境FileNotFoundError文件路径不对用pathlib和绝对路径UnicodeDecodeError文件编码与默认编码不一致open()时显式指定encodingRuntimeError: dictionary changed size遍历时修改了字典结构收集键后再删或用推导式最后分享一个我自己的习惯在项目根目录建一个DEBUG.md文件每遇到一个陌生的报错就把报错信息、触发场景、解决方案记一笔。半年下来这本小册子就成了自己的专属错误字典再遇到同类问题打开一翻几秒钟就定位了。报错不是Python在刁难你而是它在很努力地告诉你程序出了什么问题。读懂了它的提示你的调试速度至少能翻一倍。