Skip to main content

A Python library for validating function arguments using decorators

Project description

pyguard-args

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-args

Requirements:

  • Python 3.10 or higher

🚀 Quick Start

Without guard - Manual validation requires verbose boilerplate:

import re

def register_user(age: int, email: str, username: str):
    # Validate age
    if age < 18:
        raise ValueError("age must be at least 18")
    if age > 120:
        raise ValueError("age must be at most 120")
    
    # Validate email
    email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if not re.match(email_pattern, email):
        raise ValueError("email must be a valid email address")
    
    # Validate username
    if len(username) < 3:
        raise ValueError("username must be at least 3 characters")
    if len(username) > 20:
        raise ValueError("username must be at most 20 characters")
    if not re.match(r"^[a-zA-Z0-9_]+$", username):
        raise ValueError("username must match pattern ^[a-zA-Z0-9_]+$")
    
    return f"User {username} registered successfully!"

With guard - Clean, declarative, and maintainable:

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")

Async support

from pyguard import guard

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

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

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

Custom error messages

from pyguard import guard

@guard(
    age={"gte": (18, "Age must be at least 18"), "lte": (120, "Age must be at most 120")},
    email={"email": (True, "Email must be a valid email address")},
    username={"length": (3, 20, "Username must be between 3 and 20 characters long"), "regex": (r"^[a-zA-Z0-9_]+$", "Username must match pattern ^[a-zA-Z0-9_]+$")}
)
def register_user(age: int, email: str, username: str):
    return f"User {username} registered successfully!"

📖 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

Uppercase and lowercase validation

@guard(
    name={"uppercase": True},
    code={"lowercase": True}
)
def create_account(name: 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

StartsWith Validation

@guard(
    username={"startswith": "admin"},
    code={"startswith": "ABC"}
)
def create_account(username: str, code: str):
    pass

EndsWith Validation

@guard(
    filename={"endswith": ".txt"},
    extension={"endswith": ".py"}
)
def process_file(filename: str, extension: str):
    pass

Contains Validation

@guard(
    name={"contains": "John"},
    code={"contains": "123"}
)
def find_user(name: str, code: str):
    pass

IP Address Validation

@guard(
    ip={"ip_address": True},
    ipv4={"ip_address": "ipv4"},
    ipv6={"ip_address": "ipv6"}
)
def process_ip(ip: str, ipv4: str, ipv6: str):
    pass

UUID Validation

@guard(
    uuid={"uuid": True},
    uuid4={"uuid": "uuid4"},
    uuid5={"uuid": "uuid5"}
)
def process_uuid(uuid: str, uuid4: str, uuid5: 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
  • Collection validators (all_items, any_item, unique)
  • Add support for Python 3.9
  • Performance optimization
  • Cross argument validation
  • File validators
  • Date/Time validators

📊 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.2.0.tar.gz (22.7 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.2.0-py3-none-any.whl (24.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyguard_args-0.2.0.tar.gz
  • Upload date:
  • Size: 22.7 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.2.0.tar.gz
Algorithm Hash digest
SHA256 9f6fcfcd56d5b383d0f03bfa5000a7f3b5b97adb5698809c28f21fd56d662ef9
MD5 880ba9485cfbd8f8a739e7b66e1d5d07
BLAKE2b-256 4cf4426627e0a17718da28229d183c8d7ce85ac9c106a85ddb3bd0d3286b3070

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pyguard_args-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 24.8 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a01a62d070437fffac4499c67022085638a3f624331fa338a6c601655441ec16
MD5 f9339bc90385e395519d92e7143456f4
BLAKE2b-256 49a8a43d3deba414d13db74499e1402d25f95d2ac674323e8fc884727b9f8c03

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