Skip to main content

A Python library for validating function arguments using decorators

Project description

py-guard

A lightweight, decorator-based Python library for validating function arguments with an intuitive and expressive syntax.

✨ Features

  • 🎯 Simple decorator syntax - Just add @guard() to your functions
  • 🔍 Type validation - Automatic type checking from type hints
  • Choice validation - Restrict values to specific options
  • 📝 String validation - Email, URL, regex patterns, and length
  • 📦 Schema validation - Validate dictionary structures
  • 🔧 Custom validators - Easy to create and register your own
  • 🎨 Clear error messages - Detailed validation failures
  • 🚀 Zero dependencies - Pure Python, stdlib only

📦 Installation

pip install pyguard

Requirements:

  • Python 3.9 or higher

🚀 Quick Start

from pyguard import guard

@guard(
    age={"gte": 18, "lte": 120},
    email={"email": True},
    username={"length": (3, 20), "regex": r"^[a-zA-Z0-9_]+$"}
)
def register_user(age: int, email: str, username: str):
    return f"User {username} registered successfully!"

# Valid call
register_user(25, "john@example.com", "john_doe")

# Raises GuardValidationError: age must be at least 18
register_user(15, "john@example.com", "john_doe")

# Raises GuardValidationError: email must be a valid email address
register_user(25, "invalid-email", "john_doe")

📖 Available Validators

Comparison Validators

Validate numeric values with comparison operators:

@guard(
    age={"gte": 18},          # Greater than or equal
    score={"gt": 0},          # Greater than
    temperature={"lte": 100}, # Less than or equal
    count={"lt": 1000}        # Less than
)
def process_data(age: int, score: float, temperature: int, count: int):
    pass

Keywords:

  • gte - Greater than or equal to
  • gt - Greater than
  • lte - Less than or equal to
  • lt - Less than

Type Validation

Type validation is automatic when you use type hints:

@guard()
def calculate(x: int, y: float) -> float:
    return x * y

# Raises GuardValidationError: x must be of type <class 'int'>
calculate("5", 2.5)

You can also explicitly specify types:

@guard(value={"type": int})
def process(value):
    pass

Choice Validation

Restrict values to a specific set of choices:

@guard(currency={"choices": ["USD", "EUR", "GBP"]})
def process_payment(amount: float, currency: str):
    pass

# Works with Enums too
from enum import Enum

