Python error handling and debugging guide

In the entire process of Python development, error handling is the line of defense to ensure the robustness of the code, debugging tools are the scalpels for troubleshooting, and automated testing is the safety net for refactoring and iteration—all three are indispensable and can help you evolve from a "run once and it's done" script to "long-term stable delivery" of production-grade code.


1. Three common types of problems in development

Not all program exceptions are called bugs. Only by accurately distinguishing the type of problem can you choose the right tools and strategies.

1.1 Program logic/implementation errors (Bugs)

This type of error is purely a pitfall created by the programmer and must be fixed in order for the program to work as expected. Common examples:

  • Variable name misspelling (agewritten asagw
  • Type mixing (in Python 3"30" + 30Throw TypeError directly)
  • Missing boundary conditions (empty list index, negative modulo, etc. scenarios)

Modern Python has tools that can prevent some logical bugs before running:

# 1. 类型提示(Python 3.5+),配合 mypy 静态检查
def calculate_discount(price: float, rate: float) -> float:
    # 2. 显式检查业务规则
    if rate < 0 or rate > 1:
        raise ValueError("折扣率必须在 0~1 之间")
    return price * (1 - rate)

cooperatemypyWith static checking tools, many type errors will be discovered before deployment, and there is no need to wait until runtime.

1.2 User input error

This is an externally triggered but predictable problem. The solution is "early interception + friendly feedback". The stack information must not be dumped directly to the end user. For example:

  • The email format is incorrect
  • Enter age as a string or negative number
  • The two passwords during registration are inconsistent

Pydantic in the Python ecosystem is a powerful tool for dealing with such problems. It has built-in common validators and can generate clear error messages:

from pydantic import BaseModel, EmailStr, field_validator

class UserSignup(BaseModel):
    email: EmailStr
    password: str
    confirm_password: str
    age: int

    @field_validator("confirm_password")
    def passwords_match(cls, v, info):
        if "password" in info.data and v != info.data["password"]:
            raise ValueError("两次输入的密码不一致")
        return v

    @field_validator("age")
    def age_positive(cls, v):
        if v <= 0:
            raise ValueError("年龄必须大于 0")
        return v

# 测试输入
try:
    user = UserSignup(
        email="test#example.com",   # 故意写错邮箱格式
        password="123456",
        confirm_password="123457",  # 密码不一致
        age=-5                      # 负年龄
    )
except Exception as e:
    print(e)   # 会自动汇总所有字段的错误及原因

1.3 External environment exception during runtime

This is a problem that is beyond the control of the program itself. If it is not dealt with, it will crash directly. for example:

  • File deleted midway
  • Calling third-party API timeout
  • Database connection lost

The core of the solution is "Graceful downgrade" or "Automatic retry". Combined with the context manager (with)andtenacityLibrary, it will be very comfortable to write:

import requests
from tenacity import retry, stop_after_attempt, wait_exponential

# 指数退避重试:最多重试 3 次,间隔 2~10 秒
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def fetch_user_profile(user_id: int):
    response = requests.get(f"https://api.example.com/users/{user_id}", timeout=5)
    response.raise_for_status()   # 非 2xx 响应会抛出异常
    return response.json()

Local file operations also need to be done carefully:

def read_config():
    try:
        with open("config.yaml", "r") as f:
            return f.read()
    except FileNotFoundError:
        print("⚠️  未找到配置文件,使用默认配置")
        return DEFAULT_CONFIG
    except IOError as e:
        print(f"❌  配置文件读取失败: {e}")
        exit(1)   # 严重错误直接退出程序

2. Best practices for Python exception-handling

Python usestry-exceptMechanism to catch runtime exceptions, but abuse of nakedexcept, The capture range is too large** is a common misunderstanding among novices.

2.1 Standardized infrastructure

import logging

# 配置全局日志(比 print 专业得多)
logging.basicConfig(
    level=logging.WARNING,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

def divide(a: float, b: float):
    try:
        # ① 只把可能抛出特定异常的代码放这里
        result = a / b
    except ZeroDivisionError:
        # ② 精准捕获,不放过不该放过的异常
        logging.error("除数不能为 0")
        return None
    except TypeError as e:
        # ③ 用异常链保留原始信息
        logging.error(f"输入类型错误: {e}", exc_info=True)
        raise ValueError("a 和 b 必须是数字") from e
    else:
        # ④ else 块只放 try 成功后才执行的逻辑
        logging.info(f"计算成功: {a} / {b} = {result}")
        return result
    finally:
        # ⑤ finally 块无论成功失败都会执行(适合清理资源)
        logging.debug("除法函数执行完毕")

2.2 Custom exceptions

When the project becomes complex, don’t just throwValueError / TypeErrorThis type of built-in exception. Custom exceptions can make errors more identifiable, and upper-level callers can handle them hierarchically:

# 自定义基础异常
class EShopError(Exception):
    """电商系统基础异常"""
    def __init__(self, message: str, code: int = 500):
        super().__init__(message)
        self.code = code

# 具体业务异常
class StockShortageError(EShopError):
    """库存不足异常"""
    def __init__(self, product_id: int, remaining: int):
        super().__init__(f"商品 ID {product_id} 库存不足,仅剩 {remaining} 件", code=400)
        self.product_id = product_id
        self.remaining = remaining

# 上层分层调用
try:
    checkout_cart(123)   # 假设内部触发了库存不足
except StockShortageError as e:
    # 处理库存不足:返回友好提示给用户
    print(f"前端提示: {e}")
except EShopError as e:
    # 处理其他电商系统内部错误
    logging.error(f"电商内部错误: {e}", exc_info=True)
except Exception as e:
    # 最后一层兜底(尽量少用)
    logging.critical(f"未预期的严重错误: {e}", exc_info=True)

3. Tools and techniques for efficient debugging

When error handling is not covered, or logic bugs are hidden deep, it’s time for debugging tools to come into play - don’t just useprintdebug! **

3.1 Built-in debugger pdb (essential for emergencies)

pdbYes, Python comes with a command line debugger, no installation required, especially suitable for GUI-less environments such as servers:

import pdb

def buggy_sorting(arr: list):
    pdb.set_trace()   # 这里会进入调试模式
    # Python 3.7+ 也可以直接用 breakpoint()

    sorted_arr = sorted(arr, reverse=True)
    return sorted_arr[:3]   # 本意是返回前 3 大的数

buggy_sorting([5, 2, 9, 1, 7])

Commonly used pdb commands (just remember these are enough):

CommandAbbreviationFunction
nextnExecute the next line (without entering the function)
stepsExecute the next line (will enter the function)
continuecContinue execution until next breakpoint
listlDisplay the codes before and after the current position
printp 变量名Print variable value
quitqForce quit debugger

3.2 Modern debugging tools (first choice for daily development)

For daily development, it is strongly recommended to use the IDE's built-in debugger (VS Code, PyCharm). You can click to set breakpoints, view the variable stack, and debug line by line. The visual experience far exceeds the command line.

If you still prefer the command line but feelpdbToo simple, you can try ipdb, which supports syntax highlighting and auto-completion:

pip install ipdb
# 只需把 pdb 替换为 ipdb
import ipdb; ipdb.set_trace()

4. Automated testing: a safety net for refactoring and iteration

Automated testing is not a waste of time - code without tests, refactoring is like defusing a bomb.

4.1 Use pytest to write unit tests (10 times simpler than unittest)

unittestIt is a built-in framework, but the syntax is long-winded and is almost used in modern projects.pytest

pip install pytest

There are conventions for naming test files and functions:

# 文件命名:test_*.py 或 *_test.py
def add(a: float, b: float) -> float:
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("a 和 b 必须是数字")
    return a + b

# 测试函数命名:test_*
def test_add_positive_numbers():
    assert add(2, 3) == 5     # 直接 assert,不用记 self.assertEqual

def test_add_negative_numbers():
    assert add(-1, 1) == 0

def test_add_type_error():
    with pytest.raises(TypeError):   # 检查是否抛出预期异常
        add("2", 3)

Execute in terminalpytest test_math.pyAll tests will be automatically discovered and run.

4.2 Check test coverage with coverage.py

coverage.pyIt can tell you "which code has not been covered by tests" to avoid missing key logic:

pip install coverage

# 运行测试并收集覆盖率数据
coverage run -m pytest test_math.py

# 查看报告
coverage report -m

5. Summary: Core Principles

  1. Accurately distinguish problem types - Don’t treat user input errors as logic bugs, and don’t skip external environment exceptions.
  2. Defensive programming, but don’t overdo it – validate input ahead of time, but don’t fill every function with useless checks.
  3. Log is 100 times more effective than print - must be used in production environmentloggingmodule.
  4. Automated testing is the bottom line - at least write unit tests for the core business logic.
  5. Fail early and give friendly feedback - Report an error immediately when a problem is discovered, and give clear and useful error information.

Mastering error handling, debugging and automated testing will make your Python code more robust and your development efficiency will reach a new level!