1. 35
  1. 7

    Either there was a shift in what’s considered “good” in Python, or it’s just my bias towards such articles, but this quote is quintessentially un-Pythonic, in my book:

    If the code is widespread enough you’re almost guaranteed that someone is using your code in a way you didn’t intend.

    In general, what’s wrong with people using your code in ways you haven’t foreseen? You are responsible for making it clear how to use your code, but if people still do it differently, may be they have their reasons? In Python it was called the “consenting adults” principle.

    In fact, if you think about it, people using your library always have more context about their use cases than you when you’re writing it!

    Another quote, “but what about beginners to Python or the library?”, shows a rather condescending assumption where a grizzled pro is supposed to write an iron-clad, bug-free library before handing it down to infantile juniors who would undoubtedly wreak all sorts of havoc, casting a shade on the perfect library code (poetic exaggeration). Sometimes it’s the other way around.

    In particular, the suggested function signature:

    def process_data(data, /, *, encoding="ascii"):

    … forces my to parrot the name encoding in situation like this:

    data, encoding = load_data()
    process_data(data, encoding=encoding)

    … only because you didn’t trust me in taking care about readability of my own code.

    1. 7

      There’s “haven’t foreseen” as in, “ah that’s interesting” but also as in “that’s totally not how I though about it so it’s not tested and will accidentally get broken in the next minor revision”. It’s the second one that a stricter approach can help you avoid.

      1. 1

        How can you know the more strict API is correct, and you won’t discover a need for an incompatible change in it later? It’s juts a completely orthogonal question.

        You are not responsible for how your users use your code. The moment they started dependent on your library they become responsible for testing how it behaves in all their uses.

        1. 4

          kw-only arguments provide a stricter signature, and just like type annotations, these are helpful to check for correctness, and also to evolve a codebase over time.

          as an example, imagine that the library evolves into using input_encoding= and output_encoding= arguments for this function, because of new insights:

          • with kw-only arguments, the new arguments can be added, and it’s easy to keep encoding= working for backwards compatibility. code using the library will keep working without changes.

          • without kw-only arguments, what would process(data, 'ascii', 'utf-8') mean? input and output encoding, as the new approach suggests? nope! it suddenly starts to matter which arguments are declared first, and that depends on the history of the code, which noone should care about anymore for new uses. but the legacy encoding argument has to be forever kept in the same position because otherwise it would break backward-compatibility. so process(data, 'ascii', 'utf-8') would specify both encoding and the new input_encoding argument (or output_encoding, depending on the order in which they are specified; see where this is going?). this is surprising and perhaps even wrong since encoding and input_encoding semantically overlap. sure, one could write extra checks and code to guard against this, but it’s a hassle and the end result is not nice.

          kw-only arguments are a great way to avoid footguns like this altogether.

          taking this approach has absolutely nothing to do with ‘annoying’ people who don’t like to type an argument name. code is read way more often than it’s written. ‘readability counts’ and ‘explicit is better than implicit’, as the python mantras go.

          there’s absolutely nothing wrong with ‘parrotting’ code like encoding=encoding. unlike positional and positional-or-keyword arguments, kw-only args make it clear at the call site what their meaning is, even for people who never saw the code before. that’s a good thing.

          and as a bonus it also becomes impossible to accidentally swap arguments around, something that type checkers cannot reliably check (because the arguments may be of the same type).

          1. 4

            How can you know the more strict API is correct,

            It’s not really about “correct” in some absolute sense. Just “less likely to cause issues later”. You’re of course not responsible for downstream code, but at the same time you probably want to avoid unexpected issues if you can tighten up your published signatures.

            If you wouldn’t want to purposefully break downstream code, why would you avoid methods to protect from accidental breakage?

            1. 1

              How do you know if your tightening up will avoid or solve problems. Generally, you can’t know the uses you’re preventing are going to be useful to the caller. Especially stylistic choices! All you can do is to clearly document what the function expects and what it returns. But don’t police your future users.

              1. 2

                But don’t police your future users.

                On the flip-side, don’t try to dictate what decisions open-source maintainers make.

      2. 6

        Good post! In general I’d recommend more (Python) devs read Effective Python which does a pretty good job of covering parts of the language you don’t always think of or good ways to write better code.

        1. 4

          I wish more library authors took this approach where using the API is clear and obvious. I’m indifferent on the positional-only arguments even though it leads to cleaner-looking code, but keyword-only arguments benefit the clear and obvious usage.

          1. 8

            So many times I have been stumped by functions that have no parameter or return typr signatures. One function I used recently returns Optional[Union[Tuple[str, int, int], Tuple[str, str, str]]]

            Once I figure out what it returns, I immediately add the signature or wrap the function inside another with signature. This is the only way I can sleep.

            1. 5

              Totally empathize. There’s a weird subset of Python devs who refuse to add type annotations entirely. I could understand not wanting to type-annotate your private code, but at the very least add what’s there to your public API! I think doing so makes them realize they have these totally unwieldy types that put all the abstraction leaking onto the caller like multiple types being possible to be returned everywhere.