class Status(Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"

@guard(status={"choices": Status})
def update_status(status: Status):
    pass

String Validators

Length Validation

@guard(
    username={"length": (3, 20)},     # Min 3, max 20
    password={"length": (8, None)},   # Min 8, no max
    code={"length": (None, 10)}       # No min, max 10
)
def create_account(username: str, password: str, code: str):
    pass

Email Validation

@guard(email={"email": True})
def send_message(email: str, message: str):
    pass

URL Validation

@guard(website={"url": True})
def add_bookmark(website: str):
    pass

Regex Validation

@guard(
    phone={"regex": r"^\d{3}-\d{3}-\d{4}$"},
    zip_code={"regex": r"^\d{5}(-\d{4})?$"}
)
def update_contact(phone: str, zip_code: str):
    pass

Dictionary Validators

Required Keys

@guard(config={"keys": ["host", "port", "database"]})
def connect_db(config: dict):
    pass

# Must include all required keys
connect_db({"host": "localhost", "port": 5432, "database": "mydb"})

Schema Validation

@guard(
    user={
        "schema": {
            "name": str,
            "age": int,
            "email": str
        }
    }
)
def create_user(user: dict):
    pass

# Validates both keys and types
create_user({"name": "John", "age": 30, "email": "john@example.com"})

Required Validation

Mark arguments as required (cannot be None):

@guard(user_id={"required": True})
def get_user(user_id):
    pass

# Raises GuardValidationError: user_id must not be None
get_user(None)

🎨 Multiple Validators

You can combine multiple validators on a single argument:

@guard(
    username={
        "length": (3, 20),
        "regex": r"^[a-zA-Z0-9_]+$"
    },
    age={
        "gte": 13,
        "lte": 120,
        "type": int
    }
)
def register(username: str, age: int):
    pass

🔧 Custom Validators

Create your own validators by extending the Validator class:

from pyguard import Guard, Validator, guard
from typing import Any

@Guard.register_validator("divisible_by")
class DivisibleByValidator(Validator):
    def validate(self, value: Any):
        if value % self.expected != 0:
            return f"{self.name} must be divisible by {self.expected}"
        return None

# Use your custom validator
@guard(number={"divisible_by": 5})
def process_number(number: int):
    return number * 2

process_number(10)  # OK
process_number(7)   # Raises GuardValidationError

🎯 Real-World Examples

User Registration

@guard(
    username={"length": (3, 20), "regex": r"^[a-zA-Z0-9_]+$"},
    email={"email": True},
    password={"length": (8, 128)},
    age={"gte": 13, "lte": 120}
)
def register_user(username: str, email: str, password: str, age: int):
    # Registration logic here
    return {"status": "success", "username": username}

API Pagination

@guard(
    page={"gte": 1, "type": int},
    per_page={"gte": 1, "lte": 100, "type": int}
)
def get_items(page: int = 1, per_page: int = 20):
    # Fetch paginated items
    return {"page": page, "per_page": per_page, "items": []}

Configuration Validation

@guard(
    config={
        "schema": {
            "database": dict,
            "api_key": str,
            "timeout": int
        },
        "keys": ["database", "api_key"]
    }
)
def initialize_app(config: dict):
    # App initialization
    pass

🚨 Error Handling

When validation fails, GuardValidationError is raised with detailed information:

from pyguard import guard, GuardValidationError

@guard(age={"gte": 18}, email={"email": True})
def register(age: int, email: str):
    pass

try:
    register(15, "invalid-email")
except GuardValidationError as e:
    print(e)
    # Output:
    # Validation failed for register:
    #   - age:
    #     • age must be at least 18
    #   - email:
    #     • email must be a valid email address
    
    # Access structured error data
    print(e.errors)
    # {'age': ['age must be at least 18'], 
    #  'email': ['email must be a valid email address']}

🔄 Optional Parameters

Use Optional type hints for parameters that can be None:

from typing import Optional

@guard(
    name={"length": (2, 50)},
    email={"email": True}
)
def update_profile(name: str, email: Optional[str] = None):
    pass

update_profile("John")  # OK - email is optional
update_profile("John", "john@example.com")  # OK

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

📞 Support

🗺️ Roadmap

  • Pydantic integration for schema validation
  • Async function support
  • Additional string validators (startswith, endswith, contains)
  • Collection validators (all_items, any_item, unique)
  • Custom error messages
  • Add support for Python 3.9

📊 Changelog

See CHANGELOG.md for version history.


Made with ❤️ by Mateusz Jasinski & Tadow Dev

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyguard_args-0.1.0.tar.gz (19.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyguard_args-0.1.0-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file pyguard_args-0.1.0.tar.gz.

File metadata

  • Download URL: pyguard_args-0.1.0.tar.gz
  • Upload date:
  • Size: 19.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for pyguard_args-0.1.0.tar.gz
Algorithm Hash digest
SHA256 46ac03a67e5ccfae539893a75658de508e388e087711a8a81329c64aeb8b74ef
MD5 5b85c30f3546bdd1b2755ac318f1f9fa
BLAKE2b-256 36eae5352c5b17d5e6e7503e33b43292208ae40d0528da760aedc5659893992a

See more details on using hashes here.

File details

Details for the file pyguard_args-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyguard_args-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for pyguard_args-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0cd6eeb501d8d3e6ee9bd5e6ae3a6cf7136d304e8b34d6f0261d85f0de9f7e81
MD5 b52aace4180251411e332ac246983a27
BLAKE2b-256 59ba6357f310c42b1a9864e012e19fa9755929e5d9e6b34e38b48a214ffd3341

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page