4

I'm trying to know if we can freely raise new exceptions when maintaining methods of a versioned library.

Here is a minimal example of what the change could be:

import logging


def check_id_old(identifier):
    """
    :param str identifier: An identifier that's less than 10 characters long
    :return: a verified identifier
    """
    if len(identifier) > 10:
        logging.error("Unexpected identifier")
    return identifier[:10]


def check_id_new(identifier):
    """
    :param str identifier: An identifier that's less than 10 characters long
    :return: a verified identifier
    :raises ValueError: if the identifier exceeds 10 characters
    """
    if len(identifier) > 10:
        raise ValueError("Unexpected identifier")
    return identifier


long_id = "1234567891011"
print("You see me", check_id_old(long_id))
print("You won't see me", check_id_new(long_id))

While using an invalid identifier was not welcome before, it was fine as it only logged and didn't raise an exception, allowing the code to keep running. With the updated method, the app might break.

Does this make that type of change backward-incompatible?

1
  • 4
    It's definitely a breaking change - consider deprecating it with a python warning before you move all the way to raising an exception. See lesinskis.com/python_deprecation_tutorial.html. In case the link dies: replace the logging with warning.warn("Identifiers longer than 10 characters are deprecated and will cause a ValueError starting in version X", PendingDeprecationWarning), and then only introduce the exception change in the next major version.
    – Blackhawk
    Commented Jan 17, 2023 at 16:02

2 Answers 2

15

Yes, it is backward incompatible.

We can debate about whether theoretically it's backward compatible or not. The problem is that while in theory, both theory and practice agree, in practice, they don't.

I offer to you Hyrum's law, which put succinctly is:

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.

Whether you documented that the function may or may not throw, somebody1, somewhere1, has used the function and did not handle that exception properly.

Maybe it's not handled at all, maybe it's handled, but assumes that some step of the process has been reached which is no longer the case, maybe it's handled, but the code never actually ran and just plain doesn't work... doesn't matter, really. It's broken.

1By which I mean you, in that other library over there.

But breaking changes may be worth it!

This does not mean you should not change it anyway.

There are many cases where a breaking change is worth it: improving correctness, improving performance, improving ergonomics, improving maintainability, ... it's up to the maintainer to take such decisions, and they may wish to check with their clients the costs of such changes to better understand the costs/benefits.

If you do end up making such a change, document it front-and-center in the release notes:

  • People who read the release notes before upgrading can then review their uses as they do upgrade.
  • People who didn't read them, and are faced with a confusing issue, may save time if pointed in the right direction.
6
  • 1
    This makes it sound like there is no value in semantic versioning and explicitly documenting behavior, which I absolutely disagree. Yes people will ignore documentation and end up with undefined behavior. That's too bad for them, but they might learn valuable lessons. But deciding whether a change breaks the contract of the API or not is very valuable.
    – Voo
    Commented Jan 17, 2023 at 12:02
  • 3
    @Voo: I do believe in semantic versioning, but I am also a pragmatic. As an anecdote, for a long time, the Rust IP (or socket?) type was just a wrapper around the C struct. This was an implementation detail, of course. At some point, it was decided to switch to a native representation... and it was soon discovered that a number of foundational libraries in the ecosystem had been relying on this detail and the update them broke them utterly. So instead, the change was put on hold, the libraries fixed, then the team waited a year or two for usage to dwindle before actuating the change... Commented Jan 17, 2023 at 12:42
  • 1
    @Voo: ... As a maintainer, you can say "not my problem", "their fault", etc... You can. But breaking half the ecosystem by doing so isn't going to earn you any goodwill. So at some point you have to be pragmatic. Commented Jan 17, 2023 at 12:43
  • 1
    It's also a question of scale I think - replacing the internal representation of core functionality is probably reasonable to put into a major release even if it shouldn't be breaking. But changing a few APIs to throw exceptions instead of undefined behavior/swallowing them would be something I'd be somewhat comfortable to put into a minor library release (but again, if a million people complain during the beta, be pragmatic, absolutely)
    – Voo
    Commented Jan 17, 2023 at 13:00
  • 1
    @Voo: Exactly, and that's what I attempted to convey here, though in a more succinct manner. I've seen too many maintainers genuinely surprised by the backlash they received after effecting an in-theory backwards compatible change, or fixing a bug, etc... in theory, they were right to do the change... in practice, when you break your users, they may still not be happy, and "it's backward compatible as per our policy" is about the worst answer you can give. Hence, it's best to always assume a change is breaking, inquire whether it does break anyone, and adapt. Commented Jan 17, 2023 at 13:34
10

This depends entirely on what you documented as the behaviour of your library. If at one extreme you had explicitly stated in your documentation that

All methods in this library may throw any exceptions.

then it's clearly backwards compatible. If on the other hand, you had stated

Methods in this library will not throw any exceptions not explicitly listed in the documentation.

then it's clearly not backwards compatible.

If you hadn't documented the behaviour, then it's undefined as to whether this is backwards compatible or not; I note that there are plenty of situations in which check_id_old can throw an exception even though you haven't listed any - e.g. check_id_old(1).

Stepping back a bit, Python may not be the language for you if you want this kind of ultra-explicit definition of backwards compatibility.

10
  • 3
    Why would Python have lower backwards compatibility standards? No matter the language, I am annoyed if library breaks software one every update or for no good reason.
    – user694733
    Commented Jan 17, 2023 at 9:27
  • 2
    @user694733 it's about the philosophy of the language design; note how this question couldn't even be asked about Java because the language forces you to declare all exceptions a function might throw. Commented Jan 17, 2023 at 9:35
  • 4
    @PhilipKendall: You mean, apart from RuntimException, such as IllegalArgumentException, which is definitely used in user-land applications? Also, just because an exception is potentially thrown doesn't mean that applications are not built relying on the fact it's not thrown in a specific case -- and breaking that may break code regardless of whether the exception is caught... Commented Jan 17, 2023 at 9:45
  • 6
    @PhilipKendall False. Java does allow exceptions without declarations and in fact most people think checked exceptions were a mistake and they use only RuntimeException subclasses (e.g. Kotlin even though it's built on the JVM only uses unchecked exceptions)
    – GACy20
    Commented Jan 17, 2023 at 10:31
  • 3
    How is changing a function from returning a string to throwing an exception not a breaking change? Existing code that calls check_id_old("xxxxxxxxxxxxxxxxxx") will start failing. For a library the test is whether your users can upgrade the library version (eg to take a security fix) without changing their code. This change requires users of the library to change their code. Commented Jan 17, 2023 at 19:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.