From d0bc9a16cffc2afdbb1b59752afcfda00379c477 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 25 Nov 2025 16:22:31 +0100 Subject: [PATCH 1/2] ignore overloaded init, accept exceptions with str+pickle dunder, improve README blurb --- README.rst | 2 +- bugbear.py | 31 ++++++++++++++++++++++++++++- tests/eval_files/b042.py | 43 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ba1b1a8..2471c4f 100644 --- a/README.rst +++ b/README.rst @@ -288,7 +288,7 @@ second usage. Save the result to a list if the result is needed multiple times. **B041**: Repeated key-value pair in dictionary literal. Only emits errors when the key's value is *also* the same, being the opposite of the pyflakes like check. -**B042**: Remember to call super().__init__() in custom exceptions initalizer. +**B042**: Exception classes with a custom `__init__` should pass all args to `super().__init__()` in order to work correctly with `copy.copy` and `pickle`. Both `BaseException.__reduce__` and `BaseException.__str__` relies on the `args` attribute being set correctly, which is set in `BaseException.__new__` and `BaseException.__init__`. If you define `__init__` yourself without passing all arguments to `super().__init__` it is very easy to break pickling, especially if they pass keyword arguments which both `BaseException.__new__` and `BaseException.__init__` ignores. It's also important that `__init__` not accept any keyword-only parameters. Alternately you can define both `__str__` and `__reduce__` to bypass the need for correct handling of `args`. If you define str/reduce in super classes this check is unable to detect it, and we advise disabling it. Opinionated warnings ~~~~~~~~~~~~~~~~~~~~ diff --git a/bugbear.py b/bugbear.py index 1a3af60..d37ebcd 100644 --- a/bugbear.py +++ b/bugbear.py @@ -1749,10 +1749,39 @@ def is_exception(s: str): else: return + # if the user defines __str__ + a pickle dunder they're probably in the clear. + has_pickle_dunder = False + has_str = False + for fun in node.body: + if isinstance(fun, ast.FunctionDef) and fun.name in ( + "__getnewargs_ex__", + "__getnewargs__", + "__getstate__", + "__setstate__", + "__reduce__", + "__reduce_ex__", + ): + if has_str: + return + has_pickle_dunder = True + elif isinstance(fun, ast.FunctionDef) and fun.name == "__str__": + if has_pickle_dunder: + return + has_str = True + # iterate body nodes looking for __init__ for fun in node.body: if not (isinstance(fun, ast.FunctionDef) and fun.name == "__init__"): continue + if any( + (isinstance(decorator, ast.Name) and decorator.id == "overload") + or ( + isinstance(decorator, ast.Attribute) + and decorator.attr == "overload" + ) + for decorator in fun.decorator_list + ): + continue if fun.args.kwonlyargs or fun.args.kwarg: # kwargs cannot be passed to super().__init__() self.add_error("B042", fun) @@ -2411,7 +2440,7 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro "B042": Error( message=( "B042 Exception class with `__init__` should pass all args to " - "`super().__init__()` in order to work with `copy.copy()`. " + "`super().__init__()` to work in edge cases of `pickle` and `copy.copy()`. " "It should also not take any kwargs." ) ), diff --git a/tests/eval_files/b042.py b/tests/eval_files/b042.py index d1efcc3..952f029 100644 --- a/tests/eval_files/b042.py +++ b/tests/eval_files/b042.py @@ -1,3 +1,7 @@ +import typing +from typing import overload + + class MyError_no_args(Exception): def __init__(self): # safe ... @@ -46,6 +50,25 @@ class MyError_posonlyargs(Exception): def __init__(self, x, /, y): super().__init__(x, y) +# ignore overloaded __init__ +class MyException(Exception): + @overload + def __init__(self, x: int): ... + @overload + def __init__(self, x: float): ... + + def __init__(self, x): + super().__init__(x) + +class MyException2(Exception): + @typing.overload + def __init__(self, x: int): ... + @typing.overload + def __init__(self, x: float): ... + + def __init__(self, x): + super().__init__(x) + # triggers if class name ends with, or # if it inherits from a class whose name ends with, any of # 'Error', 'Exception', 'ExceptionGroup', 'Warning', 'ExceptionGroup' @@ -70,5 +93,23 @@ def __init__(self, x): ... # B042: 4 class ExceptionHandler(Anything): def __init__(self, x): ... # safe -class FooException: +class FooException: # safe, doesn't inherit from anything + def __init__(self, x): ... + +### Ignore classes that define __str__ + any pickle dunder +class HasReduceStr(Exception): + def __reduce__(self): ... + def __str__(self): ... + def __init__(self, x): ... + +class HasReduce(Exception): + def __reduce__(self): ... + def __init__(self, x): ... # B042: 4 +class HasStr(Exception): + def __str__(self): ... + def __init__(self, x): ... # B042: 4 + +class HasStrReduceEx(Exception): + def __reduce_ex__(self, protocol): ... + def __str__(self): ... def __init__(self, x): ... From f33b3fd218065d1cc400850f44e34c84c8438322 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 25 Nov 2025 20:08:55 -0600 Subject: [PATCH 2/2] Update README.rst Co-authored-by: Kurt McKee --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2471c4f..6b7081b 100644 --- a/README.rst +++ b/README.rst @@ -288,7 +288,7 @@ second usage. Save the result to a list if the result is needed multiple times. **B041**: Repeated key-value pair in dictionary literal. Only emits errors when the key's value is *also* the same, being the opposite of the pyflakes like check. -**B042**: Exception classes with a custom `__init__` should pass all args to `super().__init__()` in order to work correctly with `copy.copy` and `pickle`. Both `BaseException.__reduce__` and `BaseException.__str__` relies on the `args` attribute being set correctly, which is set in `BaseException.__new__` and `BaseException.__init__`. If you define `__init__` yourself without passing all arguments to `super().__init__` it is very easy to break pickling, especially if they pass keyword arguments which both `BaseException.__new__` and `BaseException.__init__` ignores. It's also important that `__init__` not accept any keyword-only parameters. Alternately you can define both `__str__` and `__reduce__` to bypass the need for correct handling of `args`. If you define str/reduce in super classes this check is unable to detect it, and we advise disabling it. +**B042**: Exception classes with a custom `__init__` should pass all args to `super().__init__()` to work correctly with `copy.copy` and `pickle`. Both `BaseException.__reduce__` and `BaseException.__str__` rely on the `args` attribute being set correctly, which is set in `BaseException.__new__` and `BaseException.__init__`. If you define `__init__` yourself without passing all arguments to `super().__init__` it is very easy to break pickling, especially if they pass keyword arguments which both `BaseException.__new__` and `BaseException.__init__` ignore. It's also important that `__init__` not accept any keyword-only parameters. Alternately you can define both `__str__` and `__reduce__` to bypass the need for correct handling of `args`. If you define `__str__/__reduce__` in super classes this check is unable to detect it, and we advise disabling it. Opinionated warnings ~~~~~~~~~~~~~~~~~~~~