绝大多数情况下自定义异常应继承Exception;避免继承BaseException;通过重写__init__和__str__携带结构化上下文;按恢复策略差异拆分异常类;集中定义在exceptions.py并控制__all__导出。

Python自定义异常该继承哪个基类
绝大多数情况下,直接继承 Exception 就够了。不要为了“语义清晰”去继承 RuntimeError、ValueError 等内置异常——除非你明确要触发某个已有错误处理逻辑(比如某些框架会专门捕获 ConnectionError 并重试)。
常见误操作是继承 BaseException:这会让异常绕过 except Exception: 捕获,连 sys.excepthook 都可能漏掉,调试时消失得无影无踪。
- 业务逻辑错误 → 继承
Exception - 需要被
except OSError:捕获的底层系统错误 → 继承OSError - 绝对不要继承
BaseException(除SystemExit、KeyboardInterrupt这类特殊用途)
如何让自定义异常携带上下文信息
靠重写 __init__ 和 __str__ 是最稳妥的方式。别依赖 args 元组拼接,容易在多语言环境或日志序列化时出错。
class ConfigLoadError(Exception):
def __init__(self, path: str, reason: str, line_no: int = None):
self.path = path
self.reason = reason
self.line_no = line_no
super().__init__(self._build_message())
def _build_message(self) -> str:
msg = f"Failed to load config from {self.path}: {self.reason}"
if self.line_no is not None:
msg += f" (line {self.line_no})"
return msg这样设计的好处:
立即学习“Python免费学习笔记(深入)”;
- 字段可被结构化日志(如
structlog)直接提取 - 调试时能直接访问
e.path、e.line_no,不用解析字符串 - 避免在
raise ConfigLoadError("a.json", "invalid JSON")中漏传参数导致AttributeError
什么时候该拆分成多个异常类而不是加 flag 参数
当错误恢复策略不同、日志级别不同、或上游调用方需要分别处理时,必须拆分。例如:
-
RateLimitExceeded(429,应退避重试)和InvalidAPIKey(401,应报错并通知运维)不能共用一个APICallError加error_type="rate_limit" -
TimeoutError和ConnectionRefusedError虽然都属网络问题,但前者可能重试,后者大概率要换节点 - 用
isinstance(e, TimeoutError)比e.error_type == "timeout"更可靠、IDE 可跳转、类型检查器能识别
模块内异常组织与导入陷阱
把所有自定义异常集中放在模块顶层的 exceptions.py 文件里,而不是散落在各处。否则容易出现循环导入,尤其是当你在 models.py 里 raise 异常,又在 exceptions.py 里 import models 做类型提示时。
对外暴露时只用 __all__ 控制,避免用户误导入内部辅助异常:
# exceptions.py
class _InternalValidationError(Exception): # 下划线前缀,不公开
pass
class ValidationError(Exception):
pass
all = ["ValidationError"]
调用方只需 from mypkg.exceptions import ValidationError,不会意外拿到未文档化的内部异常。
真正容易被忽略的是:异常类本身也是对象,如果它引用了大对象(比如存了整个请求体 self.request_data = request),会导致内存泄漏——尤其在长周期服务中。只保留必要字段。










