Nitro Validate
Check incoming data against rules like 'required|email' and get every error back at once, keyed by field. 51 built-in rules, custom rules in a few lines, and no dependencies.
Installation
pip install nitro-validator
Needs Python 3.7 or newer.
AI assistant integration
Teach your AI coding assistant the rule names and API:
npx skills add nitrosh/nitro-validate
Assistants that support skills, like Claude Code, will then write validation code that uses the right rules.
Quick Start
from nitro_validator import NitroValidator, NitroValidationError
validator = NitroValidator()
data = {
'email': 'user@example.com',
'age': 25,
'password': 'secret123',
'confirm_password': 'secret123'
}
rules = {
'email': 'required|email',
'age': 'required|numeric|min:18',
'password': 'required|min:8',
'confirm_password': 'required|same:password'
}
try:
validated_data = validator.validate(data, rules)
print("Valid:", validated_data)
except NitroValidationError as e:
print("Invalid:", e.errors)
Every rule runs for every field, so a bad payload reports all its problems in one go:
{'email': ['The email field must be a valid email address.'],
'age': ['The age field must be at least 18.']}
Every class has a shorter alias: Validator, ValidationError, Rule and RuleRegistry. The examples below use both.
Why Nitro Validator?
- No runtime dependencies, so it drops into any project.
- 51 built-in rules for strings, numbers, dates, formats and cross-field checks.
- A custom rule is one small class with a name, a message and a
validate()method. - Error messages can be overridden per field or per rule.
- 100% test coverage.
Available Rules
Basic Rules
| Rule | Description | Example |
|---|---|---|
required |
Field must be present and not empty | 'email': 'required' |
optional |
Field is optional (always passes) | 'middle_name': 'optional' |
Most other rules pass when the value is missing or empty, so 'email' on its own accepts a blank field. Add required when the field has to be there.
String Rules
| Rule | Description | Example |
|---|---|---|
alpha |
Only alphabetic characters | 'name': 'alpha' |
alphanumeric |
Only alphanumeric characters | 'username': 'alphanumeric' |
alpha_dash |
Letters, numbers, dashes, underscores | 'slug': 'alpha_dash' |
lowercase |
Only lowercase characters | 'code': 'lowercase' |
uppercase |
Only uppercase characters | 'code': 'uppercase' |
email |
Valid email address | 'email': 'email' |
url |
Valid http/https URL | 'website': 'url' |
uuid |
UUID in 8-4-4-4-12 form | 'id': 'uuid' |
ip |
Valid IP address (v4 or v6) | 'address': 'ip' |
ipv4 |
Valid IPv4 address | 'address': 'ipv4' |
ipv6 |
Valid IPv6 address | 'address': 'ipv6' |
json |
Valid JSON string | 'data': 'json' |
slug |
Valid URL slug | 'slug': 'slug' |
ascii |
Only ASCII characters | 'text': 'ascii' |
base64 |
Valid base64 encoding | 'encoded': 'base64' |
hex_color |
Valid hex color code | 'color': 'hex_color' |
credit_card |
Valid credit card number | 'card': 'credit_card' |
mac_address |
Valid MAC address | 'mac': 'mac_address' |
timezone |
IANA timezone name | 'tz': 'timezone' |
locale |
Valid locale code | 'locale': 'locale' |
regex:pattern |
Matches regex pattern | 'code': 'regex:^[A-Z]{3}$' |
starts_with:str |
Starts with substring | 'name': 'starts_with:Mr' |
ends_with:str |
Ends with substring | 'file': 'ends_with:.pdf' |
contains:str |
Contains substring | 'text': 'contains:hello' |
A few of these are stricter than you might expect:
- Format rules check the whole value, so
"a@b.co\n"failsemail. urlaccepts http and https only. The host can be any domain,localhost, an IPv4 address or a bracketed IPv6 address, with an optional port from 1 to 65535.timezonewants an IANA name likeAmerica/New_York. It checks the real zone list when Python has one (3.9+ with system tz data or thetzdatapackage), and only checks the shape otherwise. Offsets like+02:00always fail.regexpatterns containing|or,clash with the rule syntax, so pass them as an object:RegexRule(r'^(a|b)$'). A pattern that won't compile raisesInvalidRuleError.
Numeric Rules
| Rule | Description | Example |
|---|---|---|
numeric |
Must be numeric | 'price': 'numeric' |
integer |
Must be an integer | 'quantity': 'integer' |
positive |
Must be positive number | 'amount': 'positive' |
negative |
Must be negative number | 'deficit': 'negative' |
min:value |
Minimum value or length | 'age': 'min:18' |
max:value |
Maximum value or length | 'rating': 'max:5' |
between:min,max |
Between two values | 'score': 'between:0,100' |
divisible_by:n |
Divisible by number | 'even': 'divisible_by:2' |
Numbers can be ints, floats, or numeric strings like "42", "-1.5" and "1e3". "nan", "inf", "1_000" and padded strings like " 42" are rejected.
min, max, between and size compare numbers by value, and strings, lists and dicts by length. Add numeric or integer to the field and numeric strings are compared by value too, which is what you want for form input:
validator.is_valid({'age': '25'}, {'age': 'numeric|min:18'}) # True, compares 25 >= 18
validator.is_valid({'name': '25'}, {'name': 'min:18'}) # False, compares len('25') >= 18
Comparison Rules
| Rule | Description | Example |
|---|---|---|
same:field |
Must match another field | 'password_confirm': 'same:password' |
different:field |
Must differ from another field | 'new_email': 'different:old_email' |
in:val1,val2 |
Must be in list of values | 'role': 'in:admin,user,guest' |
not_in:val1,val2 |
Must not be in list | 'status': 'not_in:banned,deleted' |
Boolean Rules
| Rule | Description | Example |
|---|---|---|
boolean |
Must be a boolean value | 'active': 'boolean' |
Date Rules
| Rule | Description | Example |
|---|---|---|
date |
Must be a valid date | 'birthdate': 'date' |
before:date |
Date must be before | 'start': 'before:2025-12-31' |
after:date |
Date must be after | 'end': 'after:2024-01-01' |
date_equals:date |
Date must equal | 'today': 'date_equals:2024-11-23' |
date_format:fmt |
Date must match format | 'date': 'date_format:%Y-%m-%d' |
date, before, after and date_equals accept date and datetime objects, and ISO 8601 strings like 2024-01-31, 2024/01/31 and 2024-01-31T14:30:00. Fractional seconds and a Z or +02:00 offset are fine too, so JavaScript's toISOString() output works. Times with an offset are converted to UTC before comparing, and times without one are treated as UTC.
Regional formats are rejected because they're ambiguous: is 01-02-2024 the 1st of February or January 2nd? If you need one, spell out the format with date_format, for example 'date_format:%d-%m-%Y'.
Convenience Rules
| Rule | Description | Example |
|---|---|---|
confirmed |
Matches {field}_confirmation | 'password': 'confirmed' |
accepted |
Must be accepted (yes/true/1/on) | 'terms': 'accepted' |
declined |
Must be declined (no/false/0/off) | 'marketing': 'declined' |
Length Rules
| Rule | Description | Example |
|---|---|---|
length:value |
Exact length | 'zip_code': 'length:5' |
Collection Rules
| Rule | Description | Example |
|---|---|---|
array |
Must be a list or tuple | 'items': 'array' |
size:n |
Exact size (value or length) | 'tags': 'size:3' |
distinct |
Array must have unique values | 'ids': 'distinct' |
Usage Examples
Basic Validation
from nitro_validator import NitroValidator, NitroValidationError
validator = NitroValidator()
data = {'username': 'johndoe', 'age': '25'}
rules = {'username': 'required|alphanumeric', 'age': 'required|integer|min:18'}
try:
validated = validator.validate(data, rules)
print(validated) # {'username': 'johndoe', 'age': '25'}
except NitroValidationError as e:
print(e.errors)
Custom Error Messages
# One message for every rule on a field
messages = {
'email': 'Please provide a valid email address'
}
# Or a message per rule
messages = {
'password': {
'required': 'Password is required',
'min': 'Password must be at least 8 characters'
}
}
validator.validate(data, rules, messages)
Using Rule Objects
from nitro_validator import Validator, RequiredRule, EmailRule, MinRule
validator = Validator()
data = {'email': 'test@example.com', 'age': 25}
rules = {
'email': [RequiredRule(), EmailRule()],
'age': [RequiredRule(), MinRule(18)]
}
validated = validator.validate(data, rules)
Rules that don't take arguments can be passed as classes too: [RequiredRule, EmailRule]. Anything in the list that isn't a rule string, instance or class raises InvalidRuleError.
Rule arguments are checked when the rule is built. A missing or malformed argument like 'min:abc', 'between:1' or 'before:not-a-date' raises InvalidRuleError the first time the rules run, instead of quietly failing every value.
Checking Without Exceptions
validator = Validator()
if validator.is_valid(data, rules):
print("Data is valid")
else:
print("Errors:", validator.get_errors())
Validate in One Call
from nitro_validator import Validator, ValidationError
try:
validator = Validator.make(data, rules)
print("Valid:", validator.validated_data)
except ValidationError as e:
print("Errors:", e.errors)
Creating Custom Rules
A custom rule subclasses NitroValidationRule and sets a name, a message and a validate() method. Return True for empty values, so the rule only kicks in when the field has something in it:
from typing import Any
from nitro_validator import NitroValidationRule, NitroValidator
class StrongPasswordRule(NitroValidationRule):
name = "strong_password"
message = "The {field} must contain uppercase, lowercase, numbers, and symbols."
def validate(self, field: str, value: Any, data: dict) -> bool:
if not value:
return True
has_upper = any(c.isupper() for c in value)
has_lower = any(c.islower() for c in value)
has_digit = any(c.isdigit() for c in value)
has_symbol = any(c in '!@#$%^&*()_+-=' for c in value)
return has_upper and has_lower and has_digit and has_symbol
validator = NitroValidator()
validator.register_rule(StrongPasswordRule)
data = {'password': 'MyP@ssw0rd!'}
rules = {'password': 'required|strong_password'}
validated = validator.validate(data, rules)
If your rule takes arguments, set min_args, and building it with fewer raises InvalidRuleError. Arguments from a rule string like 'multiple_of:5' arrive as strings, so cast them:
class MultipleOfRule(NitroValidationRule):
name = "multiple_of"
message = "The {field} must be a multiple of {0}."
min_args = 1
def validate(self, field, value, data):
if value is None or value == "":
return True
return isinstance(value, int) and value % int(self.args[0]) == 0
Advanced Usage
Cross-field Validation
data = {
'password': 'secret123',
'password_confirmation': 'secret123'
}
rules = {
'password': 'required|min:8',
'password_confirmation': 'required|same:password'
}
validator.validate(data, rules)
Conditional Validation
# Only require an email from customers
data = {'user_type': 'customer', 'email': 'user@example.com'}
if data.get('user_type') == 'customer':
rules = {'email': 'required|email'}
else:
rules = {'email': 'optional'}
validator.validate(data, rules)
Handling Validation Errors
from nitro_validator import ValidationError
try:
validator.validate(data, rules)
except ValidationError as e:
# Errors keyed by field
print(e.errors) # {'email': ['Email is required'], 'age': ['Age must be at least 18']}
# Or every message in one list
flat_errors = validator.get_errors_flat()
print(flat_errors) # ['Email is required', 'Age must be at least 18']
validator.errors and validator.validated_data always hold the results of the most recent call. If one validator is shared between threads, read results from the return value and e.errors instead, or give each thread its own Validator().
Custom Rule Registry
Each Validator() gets its own copy of the built-in rules, so register_rule() on one validator never affects another. To build a registry yourself, start from an empty RuleRegistry() and add the built-ins back if you want them:
from nitro_validator import Validator, RuleRegistry, register_builtin_rules
registry = RuleRegistry()
register_builtin_rules(registry)
registry.register(StrongPasswordRule)
validator = Validator(registry=registry)
Examples
Runnable scripts live in examples/:
python examples/basic_usage.py
python examples/custom_rules.py
python examples/advanced_validation.py
python examples/date_validation.py
python examples/formats.py
Development
Setup
git clone https://github.com/nitrosh/nitro-validate.git
cd nitro-validate
pip install -e ".[dev]"
Run Tests
pytest
The coverage report is included in every run.
Format Code
black nitro_validator tests examples
Coming from GUMP
Nitro Validator grew out of GUMP, a PHP validation library by the same author, so the rule syntax will feel familiar. What's different:
- Custom rules are Python classes you register by name.
- Failures raise
ValidationErrorwith every error attached, or callis_valid()if you'd rather not catch anything. - Type hints throughout.
- No dependencies, where GUMP needs PHP extensions.
Ecosystem
- nitro-cli: Static site generator that builds sites with Python code
- nitro-ui: Build HTML with Python, not strings
- nitro-datastore: Schema-free JSON data store with dot notation access
- nitro-dispatch: Framework-agnostic plugin system
- nitro-image: Fast, friendly image processing for the web
License
This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.
Metadata
Release files for nitro-validator 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nitro_validator-1.1.0.tar.gz | 50.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nitro_validator-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 78.8 kB
Release files / nitro_validator-1.1.0.tar.gz
| Download URL | nitro_validator-1.1.0.tar.gz |
|---|---|
| Size | 50.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
81f4e3e5cebbf40c711f9f926be99aec34486b6fcc84f48b0c11314c7294dc1e
|
|
BLAKE2b-256 checksum How to use checksums |
0e1b56b8423ab4def4762fee6cd41110cc6f1a54c3d01ef4e4c9660c80b8abb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / nitro_validator-1.1.0-py3-none-any.whl
| Download URL | nitro_validator-1.1.0-py3-none-any.whl |
|---|---|
| Size | 28.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
693f6842fd5b64150d8d7233cd8683ad73ffe02a0721a1246e324d609743ecbd
|
|
BLAKE2b-256 checksum How to use checksums |
d657a0f0d49cd94e9a9d6f7273efd073a5fe0bf42b2139e806194078c0a22b7a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|