Recipes

Some ideas that are useful in the real world applications.

Keep contracts simple

If a function accepts only a few short arguments, duplicate the original signature (without annotations) for contracts:

@deal.pre(lambda left, right: right != 0)
def div(left: float, right: float) -> float:
    return left / right

Otherwise, or if a function has default arguments, use simplified signature for contracts:

@deal.pre(lambda _: _.default is not None or _.right != 0)
def div(left: float, right: float, default: float = None) -> float:
    try:
        return left / right
    except ZeroDivisionError:
        if default is not None:
            return default
        raise

Don’t check types

Never check types with deal. MyPy does it much better. Also, there are plenty of alternatives for both static and dynamic validation. Deal is intended to empower types, to tell a bit more about possible values set than you can do with type annotations, not replace them. However, if you want to play with deal a bit or make types a part of contracts, PySchemes-based contract is a good solution:

import deal
from pyschemes import Scheme

@deal.pre(Scheme(dict(left=str, right=str)))
def concat(left, right):
    return left + right

concat('ab', 'cd')
# 'abcd'

concat(1, 2)
# PreContractError: at key 'left' (expected type: 'str', got 'int')

Prefer pre and post over ensure

If a contract needs only function arguments, use pre. If a contract checks only function result, use post. And only if a contract need both input and output values at the same time, use ensure. Keeping available namespace for contract as small as possible makes the contract signature simpler and helps with partial execution in the linter.

Prefer reason over raises

Always try your best to tell why exception can be raised. However, keep in mind that all exceptions from reason still have to be explicitly specified in raises since contracts are isolated and have no way to exchange information between each other:

@deal.reason(ZeroDivisionError, lambda a, b: b == 0)
@deal.raises(ZeroDivisionError)
def divide(a, b):
    return a / b

Keep module initialization pure

Nothing should happen on module load. Create some constants, compile RegExes, and that’s all. Make it lazy.

deal.module_load(deal.pure)

Contracts shouldn’t be important

Never catch contract errors. Never rely on them in runtime. They are for tests and humans. The shouldn’t have an actual logic, only validate it.

Short signature conflicts

In short signature, _ is a dict with access by attributes. Hence it has all dict attributes. So, if argument we need conflicts with a dict attribute, use getitem instead of getattr. For example, we should use _['items'] instead of _.items.

Keep contracts pure

You can use any logic inside the validator. However, thumb up rule is to keep contracts pure (without any side-effects, even logging). The main motivation for it is that some contracts can be partially executed by linter.

The message is a description, not an error

The message argument should tell what is expected behavior without assuming that the user violated it. This is because the users can encounter it not only when a ContractError is raised but also when they just read the source code or generated documentation. For example, if your contract checks that b >= 0, don’t say “b is negative” (what is violated), say “b must be non-negative” (what is expected).

Markers are not only side-effects

The @deal.has decorator is used to track markers. Some of the markers describing side-effects (like stdout) are predefined and detected by linter and in runtime. However, markers can be also used to track anything else you’d like to track in your code. A few examples:

  • Functions that are usually slow.

  • Functions that can be called only for a user with admin access.

  • Functions that access the database.

  • Functions that access the patient data.

  • Functions that can only work with some additional dependencies installed.

  • Deprecated functions.

  • Functions that need refactoring.

Permissive license

Deal is distributed under MIT License which is a permissive license with high license compatibility.. On practice, do whatever you want with deal, I don’t care. However, if you install deal with all extra (pip3 install 'deal[all]'), it will also install astroid which is licensed under LGPL. While this license allows to be used in non-LGPL proprietary software too, it still can be not enough for some companies. So, if this is the case for you, avoid bringing all extra on the prod.