Hacker News new | ask | show | jobs
by chipx86 346 days ago
That's always the challenge when iterating on interfaces that other people depend on.

What we do is go through a deprecation phase. Our process is:

* We provide compatibility with the old signature for 2 major releases.

* We document the change and the timeline clearly in the docstring.

* The function gets decorated with a helper that checks the call, and if any keyword-only arguments are provided as positional, it warns and converts them to keyword-only.

* After 2 major releases, we move fully to the new signature.

We buit a Python library called housekeeping (https://github.com/beanbaginc/housekeeping) to help with this. One of the things it contains is a decorator called `@deprecate_non_keyword_only_args`, which takes a deprecation warning class and a function using the signature we're moving to. That decorator handles the check logic and generates a suitable, consistent deprecation message.

That normally looks like:

    @deprecate_non_keyword_only_args(MyDeprecationWarning)
    def my_func(*, a, b, c):
        ...
But this is a bit more tricky with dataclasses, since `__init__()` is generated automatically. Fortunately, it can be patched after the fact. A bit less clean, but doable.

So here's how we'd handle this case with dataclasses:

    from dataclasses import dataclass
    
    from housekeeping import BaseRemovedInWarning, deprecate_non_keyword_only_args
    
    
    class RemovedInMyProject20Warning(BaseRemovedInWarning):
        product = 'MyProject'
        version = '2.0'
    
    
    @dataclass(kw_only=True)
    class MyDataclass:
        a: int
        b: int
        c: str
    
    MyDataclass.__init__ = deprecate_non_keyword_only_args(
        RemovedInMyProject20Warning
    )(MyDataclass.__init__)

Call it with some positional arguments:

    dc = MyDataclass(1, 2, c='hi')

and you'd get:

    testdataclass.py:26: RemovedInMyProject20Warning: Positional arguments `a`, `b` must be passed as keyword arguments when calling `__main__.MyDataclass.__init__()`. Passing as positional arguments will be required in MyProject 2.0.
      dc = MyDataclass(1, 2, c='hi')

We'll probably add explicit dataclass support to this soon, since we're starting to move to kw_only=True for dataclasses.
1 comments

Shouldn't you also be able to patch MyDataclass in a class decorator (on top of/after @dataclass)?
Yeah, that's the approach we'll be taking in housekeeping. I didn't want to complicate the example any more than I already did :)