异常处理是写出"能跑"和"能用"之间那道真正的分水岭。很多人用了多年 Python,try/except 写得飞起,却在自定义异常、异常链、finally 的边界行为上踩过无数坑。下面这份指南,从核心概念到工程实践,帮你把这块知识彻底理清楚。
Python 的异常本质上是一棵继承树。所有异常都从 BaseException 派生,而我们日常打交道的几乎都是 Exception 的子类。

try/except/else/finally 四个关键字各司其职 :
try:放可能出错的代码except:捕获并处理异常else:try 块没有抛出异常时才执行(常被忽视的好东西)finally:无论如何都执行,用于清理资源 复制代码try:
result = int(input("请输入数字: "))
except ValueError as e:
print(f"输入不合法: {e}")
else:
print(f"成功,结果是 {result}") # 只有没异常时才跑
finally:
print("无论如何都会执行这里")
内置异常太通用了。ValueError 能告诉你"值有问题",但说不清楚是"用户输入非法"还是"配置文件格式错误"。自定义异常让错误有名有姓,调用方可以精准捕获,日志也更清晰。
工程上的标准做法是先建一个项目级基类,再按模块细分:
复制代码# exceptions.py —— 项目异常统一定义class AppError(Exception):
"""项目所有自定义异常的根基类"""
passclass DatabaseError(AppError):
"""数据库相关错误"""
def __init__(self, message: str, query: str = None):
super().__init__(message)
self.query = query # 附带上下文信息class NetworkError(AppError):
"""网络请求相关错误"""
def __init__(self, message: str, status_code: int = None, url: str = None):
super().__init__(message)
self.status_code = status_code
self.url = urlclass ValidationError(AppError):
"""数据校验错误"""
def __init__(self, field: str, reason: str):
super().__init__(f"字段 '{field}' 校验失败: {reason}")
self.field = field
self.reason = reason
__str__ 和附加信息自定义异常可以携带结构化数据,这比把所有信息塞进一个字符串要优雅得多 :
复制代码class PCSException(AppError):
def __init__(self, message: str, reason: str = None):
super().__init__(message)
self.reason = reason def __str__(self):
base = f"[PCS_ERROR] {self.args[0]}"
if self.reason:
base += f"n 原因: {self.reason}"
return base
add_note:动态追加上下文Python 3.11 引入了 add_note(),可以在不改变异常类型的前提下追加说明,特别适合在异常向上冒泡时层层补充信息 :
复制代码try:
load_config("config.yaml")
except FileNotFoundError as e:
e.add_note("提示:请先运行 `init` 命令生成默认配置文件")
raise # 重新抛出,附带了额外说明
raise from 的精髓当你在处理一个异常的过程中又触发了另一个异常,Python 会自动把两者关联起来,这叫隐式异常链(__context__)。但更推荐的是显式异常链,用 raise ... from ... 明确表达因果关系。
复制代码# 隐式链(自动发生,但语义不清晰)
try:
open("不存在的文件.txt")
except FileNotFoundError:
raise RuntimeError("初始化失败") # traceback 会显示两个异常# 显式链(推荐!语义清晰)
try:
open("不存在的文件.txt")
except FileNotFoundError as e:
raise RuntimeError("初始化失败:配置文件缺失") from e
输出的 traceback 会明确写出:The above exception was the direct cause of the following exception,一眼就能看清楚根因。
raise from None 隐藏实现细节有时候底层异常是内部实现细节,不应该暴露给调用方(比如数据库驱动的原始错误):
复制代码try:
db_driver.execute(sql)
except SomeLowLevelDriverError as e:
# 不想让调用方看到底层驱动细节
raise DatabaseError("数据库查询失败", query=sql) from None
from None 会切断异常链,让错误信息更干净。

finally 的正确姿势与陷阱finally 的本质finally 的承诺是:无论发生什么,我都会跑。不管 try 正常结束、except 捕获了异常、还是遇到 return/break/continue,finally 都不会缺席。这让它成为资源释放的最佳位置。
复制代码def read_file(path):
f = None
try:
f = open(path, 'r')
return f.read()
except IOError as e:
print(f"读取失败: {e}")
return None
finally:
if f:
f.close() # 即使 return 了,这里也会执行
陷阱一:finally 里的 return 会吞掉异常
复制代码def dangerous():
try:
raise ValueError("出错了!")
finally:
return "看起来没问题" # ️ 异常被静默吞掉了!result = dangerous() # 不会抛异常,返回 "看起来没问题"
这是最隐蔽的 bug 之一——finally 里的 return 会无声无息地压制异常。
陷阱二:finally 里再次抛异常,原始异常丢失
复制代码def also_dangerous():
try:
raise ValueError("原始错误")
finally:
raise RuntimeError("清理时出错") # 原始 ValueError 就此消失
陷阱三:finally 不等于"异常被处理了"
finally 执行完后,如果没有 except 捕获,异常依然会继续向上传播。很多初学者以为进了 finally 就万事大吉,其实不然。
with 替代手动 finally绝大多数资源管理场景,with 语句(上下文管理器)比手写 finally 更安全、更优雅:
复制代码# 不推荐:手动管理
f = open("data.txt")
try:
data = f.read()
finally:
f.close()# 推荐:with 语句
with open("data.txt") as f:
data = f.read() # 退出 with 块时自动关闭,即使抛异常也一样
| 原则 | 好的做法 | 坏的做法 |
|---|---|---|
| 精准捕获 | except ValueError | except Exception 或裸 except: |
| 不要吞异常 | 至少记录日志再 raise | except: pass |
| 异常要有意义 | 自定义异常 + 上下文信息 | 只抛 Exception("出错了") |
| 资源管理 | 用 with 语句 | 手写 finally + close() |
| 异常链 | raise NewError(...) from e | 裸 raise NewError(...) 丢失根因 |
复制代码import logginglogger = logging.getLogger(__name__)class ServiceError(Exception):
"""服务层统一异常基类"""
def __init__(self, message: str, code: int = 500):
super().__init__(message)
self.code = codeclass UserNotFoundError(ServiceError):
def __init__(self, user_id: int):
super().__init__(f"用户 {user_id} 不存在", code=404)
self.user_id = user_iddef get_user(user_id: int) -> dict:
try:
# 模拟数据库查询
raw = db.query(f"SELECT * FROM users WHERE id={user_id}")
if not raw:
raise UserNotFoundError(user_id)
return raw
except DatabaseConnectionError as e:
# 将底层异常转换为业务异常,保留因果链
raise ServiceError("数据库连接失败,请稍后重试", code=503) from e
except UserNotFoundError:
raise # 已经是业务异常,直接向上传
except Exception as e:
# 兜底:记录日志,重新抛出
logger.exception("get_user 发生未预期错误, user_id=%s", user_id)
raise ServiceError("内部错误") from e
当你用 asyncio 或 concurrent.futures 跑并发任务,多个子任务可能同时失败。Python 3.11 引入的 ExceptionGroup 专门处理这种情况 :
复制代码# 捕获异常组中的特定类型
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(task_a())
tg.create_task(task_b())
except* ValueError as eg:
for exc in eg.exceptions:
print(f"捕获到 ValueError: {exc}")
except* NetworkError as eg:
print(f"共 {len(eg.exceptions)} 个网络错误")
注意这里用的是 except*(带星号),这是专门配合异常组的新语法。

Python 异常处理的进阶,核心在于三件事:让异常有意义(自定义异常 + 层级设计)、让因果清晰(显式异常链 raise from)、让资源安全(with 语句 + 谨慎使用 finally)。把这三点做好,代码的健壮性和可维护性会有质的飞跃。
参考来源