Design by Contract in Python: proposal for native class invariants

But I don’t think you are doing yourself any favors by framing this as a suggestion to add it to python at this point in time. By doing that you are encouraging everyone (including myself) to provide counter arguments why not to do this and leading to an antagonistic relationship (as can be clearly seen from this thread).

If you instead frame it as a library/framework that solves some real world issues right now you will get more positive responses (even if it’s maybe fewer of them) and I think that increases your chance of this being adopted and maybe added to python (on a timeframe of 5 years at the low end ofcourse).

3 Likes

Totally fair. The original post wasn’t meant to bypass the slow path; it was just meant to get the conversation started and surface early thoughts. So thank you for your contribution.

For those curious about the broader motivation behind this or how it fits into a bigger effort, I’ve launched https://beyondtesting.dev to explore correctness, DbC, and developer trust from different angles. Still early, but growing.

1 Like

Late to party, but came across this looking for something along design by contract lines!

My suggestion is a variation on @bwoodsend suggestion; that supports invariant across multiple fields, inheritance, and pickling.

from typing import final, Self
import copy

class Account1:
    def __init__(self):
        self.balance = 0. # Calls setter and is therefore checked.

    @final
    def __invariant(self) -> None:  # Could check multiple fields.
        if self.balance < 0:
            raise ValueError("Balance must never be negative")

    @final
    def __replace__(self, **kwargs: Any) -> Self:  # Allow replacements.
        new = copy.deepcopy(self)
        for key, value in kwargs.items():
            setattr(new, key, value)
        return new

    def __setstate__(self, state: dict[str, Any]) -> None:  # Base class pickling/copying.
        self.__dict__.update(state)
        self.__invariant()

    @final
    @property
    def balance(self) -> float:
        return self.__balance

    @final
    @balance.setter
    def balance(self, value: float) -> None:
        self.__balance = value
        self.__invariant()

    def deposit(self, amount: float) -> None:
        self.balance += amount

    def withdraw(self, amount: float) -> None:
        self.balance -= amount

class NamedAccount(Account1):
    def __init__(self, name: str):
        super().__init__()  # Needs to be before call to setter so that __invariant sees a valid state.
        self.name = name
    
    @final
    def __invariant(self) -> None:  # Could check multiple fields.
        if not self.name:
            raise ValueError("Account name must not be empty")

    def __setstate__(self, state: dict[str, Any]) -> None:  # Derived class pickling/copying.
        super().__setstate__(state)
        self.__invariant()

    @final
    @property
    def name(self) -> str:
        return self.__name
    
    @final
    @name.setter
    def name(self, name: str) -> None:
        self.__name = name
        self.__invariant()

Notes

  1. Creation checked because property setter called from __init__ and __invariant from __setstate__.
  2. After an exception, object is in an invalid state. (Not a big issue for me, but different than @bwoodsend version.)
  3. Pickling probably won’t work with slots :frowning: (haven’t tested pickling with slots).
  4. Invariant is only checked on creation and when modified, unlike normal invariant that is checked before and after each call.
  5. Final is added to methods to remind people not to override them.
  6. Like everything in Python, you can circumvent the checks :slight_smile: .

Note: I changed the code to support copy.replace. 25 Aug 2025.

If there is ever a time to throw a runtime assertion error it is in production. Assert statements indicate the code has encountered a situation it is not able to handle. You don’t want to ignore that in production and forge ahead and execute code that knows it is operating outside what it can handle. Doing so is almost guaranteed to either encounter other runtime exceptions or to corrupt the data model. The latter is the bigger concern since tracking down where the model got into an “impossible” state is much harder than handling an exception that tells you where and what the problem is.

Don’t sweep bugs under the rug just because code is executing in the most critical environment. Let the assertions developers use to say “this code should not execute in this state” prevent the code from executing out of bounds and causing more problems.

The argument has been made that assertions shouldn’t be used at all since they are nothing more than checks that code executes properly and the only reason for disabling them is performance and correct functionality is higher priority than quickly executing code that shouldn’t be executed at all.

1 Like

Author of icontract here and also late to the party :-).

Please have a look at the previous longer discussion we had on python-idea mail list some years ago (2018, if I remember correctly) – the archive seems to be down at the moment, but here’s the link if you want to try later:

Icontract has been used in quite a few projects and larger code bases. There were very rarely issues with people forgetting to add a meta-class.

One of the blockers for formal verification in Python is that the standard library is not annotated with contracts. Hence, you reach quickly a point where symbolic execution and other approaches are not applicable in practice.

Btw., there are a lot of edge cases when it comes to inheritance – the implementation is not as trivial as it seems at first, and even after years of development, bugs still appear.

1 Like

Couldn’t this perhaps be handled via type hints, e.g. some typing.Condition[Greater[0] | Equal[0]], for the example the OP provided. Another name could be typing.Assert. The good thing about handing this work to checkers is, that users themselves can define wether they would like to follow contracts or not, and checkers can check it during tests, but ignore it at runtime. It would then be assumed, that some x: typing.Condition[Cond] always meets the condition cond.

This would sadly mean, that we’ll have to add following Special-Forms to typing(or more):

  • Greater
  • Equal
  • Less

Perhaps this would already satisfy the needs that users have. Obviously this list could also be extended later on.

1 Like

Like https://pypi.org/project/annotated-types/