Design by Contract for Python
Project description
Pystitia
A lightweight Python library for Design by Contract (DbC) programming, enabling you to specify preconditions and postconditions for your functions.
Overview
Pystitia brings the power of Design by Contract to Python through simple decorators. Define what must be true before your function runs (preconditions) and what must be true after it completes (postconditions), making your code more reliable and self-documenting.
Pystitia - the name
Pystitia takes its name from Justitia, the Roman goddess of justice, who is traditionally depicted holding scales to weigh evidence and a sword to execute judgment. Just as Justitia enforces fairness through balanced evaluation of obligations and rights, pystitia enforces software correctness by balancing the obligations between callers (preconditions) and implementers (postconditions). The name reflects the library's core principle: that code should honor its contracts with the same rigor that justice demands adherence to law.
Installation
pip install pystitia
Quick Start
from pystitia import contracts, setTestMode
# Enable contract checking
setTestMode(True)
@contracts(
preconditions=[
lambda x: x > 0,
lambda x: isinstance(x, (int, float))
],
postconditions=[
lambda __return__: __return__ > 0
]
)
def square_root(x):
return x ** 0.5
# This works
result = square_root(16) # Returns 4.0
# This raises PreConditionError
result = square_root(-5) # Negative number violates precondition
Features
Preconditions
Preconditions specify what must be true before a function executes. They represent the caller's obligations.
@contracts(
preconditions=[
lambda balance, amount: amount > 0,
lambda balance, amount: balance >= amount
]
)
def withdraw(balance, amount):
return balance - amount
Postconditions
Postconditions specify what the function guarantees after execution. They can access:
__return__: The function's return value__old__: A namespace containing deep copies of all arguments before execution__id__: A namespace containing the original object IDs for mutability checks- All original function arguments
@contracts(
postconditions=[
lambda __return__, amount: __return__ == __old__.balance - amount,
lambda __old__, balance: id(balance) == __id__.balance # Ensure immutability
]
)
def withdraw(balance, amount):
return balance - amount
Checking Object Mutations
Pystitia allows you to verify whether objects were modified during function execution:
@contracts(
postconditions=[
lambda data, __id__: id(data) == __id__.data # Verify list wasn't replaced
]
)
def append_item(data, item):
data.append(item)
return data
Test Mode Control
Enable or disable contract checking at runtime. This is useful for disabling overhead in production:
from pystitia import setTestMode
# Enable contract checking (recommended for development/testing)
setTestMode(True)
# Disable contract checking (for production)
setTestMode(False)
Important: You must call setTestMode() before using any decorated functions, or a NameError will be raised.
Complete Example
from pystitia import contracts, setTestMode
setTestMode(True)
class BankAccount:
def __init__(self, initial_balance):
self.balance = initial_balance
@contracts(
preconditions=[
lambda self, amount: amount > 0,
lambda self, amount: self.balance >= amount
],
postconditions=[
lambda self, __old__: self.balance == __old__.balance - amount,
lambda self: self.balance >= 0
]
)
def withdraw(self, amount):
self.balance -= amount
return self.balance
@contracts(
preconditions=[
lambda self, amount: amount > 0
],
postconditions=[
lambda self, __old__, amount: self.balance == __old__.balance + amount
]
)
def deposit(self, amount):
self.balance += amount
return self.balance
# Usage
account = BankAccount(100)
account.deposit(50) # balance = 150
account.withdraw(30) # balance = 120
account.withdraw(200) # Raises PreConditionError: insufficient funds
Writing Condition Functions
Condition functions should:
- Return
Trueif the condition is satisfied - Return
Falseif the condition is violated - Accept only the parameters they need from the decorated function
- Use lambda functions for simple conditions
- Use named functions for complex conditions
def is_positive(x):
"""Check if value is positive."""
return x > 0
def valid_email(email):
"""Check if email format is valid."""
return '@' in email and '.' in email.split('@')[1]
@contracts(
preconditions=[is_positive, valid_email]
)
def send_notification(user_id, email):
# Function implementation
pass
Special Variables in Postconditions
__return__: The value returned by the function__old__.param_name: Deep copy of the parameter before function execution__id__.param_name: Original object ID of the parameter (for mutability checks)
Error Handling
Pystitia raises two custom exceptions:
PreConditionError: Raised when a precondition failsPostConditionError: Raised when a postcondition fails
Both exceptions include:
- Function name
- File path
- Line number
- Indices of failed condition functions
from pystitia import PreConditionError, PostConditionError
try:
result = some_function(invalid_input)
except PreConditionError as e:
print(f"Invalid input: {e}")
except PostConditionError as e:
print(f"Function violated its contract: {e}")
Performance Considerations
-
Contract checking adds runtime overhead due to:
- Condition function calls
- Deep copying of arguments for postconditions (when
__old__is used) - Additional introspection
-
Use
setTestMode(False)in production to disable all contract checking -
Consider the cost of deep copying large data structures
-
Write efficient condition functions
Best Practices
- Keep conditions simple: Each condition should check one thing
- Use descriptive names: For complex conditions, use named functions with docstrings
- Test both paths: Verify that conditions properly catch violations
- Enable in development: Always run with
setTestMode(True)during development - Document side effects: Use postconditions to document and verify intentional mutations
- Fail fast: Design preconditions to catch errors as early as possible
Limitations
- Requires explicit
setTestMode()call before use - Deep copying adds overhead for postconditions using
__old__ - Decorated functions lose their original signature (affects IDE autocomplete)
- No support for class invariants (yet)
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
Note: This library is intended primarily for development and testing. For production use, consider disabling contract checking with setTestMode(False) to avoid performance overhead.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pystitia-1.4.post1.tar.gz.
File metadata
- Download URL: pystitia-1.4.post1.tar.gz
- Upload date:
- Size: 8.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
361e9ab40d8ec4ef98b990670dbeaed7f853f47b329a3a6760ca50aaede05217
|
|
| MD5 |
b76cbec30dd95389ed200edb708ec2d6
|
|
| BLAKE2b-256 |
3de8ddaa89a9b686ba70e5fe9b29b0af4bef5837694f6e1343a989908cf38103
|
File details
Details for the file pystitia-1.4.post1-py3-none-any.whl.
File metadata
- Download URL: pystitia-1.4.post1-py3-none-any.whl
- Upload date:
- Size: 9.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3c9771fbcb56bd5ed6b0c9e0e0c9e44ea9f6fe3ee861a08e0963011918031ee
|
|
| MD5 |
9bbb4e19b2e28560a28afc2ccc295763
|
|
| BLAKE2b-256 |
d0c75e8de3a5be1347c696a222edc3e93fd35fce50f2d983db8464da8a18aa61
|