Repository: Parquery/icontract

Issue:
Feature request: immediate invariant evaluation.
Hi,

Very much taken by your library's functionality: much appreciated!

I would like to request an (perhaps optional?) feature for immediate evaluation of invariants (optional because, no doubt, this incurs a runtime penalty).

At the moment, as per [the documentation](https://icontract.readthedocs.io/en/latest/usage.html#invariants), invariants are evaluated when an object's method is called...
```python
from dataclasses import dataclass
import icontract

@icontract.invariant(lambda self: self.bar >= 0)
@dataclass
class Foo():
    bar:int = 0
    def invoke(self): pass

obj = Foo(bar=0)
obj.bar = -10   # <- Violates the contract.
obj.invoke()    # <- Contract evaluation.
```

...yielding the following (expected) exception:

```
.venvuser@localhost:~/home/user/test$ python3 test.py
Traceback (most recent call last):
  File "/home/user/test/test.py", line 12, in <module>
    obj.invoke()    # <- Contract evaluation.
    ^^^^^^^^^^^^
  File "/home/user/test/.venv/lib/python3.12/site-packages/icontract/_checkers.py", line 1133, in wrapper
    _assert_invariant(contract=contract, instance=instance)
  File "/home/user/test/.venv/lib/python3.12/site-packages/icontract/_checkers.py", line 530, in _assert_invariant
    raise _create_violation_error(
icontract.errors.ViolationError: File /home/user/test/test.py, line 4 in <module>:
self.bar >= 0:
self was Foo(bar=-10)
self.bar was -10
```

In comparison, the [Deal](https://pypi.org/project/deal/) package immediately evaluates the contract...
```python
from dataclasses import dataclass
import deal

@deal.inv(lambda self: self.bar >= 0)
@dataclass
class Foo():
    bar:int = 0
    def invoke(self): pass

obj = Foo(bar=0)
#print(f"obj type={type(obj)}")
obj.bar = -10   # <- Violates the contract, contract evaluation.
```

...yielding the following (expected) exception:
```
.venvuser@localhost:~/home/user/test$ python3 test.py
Traceback (most recent call last):
  File "/home/user/test/test.py", line 14, in <module>
    obj.bar = -10   # <- Violates the contract, contract evaluation.
    ^^^^^^^
deal.InvContractError: expected self.bar >= 0
```

Likewise, [Pydantic](https://pypi.org/project/pydantic/) can be persuaded (with the [validate_assignment](https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment) [configuration](https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict) option) to immediate type hint evaluation...
```python
from pydantic.dataclasses import dataclass
from pydantic import ConfigDict

@dataclass(config=dict(validate_assignment=True))
class Foo():
    bar:int = 0
    def invoke(self): pass

obj = Foo(bar=0)
obj.bar = 'bar' # <- Violates type hint, type evaluation.
```

...yielding the following (expected) exception:
```
.venvuser@localhost:~/test$ python3 test.py
Traceback (most recent call last):
  File "/home/user/test/test.py", line 12, in <module>
    obj.bar = 'bar' # <- Violates type hint, type evaluation.
    ^^^^^^^
  File "/home/user/test/.venv/lib/python3.12/site-packages/pydantic/_internal/_dataclasses.py", line 189, in validated_setattr
    validator.validate_assignment(instance, field, value)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Foo
bar
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='bar', input_type=str]
    For further information visit https://errors.pydantic.dev/2.7/v/int_parsing
```

Such added functionality would pursue me to use your library instead of Pydantic (which requires subclassing [BaseModel](https://docs.pydantic.dev/latest/api/base_model/) and either more convoluted [model](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_validate) or [Annotated](https://docs.pydantic.dev/latest/concepts/validators/) validation) or Deal (which performs, in my opinion, undesirable runtime subclassing):
```
obj type=<class 'deal._runtime._invariant.FooInvarianted'>
```

In the solution sphere (without knowing the internals of your code) perhaps [descriptor](https://docs.python.org/3/howto/descriptor.html#validator-class)s or [object-tracker](https://pypi.org/project/object-tracker/) could assist with such functionality?

Hints from issue comments:
@mristin: Hi @GithubCamouflaged ,
Is there any way you can make the properties be converted into setters/getters *before* you decorate with the invariants? That should do the trick.

Otherwise, I can introduce a flag to decorate `__get__` and `__set__` as well, but that will be called on *any* get and set operation of the class -- that's probably not what you want?@GithubCamouflaged: > Hi @GithubCamouflaged

Hi, and thank you for your swift reply!

> Is there any way you can make the properties be converted into setters/getters _before_ you decorate with the invariants? That should do the trick.

To me this is an undesirable solution, as this creates overhead (defining getter/setter/deleter for every property, and mapping these to private attributes) which is much less cleaner then normal/plain attributes, descriptors, or dataclass fields.

> Otherwise, I can introduce a flag to decorate `__get__` and `__set__` as well, but that will be called on _any_ get and set operation of the class -- that's probably not what you want?

Decorating these methods can lead to the desired effect indeed, as demonstrated here...
```python
import inspect
from dataclasses import dataclass, Field, fields
from typing import Any, Callable, Self
from types import MethodType, FunctionType

# Based on https://docs.python.org/3/howto/descriptor.html#validator-class
class Attribute():

    def __init__(self:Self, contracts:list[Callable] = []):
        self.contracts:list[Callable] = contracts

    def __set_name__(self:Self, owner:object, name:str):
        self.private_name:str = '_' + name
        
    def __get__(self:Self, obj:object, objtype=None) -> Any:
        value = getattr(obj, self.private_name)
        self.validate(value)
        return value

    def __set__(self:Self, obj:object, value:Any) -> None:
        self.validate(value)
        setattr(obj, self.private_name, value)

    def validate(self:Self, value:Any) -> None:
        for contract in self.contracts:
            if not contract(value):
                raise TypeError(f"Value '{value}' violated '{inspect.getsource(contract).strip()}'.")

@dataclass
class MyClass():
    first_attribute:str = Attribute(contracts=[
        (lambda value: value in range(0,2))
    ])
    second_attribute:str = 'default'

    @property
    def my_property(self) -> int:
        return 1

    def my_method(self) -> None:
        pass

obj = MyClass(first_attribute=1)
obj.first_attribute = 2 # <- Violates contract, contract evaluation.
```

...resulting in the following (expected) exception:
```
(.venv) user@localhost:~/test$ python3 test.py 
Traceback (most recent call last):
  File "/home/user/test/test.py", line 44, in <module>
    obj.first_attribute = 2 # <- Violates contract, contract evaluation.
    ^^^^^^^^^^^^^^^^^^^
  File "/home/user/test/test.py", line 21, in __set__
    self.validate(value)
  File "/home/user/test/test.py", line 27, in validate
    raise TypeError(f"Value '{value}' violated '{inspect.getsource(contract).strip()}'.")
TypeError: Value '2' violated '(lambda value: value in range(0,2))'.
```

But on **every** `__get__` and `__set__` seems undesirable as well indeed (although I lack proper knowledge of Python internals to be sure). Although these methods are available (either on the object/instance or the class) for descriptors, properties and fields, they are **also** available for methods (who would have thought...) as demonstrated (append to the same `test.py` file)...
```python
# Get (not really all...) object/instance attributes (yields Attribute.private_name for custom descriptor).
print("object/instance attributes:")
for name, value in vars(obj).items():
    print(f"\tAttribute name='{name}', value='{value}' (type='{type(value)}').")

# Python 'magic' creates object/instance facades for '__get__', '__set__' and '__delete__' for properties and descriptores:
print("object/instance descriptor attribute:")      # Note the lacking '_" prefix for the attribute.
attribute_value = getattr(obj, 'first_attribute')   # Can be accessed, although it was not listed above.
print(f"\tAttribute name='first_attribute', value='{attribute_value}' (type='{type(attribute_value)}').")

# Python 'magic' resolved these facades (through MRO?) to class attributes, which can be accessed there (limited to those defined explicitly above, for this demo):
print("class descriptor, property, method or other attributes:")
for name, value in [(name, value) for name, value in vars(MyClass).items() if name in ['first_attribute', 'second_attribute', 'my_property', 'my_method']]:
    match type(value):
        case type_ if type_ is property:
            print(f"\t'{name}' has type 'property' (has '__get__': {hasattr(value, '__get__')}).")            # True
        case type_ if type_ in [FunctionType, MethodType]:
            print(f"\t'{name}' has type 'function'/'method' (has '__get__': {hasattr(value, '__get__')}).")   # True
        case type_ if type_  is Attribute:
            print(f"\t'{name}' has type 'Attribute' (has '__get__': {hasattr(value, '__get__')}).")           # True
        case type_ if type_  is Field:  # If only life were this simple...
            print(f"\t'{name}' has type 'Field'.")
        case _:
            print(f"\t'{name}' has type '{type(value)}'.")

# And, dataclasses also provide access to object/instance fields (attributes):
print("'object/instance dataclass fields:")
for field in fields(obj):
    print(f"\tField name='{field.name}', type='{field.type}' (has '__get__': {hasattr(value, '__get__')}).")  # False
```

...resulting in the following output:
```
(.venv) user@localhost:~/test$ python3 test.py 
object/instance attributes:
        Attribute name='_first_attribute', value='1' (type='<class 'int'>').
        Attribute name='second_attribute', value='default' (type='<class 'str'>').
object/instance descriptor attribute:
        Attribute name='first_attribute', value='1' (type='<class 'int'>').
class descriptor, property, method or other attributes:
        'first_attribute' has type 'Attribute' (has '__get__': True).
        'second_attribute' has type '<class 'str'>'.
        'my_property' has type 'property' (has '__get__': True).
        'my_method' has type 'function'/'method' (has '__get__': True).
'object/instance dataclass fields:
        Field name='first_attribute', type='<class 'str'>' (has '__get__': True).
        Field name='second_attribute', type='<class 'str'>' (has '__get__': True).
```

Some filtering of the type of attribute would, therefor, be in order I think.

Would that be acceptable and/or maintainable for you, since the types of attributes may change over time?@mristin: Hi @GithubCamouflaged !
Thanks for keeping this discussion rolling -- this is definitely a feature that is relevant for icontract, but it will take us some time to figure it out.

Here's another snippet, just for future reference. It confirms what you wrote above:

```
import dataclasses


@dataclasses.dataclass
class Something:
    some_property: int


for attr_name in dir(Something):
    attr_value = getattr(Something, attr_name)
    print(f"Something.{attr_name}: {type(attr_value)}: {attr_value}")
```

The relevant part of the output (reformatted for readability):
```
Something.__annotations__: <class 'dict'>: {'some_property': <class 'int'>}

Something.__dataclass_fields__: <class 'dict'>: {
    'some_property': Field(
        name='some_property',type=<class 'int'>,
        default=<dataclasses._MISSING_TYPE object at 0x0000014D247B0F40>,
        default_factory=<dataclasses._MISSING_TYPE object at 0x0000014D247B0F40>,
        init=True,repr=True,
        hash=None,
        compare=True,
        metadata=mappingproxy({}),
        _field_type=_FIELD
    )
}

Something.__dataclass_params__: <class 'dataclasses._DataclassParams'>: 
    _DataclassParams(
        init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False
    )
```

Let me just document another snippet, again for future reference. It also matches what you wrote above:

```
import dataclasses

import icontract


@icontract.invariant(
    lambda self: self.some_property > 0,
    "icontract invariant: some_property must be positive"
)
@dataclasses.dataclass
class Something:
    some_property: int


try:
    _ = Something(some_property=-1)
except Exception as exception:
    print(exception)
```

We get:

```
icontract invariant: some_property must be positive: self.some_property > 0:
self was Something(some_property=-1)
self.some_property was -1
```

Now, we would like the invariant to propagate to the setters. This can indeed be done if we decorate `__setattr__`.

If we naively decorate the `__setattr__`, it will cause an attribute error as it will be called in the constructor, *before* any values could be set:
```
import dataclasses

import icontract


def decorate_with_check_some_property(cls):
    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(a_self, key, value):
        if key in a_self.__dataclass_fields__:
            for invariant in a_self.__invariants__:
                if not invariant.condition(a_self):
                    raise AssertionError("Invariant violated")

        old_setattr(a_self, key, value)

    setattr(cls, '__setattr__', new_setattr)

    return cls

@icontract.invariant(lambda self: self.some_property > 0)
@decorate_with_check_some_property
@dataclasses.dataclass
class Something:
    some_property: int


s = Something(some_property=3)
```

which gives:
```
Traceback (most recent call last):
  File "scratch_113.py", line 28, in <module>
    s = Something(some_property=3)
  File "venv38\lib\site-packages\icontract\_checkers.py", line 1026, in wrapper
    result = func(*args, **kwargs)
  File "<string>", line 3, in __init__
  File "scratch_113.py", line 12, in new_setattr
    if not invariant.condition(a_self):
  File "scratch_113.py", line 21, in <lambda>
    @icontract.invariant(lambda self: self.some_property > 0)
AttributeError: 'Something' object has no attribute 'some_property'
```

We have to therefore check whether we are in the `__init__` call by decorating `__init__`:

```
import dataclasses
import functools

import icontract


def decorate_with_check_some_property(cls):
    old_init = getattr(cls, "__init__")

    def new_init(self, *args, **kwargs):
        setattr(self, "__icontract_in_init__", True)
        old_init(self, *args, **kwargs)
        setattr(self, "__icontract_in_init__", False)

    functools.update_wrapper(wrapper=new_init, wrapped=old_init)
    setattr(cls, "__init__", new_init)

    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(self, key, value):
        old_setattr(self, key, value)

        in_init = getattr(self, "__icontract_in_init__", False)
        if (
                key != "__icontract_in_init__"
                and not in_init
                and key in self.__dataclass_fields__
        ):
            for invariant in self.__invariants__:
                check = invariant.condition(self)
                if not check:
                    raise AssertionError("Invariant violated")

    functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
    setattr(cls, '__setattr__', new_setattr)

    return cls

@icontract.invariant(lambda self: self.some_property > 0)
@decorate_with_check_some_property
@dataclasses.dataclass
class Something:
    some_property: int

# This works.
s = Something(some_property=3)

# This throws ViolationError:
invalid_s = Something(some_property=-1)

# This throws AssertionError specified above
s.some_property = -1
```

This can indeed be integrated in icontract.

Is the check `key in self.__dataclass_fields__` the only relevant check we want to make? What about pydantic classes?@mristin: I am also a bit unsure about how to deal with decorations -- you would have to specify for each invariant whether it should be applied to `__setattr__` or not (say, with a flag `decorate_setattr`). It would be easy to forget that.

Another possibility would be to introduce yet another decorator, `check_invariants_on_setters` which takes a callable `attr_name_filter`. Something like this:

```
import dataclasses
import functools
from typing import Callable, Any

import icontract
import icontract._checkers


class check_invariants_in_setters:
    def __init__(self, attr_name_filter: Callable[[Any, str], bool]) -> None:
        self.attr_name_filter = attr_name_filter

    def __call__(self, a_cls):
        old_init = getattr(a_cls, "__init__")

        def new_init(self, *args, **kwargs):
            setattr(self, "__icontract_in_init__", True)
            old_init(self, *args, **kwargs)
            setattr(self, "__icontract_in_init__", False)

        functools.update_wrapper(wrapper=new_init, wrapped=old_init)
        setattr(a_cls, "__init__", new_init)

        old_setattr = getattr(a_cls, "__setattr__")

        decorator = self

        # noinspection PyShadowingNames
        def new_setattr(self, key, value):
            old_setattr(self, key, value)

            in_init = getattr(self, "__icontract_in_init__", False)
            if (
                    key != "__icontract_in_init__"
                    and not in_init
                    and decorator.attr_name_filter(self, key)
            ):
                for invariant in self.__invariants__:
                    icontract._checkers._assert_invariant(invariant, self)

        functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
        setattr(a_cls, '__setattr__', new_setattr)

        return a_cls


@check_invariants_in_setters(
    attr_name_filter=lambda self, key: key in self.__dataclass_fields__
)
@icontract.invariant(lambda self: self.some_property > 0)
@dataclasses.dataclass
class Something:
    some_property: int


s = Something(some_property=3)

s.some_property = -1
```

This now gives the expected error:
```
icontract.errors.ViolationError: File scratch_113.py, line 50 in <module>:
self.some_property > 0:
self was Something(some_property=-1)
self.some_property was -1
```@mristin: @GithubCamouflaged please have a look at the proposal, and let me know what you think. Once we converge on an interface -- I'll create a pull request and integrate the feature in icontract.@GithubCamouflaged: @mristin very nice to see your enthusiasm for this feature, and the immediate prototyping and ensuing conversation is very much appreciated :)

First of all: very nice to see a working prototype!

Secondly (sorry for nitpicking), but be mindful of terminology here: an [attribute](https://docs.python.org/3/glossary.html#term-attribute) is something different than a [property](https://docs.python.org/3/library/functions.html#property) (which is basically a [descriptor](https://docs.python.org/3/howto/descriptor.html)), which matters when delving into the realm of (Python) implementation, as we'll see below.

The question(s) regarding your API depends. The first aspect is that of expectations/default behavior: I would argue that immediate contract evaluation is preferable, but that just happens to be *my* (biased) expectation, and I don't represent your entire user base ;) This would also bring feature parity with [Deal](https://pypi.org/project/deal/), although I don't know if that is your goal. Secondly, I would opt for the same API (`icontract.invariant`) with a flag (e.g.: [enum](https://docs.python.org/3/library/enum.html)) for the applied 'strategy'. Whichever is the default ties into the previous point, although I do understand your point about confusion regarding explicitness. If you *do* opt for another decorator I would steer away from terminology like `setters`, given this requires domain specific knowledge regarding the Python API which might confuse/confound users. Finally, there could be either performance or compatibility issues regarding immediate contract evaluation, which no doubt your are the better judge of then I.

Regarding the implementation: your prototype works smoothly for [dataclasses](https://docs.python.org/3/library/dataclasses.html) but is tailored for/limited to [Field](https://docs.python.org/3/library/dataclasses.html#dataclasses.Field)s. I think it would be nice to have support for attributes of 'regular'/plain [classes](https://docs.python.org/3/tutorial/classes.html) (also through [\_\_setattr\_\_](https://docs.python.org/3/reference/datamodel.html#object.__setattr__), or otherwise wrapping/'decorating' with a [descriptor](https://docs.python.org/3/howto/descriptor.html)) which does complicate the implementation a bit of course (that is, indeed, not to speak of supporting [Pydantic](https://pydantic.dev/) yet). A quick augmentation of your code *seems* to remedy this...

```python
import functools
import icontract

def decorate_with_check_some_property(cls):
    old_init = getattr(cls, "__init__")

    def new_init(self, *args, **kwargs):
        setattr(self, "__icontract_in_init__", True)
        old_init(self, *args, **kwargs)
        setattr(self, "__icontract_in_init__", False)

    functools.update_wrapper(wrapper=new_init, wrapped=old_init)
    setattr(cls, "__init__", new_init)

    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(self, key, value):
        old_setattr(self, key, value)

        in_init = getattr(self, "__icontract_in_init__", False)
        if (
                key != "__icontract_in_init__"
                and not in_init
                # Changed
                and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
        ):
            for invariant in self.__invariants__:
                check = invariant.condition(self)
                if not check:
                    raise AssertionError("Invariant violated")

    functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
    setattr(cls, '__setattr__', new_setattr)

    return cls

@icontract.invariant(lambda self: self.some_attribute > 0)
@decorate_with_check_some_property
class Something:
    some_attribute:int

    def __init__(self, some_attribute:int):
        self.some_attribute = some_attribute

# This works.
s = Something(some_attribute=3)

# This throws AssertionError specified above
s.some_attribute = -1
```

...resulting in the following:
```
(.venv) user@localhost:~/test$ python3 test.py 
Traceback (most recent call last):
  File "/home/user/test/test.py", line 49, in <module>
    s.some_attribute = -1
    ^^^^^^^^^^^^^^^^
  File "/home/user/test/test.py", line 30, in new_setattr
    raise AssertionError("Invariant violated")
AssertionError: Invariant violated
```

**However**, through some further testing it becomes apparent using either a (rather standard implementation of a) [property](https://docs.python.org/3/library/functions.html#property) or [descriptor](https://docs.python.org/3/howto/descriptor.html) (hence the terminology nitpicking earlier) throws a wrench in this implementation...
```python
import functools
import icontract

def decorate_with_check_some_property(cls):
    old_init = getattr(cls, "__init__")

    def new_init(self, *args, **kwargs):
        setattr(self, "__icontract_in_init__", True)
        old_init(self, *args, **kwargs)
        setattr(self, "__icontract_in_init__", False)

    functools.update_wrapper(wrapper=new_init, wrapped=old_init)
    setattr(cls, "__init__", new_init)

    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(self, key, value):
        old_setattr(self, key, value)

        in_init = getattr(self, "__icontract_in_init__", False)
        if (
                key != "__icontract_in_init__"
                and not in_init
                # Changed
                and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
        ):
            for invariant in self.__invariants__:
                check = invariant.condition(self)
                if not check:
                    raise AssertionError("Invariant violated")

    functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
    setattr(cls, '__setattr__', new_setattr)

    return cls

class SomeDescriptor():
    def __set_name__(self, owner:object, name:str):
        self.private_name:str = '_' + name
        
    def __get__(self, obj:object, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj:object, value) -> None:
        setattr(obj, self.private_name, value)

@icontract.invariant(lambda self: self.some_attribute > 0)
@decorate_with_check_some_property
class Something:
    some_attribute:int
    some_descriptor:int = SomeDescriptor()

    @property
    def some_property(self):
        return self._some_property

    @some_property.setter
    def some_property(self, value):
        self._some_property = value

    def some_method(self, arg:int) -> bool:
        return arg > 10

    def __init__(self, some_attribute:int):
        self.some_attribute = some_attribute

# This works.
s = Something(some_attribute=3)

# This throws AssertionError specified above
s.some_attribute = -1
```

...yielding the following (unwanted) exception:
```
(.venv) user@localhost:~/test$ python3 test.py 
Traceback (most recent call last):
  File "/home/user/test/test.py", line 47, in <module>
    @icontract.invariant(lambda self: self.some_attribute > 0)
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_decorators.py", line 478, in __call__
    icontract._checkers.add_invariant_checks(cls=cls)
  File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_checkers.py", line 1178, in add_invariant_checks
    value = getattr(cls, name)
            ^^^^^^^^^^^^^^^^^^
  File "/home/user/test/test.py", line 42, in __get__
    return getattr(obj, self.private_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute '_some_descriptor'
```

I **think** I've read somewhere this has something to do with how a [descriptor](https://docs.python.org/3/howto/descriptor.html) (and, thus, a [property](https://docs.python.org/3/library/functions.html#property)) is handled by Python, but I can't reproduce it at the moment, nor find the soure for it other then the [following statement](https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance) (which may, or may not, be only vaguely related): `Instead, it is the dot operator and the getattr function that are responsible for invoking __getattr__() whenever __getattribute__() raises an AttributeError`.@GithubCamouflaged: > ...yielding the following (unwanted) exception:
> 
> ```
> (.venv) user@localhost:~/test$ python3 test.py 
> Traceback (most recent call last):
>   File "/home/user/test/test.py", line 47, in <module>
>     @icontract.invariant(lambda self: self.some_attribute > 0)
>      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_decorators.py", line 478, in __call__
>     icontract._checkers.add_invariant_checks(cls=cls)
>   File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_checkers.py", line 1178, in add_invariant_checks
>     value = getattr(cls, name)
>             ^^^^^^^^^^^^^^^^^^
>   File "/home/user/test/test.py", line 42, in __get__
>     return getattr(obj, self.private_name)
>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> AttributeError: 'NoneType' object has no attribute '_some_descriptor'
> ```

It just occurred to me (welcome back brain) this is due to my own implementation fault (not providing a default value/assuming [\_\_set\_\_](https://docs.python.org/3/reference/datamodel.html#object.__set__) is always called before [\_\_get\_\_](https://docs.python.org/3/reference/datamodel.html#object.__get__), which I've fixed below.
```python
import functools
import icontract
import typing

def decorate_with_check_some_property(cls):
    old_init = getattr(cls, "__init__")

    def new_init(self, *args, **kwargs):
        setattr(self, "__icontract_in_init__", True)
        old_init(self, *args, **kwargs)
        setattr(self, "__icontract_in_init__", False)

    functools.update_wrapper(wrapper=new_init, wrapped=old_init)
    setattr(cls, "__init__", new_init)

    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(self, key, value):
        old_setattr(self, key, value)

        in_init = getattr(self, "__icontract_in_init__", False)
        if (
                key != "__icontract_in_init__"
                and not in_init
                # Changed
                and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
        ):
            for invariant in self.__invariants__:
                check = invariant.condition(self)
                if not check:
                    raise AssertionError("Invariant violated")

    functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
    setattr(cls, '__setattr__', new_setattr)

    return cls



T = typing.TypeVar('T')
class SomeDescriptor(typing.Generic[T]):
    def __set_name__(self, owner:object, name:str):
        self.private_name:str = '_' + name
        self.type_ = self.__orig_class__.__args__[0]

    def __get__(self, obj:object, objtype=None):
        return getattr(obj, self.private_name) if hasattr(self, self.private_name) else self.type_()

    #@icontract.require(lambda value: value >= 0)   # Works.
    def __set__(self, obj:object, value) -> None:
        setattr(obj, self.private_name, value)

@icontract.invariant(lambda self: self.some_attribute >= 0, "'some_attribute' must be bigger than 0.")
@icontract.invariant(lambda self: self.some_descriptor >= 0, "'some_descriptor' must be bigger than 0.") # Doesn't work.
@icontract.invariant(lambda self: self.some_property >= 0, "'some_property' must be bigger than 0.")
@decorate_with_check_some_property
class Something:
    some_attribute:int
    some_descriptor:int = SomeDescriptor[int]()

    @property
    def some_property(self):
        return self._some_property if hasattr(self, '_some_property') else 0

    @some_property.setter
    def some_property(self, value):
        self._some_property = value

    def some_method(self, arg:int) -> bool:
        return arg > 10

    def __init__(self, some_attribute:int):
        self.some_attribute = some_attribute

# This works.
s = Something(some_attribute=3)

#s.some_attribute = -1   # Works.
s.some_descriptor = -1  # Doesn't work.
#s.some_property = -1    # Works.
```

This now plays nice with [dataclasses](https://docs.python.org/3/library/dataclasses.html) and normal [classes](https://docs.python.org/3/tutorial/classes.html), decorates 'normal'/plain [attribute](https://docs.python.org/3/glossary.html#term-attribute)s and [properties](https://docs.python.org/3/library/functions.html#property), but still omits [descriptor](https://docs.python.org/3/howto/descriptor.html)s for some reason (although directly decorating its `__set__`'r works of course).@GithubCamouflaged: > This now plays nice with [dataclasses](https://docs.python.org/3/library/dataclasses.html) and normal [classes](https://docs.python.org/3/tutorial/classes.html), decorates 'normal'/plain [attribute](https://docs.python.org/3/glossary.html#term-attribute)s and [properties](https://docs.python.org/3/library/functions.html#property), but still omits [descriptor](https://docs.python.org/3/howto/descriptor.html)s for some reason (although directly decorating its `__set__`'r works of course).

Taking a step back and reading up some more on `descriptor`s has lessened my resolve regarding whether this is something you should want, so I am curious about your viewpoint on this.

My original focus on this stems from my own attempts at crafting validation for arguments and attributes, until I encountered libraries for this, such as [icontract](https://pypi.org/project/icontract/) and [Deal](https://pypi.org/project/deal/) (and to a lesser extend, [Pydantic](https://pypi.org/project/pydantic/)). However, fact is this concerns a *pattern*, and not a *type*, which is broadly applied within Python itself, even extending to [functions and methods](https://docs.python.org/3/howto/descriptor.html#functions-and-methods). If it were a *type*, applicability of runtime decoration with contracts of e.g. [\_\_set\_\_](https://docs.python.org/3/reference/datamodel.html#object.__set__) could be checked against e.g. [property](https://docs.python.org/3/library/functions.html#property) or [MethodType](https://docs.python.org/3/library/types.html#types.MethodType), but it is not. However, from merely the implementation of the (non-)data-protocol we cannot derive what *it* is, and whether decoration is applicable. The flip-side of that argument is it is the user's responsibility on where to apply the contracts: if you can `__set__` it, it should be contract-able.

Where do you stand on this?@GithubCamouflaged: One final through for now: we have been focusing on [\_\_set\_\_](https://docs.python.org/3/reference/datamodel.html#object.__set__), but should [\_\_get\_\_](https://docs.python.org/3/reference/datamodel.html#object.__get__) not also be contracted? Otherwise we could sneakily bypass the contract...
```python
import icontract
import dataclasses

@icontract.invariant(lambda self: self.some_property >= 0, "'some_property' must be bigger than 0.")
@dataclasses.dataclass
class Something:
    @property
    def some_property(self):
        return self._some_property if hasattr(self, '_some_property') else 0

    @some_property.setter
    def some_property(self, value):
        self._some_property = value

    def some_method(self, arg:int) -> bool:
        return arg > 10

    def __init__(self, some_property:int):
        self.some_property = some_property

s = Something(some_property=0)
s._some_property = -1   # Sneaky contract violation.
print(f"s._some_property: {s._some_property}")
```

...resulting in the following unwanted behavior:
```
(.venv) user@localhost:~/test$ python3 test.py 
s._some_property: -1
```@mristin: Hi @GithubCamouflaged ,
Thanks, great ideas!

I tend towards having two decorators, one for setters and one for getters, as usually enforcing invariants on getters can slow the program down immensely. In most use cases, I envision invariants on setters to be enough, but icontract should allow both setters and getters to be covered.

> Taking a step back and reading up some more on descriptors has lessened my resolve regarding whether this is something you should want, so I am curious about your viewpoint on this.
>
> My original focus on this stems from my own attempts at crafting validation for arguments and attributes, until I encountered libraries for this, such as [icontract](https://pypi.org/project/icontract/) and [Deal](https://pypi.org/project/deal/) (and to a lesser extend, [Pydantic](https://pypi.org/project/pydantic/)). However, fact is this concerns a pattern, and not a type, which is broadly applied within Python itself, even extending to [functions and methods](https://docs.python.org/3/howto/descriptor.html#functions-and-methods). If it were a type, applicability of runtime decoration with contracts of e.g. [__set__](https://docs.python.org/3/reference/datamodel.html#object.__set__) could be checked against e.g. [property](https://docs.python.org/3/library/functions.html#property) or [MethodType](https://docs.python.org/3/library/types.html#types.MethodType), but it is not. However, from merely the implementation of the (non-)data-protocol we cannot derive what it is, and whether decoration is applicable. The flip-side of that argument is it is the user's responsibility on where to apply the contracts: if you can __set__ it, it should be contract-able.
>
> Where do you stand on this?

As a user, I would have expected that the descriptors worked just as well -- I was actually surprised that this if-statement:
```
    if (
                key != "__icontract_in_init__"
                and not in_init
                # Changed
                and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
        ):
```
in your implementation did not fire on an attribute given a descriptor value.

Maybe we should simply reduce this `if` to:
```
if key != "__icontract_in_init__" and not in_init:
```
? Whenever the user fiddles with the attributes on an object, the invariants should be verified. Even if you set a new attribute, there might be an invariant saying that no new attributes should be set, so `hasattr(self, key)` check is actually wrong in this context.

I'd assume the same applies to decorating `__getattr__`. Of course, using the *getter* decorator to check invariants on every `__getattr__` will slow everything down, but that's for people who extremely prefer correctness to speed, which might be sometimes warranted. For example, in cases where you have very difficult errors to debug.

What do you think?@GithubCamouflaged: > As a user, I would have expected that the descriptors worked just as well -- I was actually surprised that this if-statement:
> 
> ```
>     if (
>                 key != "__icontract_in_init__"
>                 and not in_init
>                 # Changed
>                 and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
>         ):
> ```
> 
> in your implementation did not fire on an attribute given a descriptor value.

Well, it turns out that condition *is* actually met as expected. After some extensive debugging (totally in the wrong direction) I've finally found the culprit, which was (yet again) a bug in my descriptor implementation (annotated below)...
```python
import functools
import icontract
import typing
import dataclasses

def decorate_with_check_some_property(cls):
    old_init = getattr(cls, "__init__")

    def new_init(self, *args, **kwargs):
        setattr(self, "__icontract_in_init__", True)
        old_init(self, *args, **kwargs)
        setattr(self, "__icontract_in_init__", False)

    functools.update_wrapper(wrapper=new_init, wrapped=old_init)
    setattr(cls, "__init__", new_init)

    old_setattr = getattr(cls, "__setattr__")

    def new_setattr(self, key, value):
        old_setattr(self, key, value)

        in_init = getattr(self, "__icontract_in_init__", False)
        if (
                key != "__icontract_in_init__"
                and not in_init
                # Changed
                and (key in self.__dataclass_fields__ if hasattr(self, '__dataclass_fields__') else hasattr(self, key))
        ):
            for invariant in self.__invariants__:
                check = invariant.condition(self)
                if not check:
                    raise AssertionError("Invariant violated")

    functools.update_wrapper(wrapper=new_setattr, wrapped=old_setattr)
    setattr(cls, '__setattr__', new_setattr)

    return cls

T = typing.TypeVar('T')
class SomeDescriptor(typing.Generic[T]):
    def __set_name__(self, owner:object, name:str):
        self.private_name:str = '_' + name
        self.type_ = self.__orig_class__.__args__[0]

    def __get__(self, obj:object, objtype=None):
        # Bugfix: don't always return default value by checking 'hasattr' against 'obj' (the descriptee) instead of 'self' (the desciptor).
        return getattr(obj, self.private_name) if hasattr(obj, self.private_name) else self.type_()

    def __set__(self, obj:object, value) -> None:
        setattr(obj, self.private_name, value)

@icontract.invariant(lambda self: self.some_attribute >= 0, "'some_attribute' must be bigger than 0.")
@icontract.invariant(lambda self: self.some_descriptor >= 0, "'some_descriptor' must be bigger than 0.")
@icontract.invariant(lambda self: self.some_property >= 0, "'some_property' must be bigger than 0.")
@decorate_with_check_some_property
#@dataclasses.dataclass
class Something:
    some_attribute:int
    some_descriptor:int = SomeDescriptor[int]()

    @property
    def some_property(self):
        return self._some_property if hasattr(self, '_some_property') else 0

    @some_property.setter
    def some_property(self, value):
        self._some_property = value

    def __init__(self, some_attribute:int=0, some_descriptor:int=0, some_property:int=0):
        self.some_attribute = some_attribute
        self.some_descriptor = some_descriptor
        self.some_property = some_property

s = Something(some_attribute=1, some_descriptor=2, some_property=3)

#s.some_attribute = -1   # Works.
s.some_descriptor = -1  # Works.
#s.some_property = -1    # Works.
```

...which finally gives us the (expected) exception for both 'normal' and dataclasses:
```
(.venv) user@localhost:~/test$ python3 test.py 
Traceback (most recent call last):
  File "/home/user/test/test.py", line 77, in <module>
    s.some_descriptor = -1  # Works.
    ^^^^^^^^^^^^^^^^^
  File "/home/user/test/test.py", line 20, in new_setattr
    old_setattr(self, key, value)
  File "/home/user/test/test.py", line 50, in __set__
    setattr(obj, self.private_name, value)
  File "/home/user/test/test.py", line 30, in new_setattr
    check = invariant.condition(self)
            ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/test/test.py", line 54, in <lambda>
    @icontract.invariant(lambda self: self.some_property >= 0, "'some_property' must be bigger than 0.")
                                      ^^^^^^^^^^^^^^^^^^
  File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_checkers.py", line 1133, in wrapper
    _assert_invariant(contract=contract, instance=instance)
  File "/home/user/test/.venv/lib/python3.11/site-packages/icontract/_checkers.py", line 530, in _assert_invariant
    raise _create_violation_error(
icontract.errors.ViolationError: File /home/myuser/test/test.py, line 53 in <module>:
'some_descriptor' must be bigger than 0.: self.some_descriptor >= 0:
self was <__main__.Something object at 0x7f1486511c10>
self.some_descriptor was -1
```

> Maybe we should simply reduce this `if` to:
> 
> ```
> if key != "__icontract_in_init__" and not in_init:
> ```
> 
> ? Whenever the user fiddles with the attributes on an object, the invariants should be verified. Even if you set a new attribute, there might be an invariant saying that no new attributes should be set, so `hasattr(self, key)` check is actually wrong in this context.

Very nice idea: that would actually give you contracts that can mimic [frozen](https://docs.python.org/3/library/dataclasses.html#frozen-instances)-like conditions!

> I'd assume the same applies to decorating `__getattr__`. Of course, using the _getter_ decorator to check invariants on every `__getattr__` will slow everything down, but that's for people who extremely prefer correctness to speed, which might be sometimes warranted. For example, in cases where you have very difficult errors to debug.

Invariant `__getattr__`'s are probably much more expensive, so it indeed makes sense to let the user decide how stringent they prefer their adherence. 

I think we're getting there!@mristin: @GithubCamouflaged great! Let's fix the names then. What about `check_invariants_on_getattr` and `check_invariants_on_setattr`?

Both decorators should accept a parameter `attr_filter` which is a callable on the instance and the key, *i.e.*, attribute name. The default is `None`, but I can imagine that this filter might be relevant for performance.@GithubCamouflaged: Hi,

I hope you won't begrudge me advocating my personal preference one last time (I promise) with the following simplified example...
```python
from typing import Self
from enum import Flag, auto

class Strategy(Flag):
    METHOD_CALL = auto()
    ATTRIBUTE_SET = auto()
    ATTRIBUTE_GET = auto()

class Invariant():
    def __init__(self:Self, strategy:Strategy = Strategy.METHOD_CALL | Strategy.ATTRIBUTE_SET) -> None:
        if Strategy.METHOD_CALL in strategy:
            print("Checking condition on method calls.")

        if Strategy.ATTRIBUTE_SET | Strategy.ATTRIBUTE_GET in strategy:
            print("Checking condition on attribute setters and getters.")
        elif Strategy.ATTRIBUTE_SET in strategy:
            print("Checking condition on attribute setters.")
        elif Strategy.ATTRIBUTE_GET in strategy:
            print("Checking condition on attribute getters.")

contract = Invariant(strategy = Strategy.METHOD_CALL | Strategy.ATTRIBUTE_SET | Strategy.ATTRIBUTE_GET)
```

...resulting in the following output:
```
(.venv) user@localhost:~/test$ python3 test.py 
Checking condition on method calls.
Checking condition on attribute setters and getters.
```

I think this has the following benefits:
* (essentially) Keeps your API the same/clean.
* Allows for gradual changes of default behavior (you could initially set `strategy` to `Strategy.METHOD_CALL`).
* Draw users attention to new functionality (will also show up in auto-complete).

That being said, I think new `check_invariants_on_getattr` and `check_invariants_on_setattr` decorators would convey their functionality clearly through those names. Two questions though:
* Would users have to use both decorators, thus duplicating contracts, when they want to check both [\_\_set\_\_](https://docs.python.org/3/reference/datamodel.html#object.__set__) and [\_\_get\_\_](https://docs.python.org/3/reference/datamodel.html#object.__get__)?
* What is the intended functionality of a `key`/`attribute_name` argument? In the current API that seems compounded in the [lambda expression](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions) argument? Or perhaps I am misunderstanding.@mristin: Indeed thanks for the suggestions @GithubCamouflaged ! I like the approach with the flags. Actually, I can imagine that some invariants need to be checked for all the properties, while others are only meaningful in operations.

I also double-checked, `enum.Flag` is available since Python 3.6 so there should be no problems due to compatibility.

What do you think about renaming the `strategy` argument in your proposal to `check_on`? Consequently, rename `Strategy` to `InvariantCheckEvent`?

I tend to use the conventional names for the flag: `CALL`, `SETATTR`, `GETATTR` which correspond to dunders which will be actually decorated (`__call__`, `__setattr__`, and `__getattr__`, respectively). 

I would also add `ALWAYS` getter in the flag class so that users can avoid typing out all the flags together, akin to: https://stackoverflow.com/a/42253518/1600678. I'm also not sure whether `ALWAYS` is a good name -- would you rather call this flag `ALL` ("all" referring to all events in the flag, where "always" might also mean in all *possible* operations over the object)?@GithubCamouflaged: > Indeed thanks for the suggestions @GithubCamouflaged ! I like the approach with the flags. Actually, I can imagine that some invariants need to be checked for all the properties, while others are only meaningful in operations.

:)

> What do you think about renaming the `strategy` argument in your proposal to `check_on`? Consequently, rename `Strategy` to `InvariantCheckEvent`?

Yeah, I think that makes sense: it conveys the mechanism behind the contract more clearly (an event being triggered on certain conditions).

> I tend to use the conventional names for the flag: `CALL`, `SETATTR`, `GETATTR` which correspond to dunders which will be actually decorated (`__call__`, `__setattr__`, and `__getattr__`, respectively).

Agreed, as that aligns more closely with existing Python terminology. Although I suspects that resonates better with advances users, whom I'd argue are the intended audience of this configurability anyway.

> I would also add `ALWAYS` getter in the flag class so that users can avoid typing out all the flags together, akin to: https://stackoverflow.com/a/42253518/1600678. I'm also not sure whether `ALWAYS` is a good name -- would you rather call this flag `ALL` ("all" referring to all events in the flag, where "always" might also mean in all _possible_ operations over the object)?

I like the shorthand idea! `ALL` has my preference over `ALWAYS`, as that indeed maps in my head to '*in all events, do this*' (which may just be how my particular brain works). Do you have a preference?

Implementation wise it turns out adding an `ALL` flag is not so complex  as I thought (you can opt either for [Flag](https://docs.python.org/3/library/enum.html#enum.Flag) or [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) here)...
```python
from typing import Self
from enum import IntFlag, auto

class InvariantCheckEvent(IntFlag):
    CALL = auto()
    SETATTR = auto()
    GETATTR = auto()
    ALL = CALL | SETATTR | GETATTR

class Invariant():
    def __init__(self:Self, check_on:InvariantCheckEvent = InvariantCheckEvent.CALL | InvariantCheckEvent.SETATTR) -> None:
        print(InvariantCheckEvent.CALL in check_on)
        print(InvariantCheckEvent.SETATTR in check_on)
        print(InvariantCheckEvent.GETATTR in check_on)
        print("")

contract1 = Invariant()
contract2 = Invariant(check_on=InvariantCheckEvent.ALL)
print([flag for flag in InvariantCheckEvent if flag in InvariantCheckEvent.ALL])
```

...resulting in the following output:
```
(.venv) user@localhost:~/test$ python test.py 
True
True
False

True
True
True

[<InvariantCheckEvent.CALL: 1>, <InvariantCheckEvent.SETATTR: 2>, <InvariantCheckEvent.GETATTR: 4>]
```

One could argue this does not scale well, but I recon the amount of members/attributes you'd want to capture in `ALL` won't really grow that much over time. The alternatives (like a [classproperty](https://stackoverflow.com/a/42252767), which is [deprecated](https://docs.python.org/3.11/whatsnew/3.11.html#language-builtins), or [employing](https://stackoverflow.com/a/59264474) features like [Enum.\_missing\_](https://docs.python.org/3/library/enum.html#enum.Enum._missing_) or the deprecated [Enum.\_create\_pseudo\_member_](https://bugs.python.org/issue40006) all feel less transparent and more convoluted to me.

One final pet peeve of mine is you can't seem to use either [Flag](https://docs.python.org/3/library/enum.html#enum.Flag) or [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) with a [match](https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching) statement like you can [Enum](https://docs.python.org/3/library/enum.html#enum.Enum). imo that would make the resulting code more readable, but that is just a minor detail (and, just the way it is anyway).@mristin: Hi @GithubCamouflaged ,

> ALL has my preference over ALWAYS [...]

I like it more as well. Ok, let's go with `ALL` then.

> Implementation wise it turns out adding an ALL flag is not so complex as I thought (you can opt either for [Flag](https://docs.python.org/3/library/enum.html#enum.Flag) or [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) here)... 
> [...] 
> One could argue this does not scale well, but I recon the amount of members/attributes you'd want to capture in ALL won't really grow that much over time.

I think this is totally manageable. I anticipate at most 2-3 more flags, but I can't even phantom at the moment what these extra flags would be.

> One final pet peeve of mine is you can't seem to use either [Flag](https://docs.python.org/3/library/enum.html#enum.Flag) or [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) with a [match](https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching) statement like you can [Enum](https://docs.python.org/3/library/enum.html#enum.Enum). imo that would make the resulting code more readable, but that is just a minor detail (and, just the way it is anyway).

The `match` feature is only available in Python 3.10, but icontract tries to maintain the compatibility with as early as Python 3.6.@mristin: @GithubCamouflaged I think we have all the details fleshed out. I'll try to implement the feature in the next few days.@GithubCamouflaged: Much appreciated!@mristin: @GithubCamouflaged I'm overwhelmed a bit with my todo backlog at the moment. Please apologize for the delay in advance -- I'll try to work on this feature in the next two-three weeks, but can't promise anything. It is the start of the summer, and there are tons of family obligations in my way.@GithubCamouflaged: @mristin totally understand, and still stoked a release of this feature is so close on the horizon! :)@mristin: (Pinging here, @GithubCamouflaged, in case you missed the PR review request: https://github.com/Parquery/icontract/pull/292)

Solution patch:
diff --git a/docs/source/api.rst b/docs/source/api.rst
index ed55e79..06e81de 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -45,3 +45,7 @@ DBC
 ViolationError
 --------------
 .. autoclass:: ViolationError
+
+InvariantCheckEvent
+-------------------
+.. autoclass:: InvariantCheckEvent
\ No newline at end of file
diff --git a/docs/source/usage.rst b/docs/source/usage.rst
index d51355b..6d187ec 100644
--- a/docs/source/usage.rst
+++ b/docs/source/usage.rst
@@ -113,7 +113,7 @@ Invariants
 ----------
 Invariants are special contracts associated with an instance of a class. An invariant should hold *after* initialization
 and *before* and *after* a call to any public instance method. The invariants are the pivotal element of
-design-by-contract: they allow you to formally define properties of a data structures that you know will be maintained
+design-by-contract: they allow you to formally define properties of data structures that you know will be maintained
 throughout the life time of *every* instance.
 
 We consider the following methods to be "public":
@@ -121,15 +121,37 @@ We consider the following methods to be "public":
 * All methods not prefixed with ``_``
 * All magic methods (prefix ``__`` and suffix ``__``)
 
-Class methods (marked with ``@classmethod`` or special dunders such as ``__new__``) can not observe the invariant
-since they are not associated with an instance of the class.
+Class methods (marked with ``@classmethod`` or special dunders such as ``__new__``) can not observe the invariants
+since they are not associated with an instance of the class. We also exempt ``__getattribute__`` method from observing
+the invariants since these functions alter the state of the instance and thus can not be considered "public".
+We exempt ``__repr__`` method as well to prevent endless loops when generating error messages.
+At runtime, many icontract-specific dunder attributes (such as ``__invariants__``) need to be accessed, so the method
+``__getattribute__`` can not be decorated lest we end up in an endless recursion.
 
-We exempt ``__getattribute__``, ``__setattr__`` and ``__delattr__`` methods from observing the invariant since
-these functions alter the state of the instance and thus can not be considered "public".
+By default, we do not enforce the invariants on calls to ``__setattr__`` as that is usually
+prohibitively expensive in terms of computation for most use cases. However, there is a parameter
+``check_on`` to an :class:`invariant` which allows you to steer in a more fine-grained manner when the invariant should
+be enforced.
 
-We also exempt ``__repr__`` method to prevent endless loops when generating error messages.
+.. note::
 
-The icontract invariants are implemented as class decorators.
+    Be careful with instance attributes referencing other instances or collections. For example, ``a.some_list.append(3)``
+    will not trigger the check of invariants as the attribute ``a.some_list``, kept as a reference, remains unchanged.
+    That is, even though the referenced object changes (the actual list), the reference does not.
+
+The default value of ``check_on`` is set to :attr:`InvariantCheckEvent.CALL`, meaning that we check
+the invariants only in the calls to the methods *excluding* ``__setattr__``. If you want to check
+the invariants *only* on ``__setattr__`` and excluding *any* other method, set it to :attr:`InvariantCheckEvent.SETATTR`.
+The combinations is also possible; to check invariants on method calls *including* ``__setattr__``, set ``check_on`` to
+:attr:`InvariantCheckEvent.CALL` ``|`` :attr:`InvariantCheckEvent.SETATTR`.
+
+To save you some typing, we introduced the shortcut, :attr:`InvariantCheckEvent.ALL`, which stands for the combination
+:attr:`InvariantCheckEvent.CALL` ``|`` :attr:`InvariantCheckEvent.SETATTR`.
+
+.. note::
+
+	The property getters and setters are considered "normal" methods. If you want to check the invariants at property
+	getters and/or setters, make sure to include :attr:`InvariantCheckEvent.CALL` in ``check_on``.
 
 The following examples show various cases when an invariant is breached.
 
@@ -229,6 +251,45 @@ After the invocation of a magic method:
     self was an instance of SomeClass
     self.x was -1
 
+Enforcing the invariants on the method calls *including* ``__setattr__``:
+
+.. code-block:: python
+
+    >>> @icontract.invariant(
+    ...     lambda self: self.x > 0,
+    ...        check_on=(
+    ...            icontract.InvariantCheckEvent.CALL
+    ...            | icontract.InvariantCheckEvent.SETATTR
+    ...			)
+    ... )
+    ... class SomeClass:
+    ...     def __init__(self) -> None:
+    ...         self.x = 100
+    ...
+    ...     def do_something_bad(self) -> None:
+    ...         self.x = -1
+    ...
+    ...     def __repr__(self) -> str:
+    ...         return "an instance of SomeClass"
+    ...
+    >>> some_instance = SomeClass()
+    >>> some_instance.do_something_bad()
+    Traceback (most recent call last):
+     ...
+    icontract.errors.ViolationError: File <doctest usage.rst[26]>, line 1 in <module>:
+    self.x > 0:
+    self was an instance of SomeClass
+    self.x was -1
+
+    >>> another_instance = SomeClass()
+    >>> another_instance.x = -1
+    Traceback (most recent call last):
+     ...
+    icontract.errors.ViolationError: File <doctest usage.rst[26]>, line 1 in <module>:
+    self.x > 0:
+    self was an instance of SomeClass
+    self.x was -1
+
 Snapshots (a.k.a "old" argument values)
 ---------------------------------------
 Usual postconditions can not verify the state transitions of the function's argument values. For example, it is
@@ -261,7 +322,7 @@ Here is an example that uses snapshots to check that a value was appended to the
     >>> some_func(lst=[1, 2], value=3)
     Traceback (most recent call last):
         ...
-    icontract.errors.ViolationError: File <doctest usage.rst[28]>, line 2 in <module>:
+    icontract.errors.ViolationError: File <doctest usage.rst[33]>, line 2 in <module>:
     lst == OLD.lst + [value]:
     OLD was a bunch of OLD values
     OLD.lst was [1, 2]
@@ -285,7 +346,7 @@ The following example shows how you can name the snapshot:
     >>> some_func(lst=[1, 2], value=3)
     Traceback (most recent call last):
         ...
-    icontract.errors.ViolationError: File <doctest usage.rst[32]>, line 2 in <module>:
+    icontract.errors.ViolationError: File <doctest usage.rst[37]>, line 2 in <module>:
     len(lst) == OLD.len_lst + 1:
     OLD was a bunch of OLD values
     OLD.len_lst was 2
@@ -311,7 +372,7 @@ The next code snippet shows how you can combine multiple arguments of a function
     >>> some_func(lst_a=[1, 2], lst_b=[3, 4])  # doctest: +ELLIPSIS
     Traceback (most recent call last):
         ...
-    icontract.errors.ViolationError: File <doctest usage.rst[36]>, line ... in <module>:
+    icontract.errors.ViolationError: File <doctest usage.rst[...]>, line ... in <module>:
     set(lst_a).union(lst_b) == OLD.union:
     OLD was a bunch of OLD values
     OLD.union was {1, 2, 3, 4}
@@ -394,7 +455,7 @@ The following example shows an abstract parent class and a child class that inhe
         >>> some_b.func(y=0)
         Traceback (most recent call last):
             ...
-        icontract.errors.ViolationError: File <doctest usage.rst[40]>, line 7 in A:
+        icontract.errors.ViolationError: File <doctest usage.rst[45]>, line 7 in A:
         result < y:
         result was 1
         self was an instance of B
@@ -405,7 +466,7 @@ The following example shows an abstract parent class and a child class that inhe
         >>> another_b.break_parent_invariant()
         Traceback (most recent call last):
             ...
-        icontract.errors.ViolationError: File <doctest usage.rst[40]>, line 1 in <module>:
+        icontract.errors.ViolationError: File <doctest usage.rst[45]>, line 1 in <module>:
         self.x > 0:
         self was an instance of B
         self.x was -1
@@ -415,7 +476,7 @@ The following example shows an abstract parent class and a child class that inhe
         >>> yet_another_b.break_my_invariant()
         Traceback (most recent call last):
             ...
-        icontract.errors.ViolationError: File <doctest usage.rst[41]>, line 1 in <module>:
+        icontract.errors.ViolationError: File <doctest usage.rst[46]>, line 1 in <module>:
         self.x < 100:
         self was an instance of B
         self.x was 101
@@ -453,7 +514,7 @@ The following example shows how preconditions are weakened:
         >>> b.func(x=5)
         Traceback (most recent call last):
             ...
-        icontract.errors.ViolationError: File <doctest usage.rst[49]>, line 2 in B:
+        icontract.errors.ViolationError: File <doctest usage.rst[54]>, line 2 in B:
         x % 3 == 0:
         self was an instance of B
         x was 5
@@ -484,7 +545,7 @@ The example below illustrates how snapshots are inherited:
         >>> b.func(lst=[1, 2], value=3)
         Traceback (most recent call last):
             ...
-        icontract.errors.ViolationError: File <doctest usage.rst[54]>, line 4 in A:
+        icontract.errors.ViolationError: File <doctest usage.rst[59]>, line 4 in A:
         len(lst) == len(OLD.lst) + 1:
         OLD was a bunch of OLD values
         OLD.lst was [1, 2]
@@ -495,7 +556,6 @@ The example below illustrates how snapshots are inherited:
         self was an instance of B
         value was 3
 
-
 Toggling Contracts
 ------------------
 By default, the contract checks (including the snapshots) are always performed at run-time. To disable them, run the
@@ -607,7 +667,7 @@ Here is an example of the error given as a subclass of `BaseException`_:
     >>> some_func(x=0)
     Traceback (most recent call last):
         ...
-    ValueError: File <doctest usage.rst[62]>, line 1 in <module>:
+    ValueError: File <doctest usage.rst[67]>, line 1 in <module>:
     x > 0: x was 0
 
 Here is an example of the error given as an instance of a `BaseException`_:
diff --git a/icontract/__init__.py b/icontract/__init__.py
index 4dfd61f..3919416 100644
--- a/icontract/__init__.py
+++ b/icontract/__init__.py
@@ -48,3 +48,5 @@
 import icontract.errors
 
 ViolationError = icontract.errors.ViolationError
+
+InvariantCheckEvent = icontract._types.InvariantCheckEvent
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index 513d52c..efb445f 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -18,9 +18,10 @@
 
 import icontract._represent
 from icontract._globals import CallableT, ClassT
-from icontract._types import Contract, Snapshot
+from icontract._types import Contract, Snapshot, InvariantCheckEvent
 from icontract.errors import ViolationError
 
+
 # pylint does not play with typing.Mapping.
 # pylint: disable=unsubscriptable-object
 # pylint: disable=raising-bad-type
@@ -965,8 +966,8 @@ def wrapper(*args, **kwargs):  # type: ignore
         """Pass the arguments to __new__ and check invariants on the result."""
         instance = new_func(*args, **kwargs)
 
-        for contract in instance.__class__.__invariants__:
-            _assert_invariant(contract=contract, instance=instance)
+        for invariant in instance.__class__.__invariants__:
+            _assert_invariant(contract=invariant, instance=instance)
 
         return instance
 
@@ -979,7 +980,7 @@ def wrapper(*args, **kwargs):  # type: ignore
 
 def _decorate_with_invariants(func: CallableT, is_init: bool) -> CallableT:
     """
-    Decorate the function ``func`` of the class ``cls`` with invariant checks.
+    Decorate the method ``func`` with invariant checks.
 
     If the function has been already decorated with invariant checks, the function returns immediately.
 
@@ -1025,8 +1026,8 @@ def wrapper(*args, **kwargs):  # type: ignore
             try:
                 result = func(*args, **kwargs)
 
-                for contract in instance.__class__.__invariants__:
-                    _assert_invariant(contract=contract, instance=instance)
+                for invariant in instance.__class__.__invariants__:
+                    _assert_invariant(contract=invariant, instance=instance)
 
                 return result
             finally:
@@ -1060,6 +1061,12 @@ async def wrapper(*args, **kwargs):  # type: ignore
                         ).format(func, param_names, args, kwargs)
                     ) from err
 
+                invariants = (
+                    instance.__class__.__invariants_on_setattr__
+                    if func.__name__ == "__setattr__"
+                    else instance.__class__.__invariants_on_call__
+                )
+
                 # We need to create a new in-progress set if it is None as the ``ContextVar`` does not accept
                 # a factory function for the default argument. If we didn't do this, and simply set an empty
                 # set as the default, ``ContextVar`` would always point to the same set by copying the default
@@ -1080,13 +1087,13 @@ async def wrapper(*args, **kwargs):  # type: ignore
 
                 # ExitStack is not used here due to performance.
                 try:
-                    for contract in instance.__class__.__invariants__:
-                        _assert_invariant(contract=contract, instance=instance)
+                    for invariant in invariants:
+                        _assert_invariant(contract=invariant, instance=instance)
 
                     result = await func(*args, **kwargs)
 
-                    for contract in instance.__class__.__invariants__:
-                        _assert_invariant(contract=contract, instance=instance)
+                    for invariant in invariants:
+                        _assert_invariant(contract=invariant, instance=instance)
 
                     return result
                 finally:
@@ -1108,6 +1115,12 @@ def wrapper(*args, **kwargs):  # type: ignore
                         ).format(func, param_names, args, kwargs)
                     ) from err
 
+                invariants = (
+                    instance.__class__.__invariants_on_setattr__
+                    if func.__name__ == "__setattr__"
+                    else instance.__class__.__invariants_on_call__
+                )
+
                 # The following dunder indicates whether another invariant is currently being checked. If so,
                 # we need to suspend any further invariant check to avoid endless recursion.
 
@@ -1129,13 +1142,13 @@ def wrapper(*args, **kwargs):  # type: ignore
 
                 # ExitStack is not used here due to performance.
                 try:
-                    for contract in instance.__class__.__invariants__:
-                        _assert_invariant(contract=contract, instance=instance)
+                    for invariant in invariants:
+                        _assert_invariant(contract=invariant, instance=instance)
 
                     result = func(*args, **kwargs)
 
-                    for contract in instance.__class__.__invariants__:
-                        _assert_invariant(contract=contract, instance=instance)
+                    for invariant in invariants:
+                        _assert_invariant(contract=invariant, instance=instance)
 
                     return result
                 finally:
@@ -1169,25 +1182,38 @@ def _already_decorated_with_invariants(func: CallableT) -> bool:
 def add_invariant_checks(cls: ClassT) -> None:
     """Decorate each of the class functions with invariant checks if not already decorated."""
     # Candidates for the decoration as list of (name, dir() value)
-    init_name_func = None  # type: Optional[Tuple[str, Callable[..., None]]]
+    init_func = None  # type: Optional[Callable[..., None]]
     names_funcs = []  # type: List[Tuple[str, Callable[..., None]]]
     names_properties = []  # type: List[Tuple[str, property]]
 
-    # Filter out entries in the directory which are certainly not candidates for decoration.
+    # As we continuously decorate the class with invariants, we never definitely know
+    # whether this decoration is the last one. Hence, we can only retrieve the list
+    # of invariants decorated *thus far*. As we only add one invariant at the time,
+    # we only need to check for the last invariant.
+    assert cls.__invariants__ is not None, (  # type: ignore
+        "Expected to set ``__invariants__`` in the invariant decorator before "
+        "the call to {}".format(add_invariant_checks.__name__)
+    )
+    assert len(cls.__invariants__) > 0, (  # type: ignore
+        "Expected at least one invariant in the ``__invariants__`` since we expect "
+        "to push the latest invariant in the invariant decorator before the call to "
+        "{}".format(add_invariant_checks.__name__)
+    )
+    last_invariant = cls.__invariants__[-1]  # type: ignore
+    assert isinstance(last_invariant, icontract._types.Invariant)
+
+    # Filter out entries in the directory which are certainly not candidates for decoration
+    # regarding the ``last_invariant``. Note that the functions which are already decorated
+    # will not be re-decorated, so that this loop runs in O( dir(cls) * len(invariants) ),
+    # but with a negligible constant.
     for name in dir(cls):
         value = getattr(cls, name)
 
         # __new__ is a special class method (though not marked properly with @classmethod!).
         # We need to ignore __repr__ to prevent endless loops when generating error messages.
-        # __getattribute__, __setattr__ and __delattr__ are too invasive and alter the state of the instance.
-        # Hence we don't consider them "public".
-        if name in [
-            "__new__",
-            "__repr__",
-            "__getattribute__",
-            "__setattr__",
-            "__delattr__",
-        ]:
+        # We also need to ignore __getattribute__ since pretty much any operation on the instance
+        # will result in an endless loop.
+        if name in ["__new__", "__repr__", "__getattribute__"]:
             continue
 
         if name == "__init__":
@@ -1197,7 +1223,19 @@ def add_invariant_checks(cls: ClassT) -> None:
                 type(value)
             )
 
-            init_name_func = (name, value)
+            init_func = value
+            continue
+
+        if (
+            name != "__setattr__"
+            and InvariantCheckEvent.CALL not in last_invariant.check_on
+        ):
+            continue
+
+        if (
+            name == "__setattr__"
+            and InvariantCheckEvent.SETATTR not in last_invariant.check_on
+        ):
             continue
 
         if (
@@ -1234,18 +1272,16 @@ def add_invariant_checks(cls: ClassT) -> None:
                 )
             )
 
-    if init_name_func:
-        name, func = init_name_func
-
+    if init_func:
         # We have to distinguish this special case which is used by named
         # tuples and possibly other optimized data structures.
         # In those cases, we have to wrap __new__ instead of __init__.
-        if func == object.__init__ and hasattr(cls, "__new__"):
+        if init_func == object.__init__ and hasattr(cls, "__new__"):
             new_func = getattr(cls, "__new__")
             setattr(cls, "__new__", _decorate_new_with_invariants(new_func))
         else:
-            wrapper = _decorate_with_invariants(func=func, is_init=True)
-            setattr(cls, name, wrapper)
+            wrapper = _decorate_with_invariants(func=init_func, is_init=True)
+            setattr(cls, init_func.__name__, wrapper)
 
     for name, func in names_funcs:
         wrapper = _decorate_with_invariants(func=func, is_init=False)
diff --git a/icontract/_decorators.py b/icontract/_decorators.py
index d5014c6..c7b0ecd 100644
--- a/icontract/_decorators.py
+++ b/icontract/_decorators.py
@@ -13,7 +13,7 @@
 
 import icontract._checkers
 from icontract._globals import CallableT, ExceptionT, ClassT
-from icontract._types import Contract, Snapshot
+from icontract._types import Contract, Snapshot, InvariantCheckEvent, Invariant
 
 
 class require:  # pylint: disable=invalid-name
@@ -350,6 +350,9 @@ class invariant:  # pylint: disable=invalid-name
     async, as most dunder methods need to be synchronous methods, and wrapping them with async code would
     break that constraint.
 
+    For efficiency reasons, we do not check the invariants at the calls to ``__setattr__`` and ``__getattr__``
+    methods by default. If you indeed want to check the invariants in those calls as well, make sure to adapt
+    the argument ``check_on`` accordingly.
     """
 
     def __init__(
@@ -361,6 +364,7 @@ def __init__(
         error: Optional[
             Union[Callable[..., ExceptionT], Type[ExceptionT], BaseException]
         ] = None,
+        check_on: InvariantCheckEvent = InvariantCheckEvent.CALL,
     ) -> None:
         """
         Initialize a class decorator to establish the invariant on all the public methods.
@@ -387,11 +391,17 @@ def __init__(
             * A subclass of ``BaseException`` which is instantiated with the violation message and raised
               on contract violation.
             * An instance of ``BaseException`` that will be raised with the traceback on contract violation.
+        :param check_on:
+            Specify when to check the invariant.
+
+            * If :py:attr:`InvariantCheckEvent.CALL` is set, the invariant will be checked on all the method calls
+              except ``__setattr__``.
+            * If :py:attr:`InvariantCheckEvent.SETATTR` is set, the invariant will be checked on ``__setattr__`` calls.
         :return:
 
         """
         self.enabled = enabled
-        self._contract = None  # type: Optional[Contract]
+        self._invariant = None  # type: Optional[Invariant]
 
         if not enabled:
             return
@@ -432,7 +442,8 @@ def __init__(
                 "Async conditions are not possible in invariants as sync methods such as __init__ have to be wrapped."
             )
 
-        self._contract = Contract(
+        self._invariant = Invariant(
+            check_on=check_on,
             condition=condition,
             description=description,
             a_repr=a_repr,
@@ -440,10 +451,12 @@ def __init__(
             location=location,
         )
 
-        if self._contract.mandatory_args and self._contract.mandatory_args != ["self"]:
+        if self._invariant.mandatory_args and self._invariant.mandatory_args != [
+            "self"
+        ]:
             raise ValueError(
                 "Expected an invariant condition with at most an argument 'self', but got: {}".format(
-                    self._contract.condition_args
+                    self._invariant.condition_args
                 )
             )
 
@@ -459,12 +472,18 @@ def __call__(self, cls: ClassT) -> ClassT:
             return cls
 
         assert (
-            self._contract is not None
+            self._invariant is not None
         ), "self._contract must be set if the contract was enabled."
 
         if not hasattr(cls, "__invariants__"):
-            invariants = []  # type: List[Contract]
+            invariants = []  # type: List[Invariant]
             setattr(cls, "__invariants__", invariants)
+
+            invariants_on_call = []  # type: List[Invariant]
+            setattr(cls, "__invariants_on_call__", invariants_on_call)
+
+            invariants_on_setattr = []  # type: List[Invariant]
+            setattr(cls, "__invariants_on_setattr__", invariants_on_setattr)
         else:
             invariants = getattr(cls, "__invariants__")
             assert isinstance(
@@ -473,7 +492,25 @@ def __call__(self, cls: ClassT) -> ClassT:
                 cls, type(invariants)
             )
 
-        invariants.append(self._contract)
+            invariants_on_call = getattr(cls, "__invariants_on_call__")
+            assert isinstance(invariants_on_call, list), (
+                "Expected invariants on call of class {} to be a list, "
+                "but got: {}".format(cls, type(invariants_on_call))
+            )
+
+            invariants_on_setattr = getattr(cls, "__invariants_on_setattr__")
+            assert isinstance(invariants_on_setattr, list), (
+                "Expected invariants on call of class {} to be a list, "
+                "but got: {}".format(cls, type(invariants_on_setattr))
+            )
+
+        invariants.append(self._invariant)
+
+        if InvariantCheckEvent.CALL in self._invariant.check_on:
+            invariants_on_call.append(self._invariant)
+
+        if InvariantCheckEvent.SETATTR in self._invariant.check_on:
+            invariants_on_setattr.append(self._invariant)
 
         icontract._checkers.add_invariant_checks(cls=cls)
 
diff --git a/icontract/_types.py b/icontract/_types.py
index 3e96bfb..727be67 100644
--- a/icontract/_types.py
+++ b/icontract/_types.py
@@ -1,4 +1,5 @@
 """Define data structures shared among the modules."""
+import enum
 import inspect
 import reprlib
 from typing import (
@@ -122,3 +123,48 @@ def __init__(
         self.arg_set = set(args)
 
         self.location = location
+
+
+class InvariantCheckEvent(enum.Flag):
+    """Define when an invariant should be checked."""
+
+    #: Evaluate the invariant before and after all calls to a method.
+    CALL = enum.auto()
+
+    #: Evaluate the invariant before and after all the calls to ``__setattr__``.
+    SETATTR = enum.auto()
+
+    #: Always evaluate the invariant, *i.e., both on calls and on attributes set.
+    ALL = CALL | SETATTR
+
+
+class Invariant(Contract):
+    """Represent a contract which is checked on all or some of the class operations."""
+
+    # NOTE (mristin):
+    # The class ``Invariant`` inherits from ``Contract`` so that we can maintain
+    # the backwards compatibility with the integrators after introducing
+    # the ``check_on`` feature.
+
+    def __init__(
+        self,
+        check_on: InvariantCheckEvent,
+        condition: Callable[..., Any],
+        description: Optional[str] = None,
+        a_repr: reprlib.Repr = icontract._globals.aRepr,
+        error: Optional[
+            Union[Callable[..., ExceptionT], Type[ExceptionT], BaseException]
+        ] = None,
+        location: Optional[str] = None,
+    ) -> None:
+        """Initialize with the given values."""
+        assert not hasattr(self, "check_on")
+        self.check_on = check_on
+
+        super().__init__(
+            condition=condition,
+            description=description,
+            a_repr=a_repr,
+            error=error,
+            location=location,
+        )
diff --git a/precommit.py b/precommit.py
index b0bb010..1c7bf5c 100755
--- a/precommit.py
+++ b/precommit.py
@@ -131,14 +131,21 @@ def main() -> int:
 
     subprocess.check_call(["coverage", "report"])
 
-    print("Doctesting...")
-    doc_files = ["README.rst"]
-    for pth in (repo_root / "docs" / "source").glob("**/*.rst"):
-        doc_files.append(str(pth.relative_to(repo_root)))
-    subprocess.check_call([sys.executable, "-m", "doctest"] + doc_files)
-
-    for pth in (repo_root / "icontract").glob("**/*.py"):
-        subprocess.check_call([sys.executable, "-m", "doctest", str(pth)])
+    if (3, 8) <= sys.version_info < (3, 9):
+        print("Doctesting...")
+        doc_files = ["README.rst"]
+        for pth in (repo_root / "docs" / "source").glob("**/*.rst"):
+            doc_files.append(str(pth.relative_to(repo_root)))
+        subprocess.check_call([sys.executable, "-m", "doctest"] + doc_files)
+
+        for pth in (repo_root / "icontract").glob("**/*.py"):
+            subprocess.check_call([sys.executable, "-m", "doctest", str(pth)])
+    else:
+        print(
+            "We pin the doctests at Python 3.8 as the output of the exception "
+            "traceback changes between the Python versions. You are running Python "
+            "{}, so we will not run the doctests.".format(sys.version_info)
+        )
 
     print("Checking the restructured text of the readme...")
     subprocess.check_call(
diff --git a/pylint.rc b/pylint.rc
index 6ce99e7..6e41e58 100644
--- a/pylint.rc
+++ b/pylint.rc
@@ -7,5 +7,5 @@ generated-members=bottle\.request\.forms\.decode,bottle\.request\.query\.decode
 max-line-length=120
 
 [MESSAGES CONTROL]
-disable=too-few-public-methods,len-as-condition,duplicate-code,no-else-raise,too-many-locals,too-many-branches,too-many-lines,too-many-arguments,too-many-statements,too-many-nested-blocks,too-many-function-args,too-many-instance-attributes,too-many-public-methods,protected-access,consider-using-in,no-member,consider-using-f-string,use-dict-literal,redundant-keyword-arg
+disable=too-few-public-methods,len-as-condition,duplicate-code,no-else-raise,too-many-locals,too-many-branches,too-many-lines,too-many-arguments,too-many-statements,too-many-nested-blocks,too-many-function-args,too-many-instance-attributes,too-many-public-methods,protected-access,consider-using-in,no-member,consider-using-f-string,use-dict-literal,redundant-keyword-arg,no-else-return
 


Test patch:
diff --git a/tests/test_invariant.py b/tests/test_invariant.py
index 38dd08b..9c8821d 100644
--- a/tests/test_invariant.py
+++ b/tests/test_invariant.py
@@ -10,6 +10,7 @@
     Mapping,
     Optional,
     Any,
+    List,
 )  # pylint: disable=unused-import
 
 import icontract
@@ -909,5 +910,408 @@ def __init__(self) -> None:
         )
 
 
+class TestCheckOn(unittest.TestCase):
+    def test_invariant_checked_in_init_if_no_flag_set(self) -> None:
+        # The check_on default is expected to be ``CALL`` here.
+        @icontract.invariant(lambda self: self.x > 0)
+        class A:
+            def __init__(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            _ = A()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_invariant_checked_in_init_if_call_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.CALL
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            _ = A()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_invariant_checked_in_init_if_setattr_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.SETATTR
+        )
+        class A:
+            def __init__(self, an_x: int) -> None:
+                # NOTE (mristin):
+                # Setting the ``x`` will trigger a violation here.
+                self.x = an_x
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            _ = A(-1)
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_invariant_checked_in_init_if_all_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.ALL
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            _ = A()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_setattr_not_affected_when_flag_not_set(self) -> None:
+        # The check_on default is expected to be ``CALL`` here.
+        @icontract.invariant(lambda self: self.x > 0)
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+        a = A()
+        a.x = -1
+
+        # No error expected here as we rely on the default ``check_on`` to be set to ``CALL``
+        # so that setattr is not affected.
+
+    def test_call_not_affected_when_flag_not_set(self) -> None:
+        # fmt: off
+        @icontract.invariant(
+            lambda self: self.x > 0,
+            check_on=icontract.InvariantCheckEvent.SETATTR
+        )
+        # fmt: on
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+            def do_something_bad(self) -> None:
+                self.__dict__["x"] = -1
+
+        a = A()
+        a.do_something_bad()
+
+        # No error expected here as ``check_on`` mandates to only check setting
+        # an attribute.
+
+    def test_setattr_affected_when_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.SETATTR
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        a = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.x = -1
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_call_and_setattr_affected_when_flags_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0,
+            check_on=(
+                icontract.InvariantCheckEvent.CALL
+                | icontract.InvariantCheckEvent.SETATTR
+            ),
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+            def do_something_bad(self) -> None:
+                self.x = -1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        that = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            that.x = -1
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+        other = A()
+
+        another_violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            other.do_something_bad()
+        except icontract.ViolationError as err:
+            another_violation_error = err
+
+        self.assertIsNotNone(another_violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(another_violation_error)),
+        )
+
+    def test_setattr_with_property(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.SETATTR
+        )
+        class A:
+            def __init__(self) -> None:
+                self._x = 10
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+            @property
+            def x(self) -> int:
+                return self._x
+
+            @x.setter
+            def x(self, value: int) -> None:
+                self._x = value
+
+        a = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.x = -1
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_that_setattr_does_not_trigger_on_a_collection(self) -> None:
+        # NOTE (mristin):
+        # We want to explicitly specify that operations on collection attributes
+        # do not trigger the SETATTR event.
+
+        @icontract.invariant(
+            lambda self: len(self.lst) < 2,
+            check_on=icontract.InvariantCheckEvent.SETATTR,
+        )
+        class A:
+            def __init__(self) -> None:
+                self.lst = [3]
+
+        a = A()
+        # NOTE (mristin):
+        # There is no ``__setattr__`` call involved here, so the invariant will not be
+        # checked.
+        a.lst.append(10)
+
+    def test_that_invariant_checked_on_call_even_though_violated_through_leaking(
+        self,
+    ) -> None:
+        # NOTE (mristin):
+        # Example adapted from a comment in:
+        # https://github.com/Parquery/icontract/pull/292
+
+        @icontract.invariant(
+            lambda self: all(item < 0 for item in self.lst),
+            check_on=icontract.InvariantCheckEvent.CALL,
+        )
+        class A:
+            def __init__(self, lst: List[int]) -> None:
+                self.lst = lst
+
+            def do_something(self) -> None:
+                pass
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        a = A(lst=[-1])
+
+        # NOTE (mristin):
+        # The invariant will be violated here through leaking, but ``a.lst`` is not
+        # changed, so no SETATTR event will be triggered.
+        a.lst.append(1)
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            # NOTE (mristin):
+            # The violation must occur here as we check it *before* and *after* every method.
+            a.do_something()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                all(item < 0 for item in self.lst):
+                all(item < 0 for item in self.lst) was False, e.g., with
+                  item = 1
+                self was an instance of A
+                self.lst was [-1, 1]"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_invariant_checked_on_setattr_if_all_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.ALL
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = 1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+            def do_something_wrong(self) -> None:
+                self.x = -1
+
+        a = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.do_something_wrong()
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+    def test_invariant_checked_on_setattr_if_setattr_flag_set(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.ALL
+        )
+        class A:
+            def __init__(self) -> None:
+                self.x = 1
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        a = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.x = -1
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests_3_7/test_invariant.py b/tests_3_7/test_invariant.py
index 60a9443..1a97d0b 100644
--- a/tests_3_7/test_invariant.py
+++ b/tests_3_7/test_invariant.py
@@ -108,5 +108,37 @@ class Foo:
         )
 
 
+class TestCheckOn(unittest.TestCase):
+    def test_setattr_with_dataclass(self) -> None:
+        @icontract.invariant(
+            lambda self: self.x > 0, check_on=icontract.InvariantCheckEvent.SETATTR
+        )
+        @dataclasses.dataclass
+        class A:
+            x: int = 10
+
+            def __repr__(self) -> str:
+                return "an instance of {}".format(self.__class__.__name__)
+
+        a = A()
+
+        violation_error = None  # type: Optional[icontract.ViolationError]
+        try:
+            a.x = -1
+        except icontract.ViolationError as err:
+            violation_error = err
+
+        self.assertIsNotNone(violation_error)
+        self.assertEqual(
+            textwrap.dedent(
+                """\
+                self.x > 0:
+                self was an instance of A
+                self.x was -1"""
+            ),
+            tests.error.wo_mandatory_location(str(violation_error)),
+        )
+
+
 if __name__ == "__main__":
     unittest.main()