Project Enforce Rules
Project Enforce Rules (or PER for short) expands the Python type system to allow more constraints.
Version #
MAJOR: 3
MINOR: 7
PATCH: 1
If you need to catch up, you can see the full version history in the CHANGELOG.
Read the documentation.
It exists to let you use features Python doesn't already provide in the typing system — things you probably want, like min, max, length, all_same, and many more.
PER now supports runtime validation anywhere using:
validate(value: object, rules: Dict[str, Any])
This enforces rule dictionaries and returns the original value if valid. If invalid, it raises a descriptive error.
Why I Made It I wanted type‑hint features Python didn’t give me. I thought dictionaries would work until I learned... well... Python didn’t enforce them.
So with the help of Microsoft Copilot and my dad, we designed a module that enforces this type of stuff. Then I realized everyone in the Python community could use this, so it became a project. Hence, creating PER.
PER uses rule dictionaries and validate() to enforce constraints at runtime.
Install it with pip:
pip install enforce-rules
or
python -m pip install enforce-rules
Then use it like:
from enforce_rules import validate
Dependencies
When installed with pip, dependencies are installed automatically in 3.2.15+.
If you are using an earlier version, or running from the source tarball, install them with:
pip install "python-chess>=1.11" "pydantic>=2"
or
python -m pip install "python-chess>=1.11" "pydantic>=2"
Features
- Runtime enforcement of rule dictionaries
- Dictionary‑based rule definitions
- No extra objects required
- Works anywhere in your code
- Extensible via must_be_true
How It Works PER validates values using:
validate(value, rules)
If the value violates a rule, PER raises an error. If the value passes, PER returns the original value unchanged.
This means validated values behave exactly like normal Python values.
Keywords and Usage
Below are all supported keywords.
length
The length of the object must be exactly this.
lst = validate([1, 2, 3, 4, 5], {"length": 5})
min_length
Minimum length (inclusive).
lst = validate(['a', 'b', 'c', 'd', 'e'], {"min_length": 3})
max_length
Maximum length (inclusive).
lst = validate([1, 2, 3, 4, 5, 6], {"max_length": 7})
min
Minimum numeric value (inclusive).
number = validate(10, {"min": 0})
max
Maximum numeric value (inclusive).
number = validate(10, {"max": 20})
allowed_values
Similar to Literal; value must be one of the allowed values.
val = validate("a", {"allowed_values": ("a", "b", "c", "d")})
invariant
Value must be truthy.
val = validate((0 == 0), {"invariant": True})
all_same
All values in the collection must be the same.
numbers = validate([1, 1, 1], {"all_same": True})
all_unique
All values in the collection must be unique.
numbers = validate([1, 2, 3], {"all_unique": True})
non_empty
Collection must not be empty.
my_strings = validate(['a', 'b', 'c'], {"non_empty": True})
no_nulls
Collection must not contain None.
my_things = validate([1, 2, 3, "a", "b", "c"], {"no_nulls": True})
sorted
List must be sorted (increasing or decreasing).
numbers = validate([1, 5, 9], {"sorted": True})
increasing
List must be strictly increasing.
numbers = validate([1, 5, 9], {"increasing": True})
decreasing
List must be strictly decreasing.
numbers = validate([9, 5, 1], {"decreasing": True})
sum_min
Minimum sum of the collection (inclusive).
numbers = validate([10, 20, 30], {"sum_min": 50})
sum_max
Maximum sum of the collection (inclusive).
numbers = validate([10, 20, 30], {"sum_max": 70})
element_min
Minimum value for any element (inclusive).
numbers = validate([10, 20, 30], {"element_min": 5})
element_max
Maximum value for any element (inclusive).
numbers = validate([10, 20, 30], {"element_max": 40})
regex
String must match the regex.
cat_or_dog = validate("cat", {"regex": "cat|dog"})
regex_flags
Turns out I didn't notice this in my code, until 1.1.0. This is the regex flags
from re import RegexFlag
cat_or_dog = validate("cat", {"regex": "cat|dog", {"regex_flags": RegexFlag.I | RegexFlag.M | RegexFlag.X
before_date
Value must be strictly before the given datetime.
validate(datetime(1999, 8, 29), {"before_date": datetime(2000, 1, 1)})
after_date
Value must be strictly after the given datetime.
validate(datetime(2026, 8, 29), {"after_date": datetime(2000, 1, 1)})
piece_color
The piece must have this exact color.
import chess
piece = validate(chess.Piece(chess.ROOK, chess.WHITE), {"piece_color": chess.WHITE})
piece_type
The piece must be exactly this type (e.g., chess.KNIGHT, chess.ROOK).
import chess
piece = validate(chess.Piece(chess.KNIGHT, chess.WHITE), {"piece_type": chess.KNIGHT})
chess_symbol
The piece’s symbol must match this string ("P", "n", "r", etc.).
import chess
piece = validate(chess.Piece(chess.ROOK, chess.BLACK), {"chess_symbol": "r"})
is_password
The value must satisfy all password requirements when this is set to True.
Requirements (sorted):
- At least 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one symbol
password = validate("Abcdef!1", {"is_password": True})
must_be_true
Custom rule: a function that returns True for allowed values.
def is_even(x: int) -> bool:
return x % 2 == 0
even_number = validate(8, {"must_be_true": is_even})
This calls:
is_even(8)
If enough people use a must_be_true lambda, it may become an added keyword in a later version.
is_valid
is_valid() is a boolean wrapper around validate() (snippet shows latest version of is_valid):
def is_valid(value: T, rules: ValidateDict) -> bool:
"""Return True if value passes every rule, otherwise False.
Any exception raised while checking counts as a failure, including
wrong-type values and misspelled rule names. Use validate() to see why.
"""
try:
validate(value, rules)
return True
except Exception:
return False
This wrapper doesn't modify rule logic. It simply converts exceptions into boolean results.
WARNING: It never raises any error while checking, including a wrong-type value or a misspelled rule name, returns False. Use validate() when you need to know why a check failed.
compile_rules
compile_rules(rules) checks a rule dictionary once, before any value is checked, and returns a function that validates values against it. (Added in .)
from enforce_rules import compile_rules
check_age = compile_rules({"min": 0, "max": 120})
age = check_age(30) # returns 30
check_age(200) # raises ValueError
If the rules themselves are wrong, compile_rules raises ValueError right away and lists every problem it finds:
compile_rules({"mni": 0, "regex": "("})
# ValueError: Invalid rules: unknown rule 'mni'; invalid regex: ...
It checks that every key is a known keyword and that a regex pattern compiles. It does not check whether the values of the other rules have the right type, so a rule like {"min": "five"} is only caught when a value is checked. The returned function behaves like validate(value, rules), and later changes to the original dictionary do not affect it.
Use compile_rules when the same rules are used many times, or when you want a mistake in the rules to fail early. is_valid would hide a misspelled rule name behind a plain False.
Contributing
Contributions are welcome. Please open an issue or pull request. When contributing, ensure backwards compatibility (you cannot remove keywords and/or features).
Please note, when using my module, that you will manually have to validate each time should you choose to mutate a variable.
Versioning Policy
This project guarantees full backwards compatibility. Existing rule files, keyword meanings, validator behaviors, and metadata formats will continue to work exactly as before. No update will ever break existing configurations.
Version Numbering
This project uses a non-breaking semantic versioning model:
MAJOR.MINOR.PATCH
MAJOR = large new feature families MINOR = small additive keywords or enhancements PATCH = bug fixes or internal improvements
Major bumps do not imply breaking changes. They only indicate that a significant new capability has been added.
Major bumps always reset MINOR and PATCH to 0. For example: 1.7.3 -> 2.0.0
1.12.0 -> 2.0.0
1.0.0 -> 2.0.0
Minor bumps always reset PATCH to 0. For example: 1.7.3 -> 1.8.0
1.12.9 -> 1.13.0
1.0.4 -> 1.1.0
Minor Version Bumps
Minor bumps occur when adding small keywords. Examples include:
- min_inclusive
- max_inclusive
- trim_whitespace
- pattern_flags
These additions do not change the meaning of existing keywords, do not require users to modify rule files, and do not alter validator behavior. They are classified as minor updates.
Minor bumps always reset PATCH to 0
Credits:
- Copilot
- Dad
- Claude
Metadata
Release files for enforce-rules 3.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| enforce_rules-3.7.1.tar.gz | 18.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| enforce_rules-3.7.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.5 kB
Release files / enforce_rules-3.7.1.tar.gz
| Download URL | enforce_rules-3.7.1.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3b25575b64e6ad57e5271258d6f13ee7229553d8c1d6ed7d4f0988bf0126699e
|
|
BLAKE2b-256 checksum How to use checksums |
5003b2da155c3fab09a3b56764b7af1d3460028534627dc6a475730239edeaf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / enforce_rules-3.7.1-py3-none-any.whl
| Download URL | enforce_rules-3.7.1-py3-none-any.whl |
|---|---|
| Size | 11.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c361438c28de798a9a918dea680eeadcefa500084f86139fdb83857463178c59
|
|
BLAKE2b-256 checksum How to use checksums |
0ebeac2121c236597266e1e0567efd8dc91de03bf8198ec19d4d9a91cb23cee9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for enforce-rules 3.7.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.