Skip to content

Range Validation — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you'll learn about Range Validation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Range validation checks that numeric values, dates, strings, or collections fall within specified minimum and maximum boundaries.

What You'll Learn

By the end of this lesson, you will validate numeric ranges, date ranges, string lengths, collection sizes, and implement inclusive/exclusive boundaries.

Why It Matters

Without range validation, your API accepts age=-5 or price=999999999999. Boundary validation prevents absurd values that cause overflow, storage waste, or business logic errors.

Real-World Use

DodaTech's payment API validates amounts: min 0.50 (to cover processing fees), max 99999.99 (anti-fraud). Values outside are rejected.

Range Validator

# range_validator.py
from typing import Any, Dict, List, Optional, Union

class RangeValidator:
    @staticmethod
    def number(value: Any, min_val: Optional[float] = None,
               max_val: Optional[float] = None,
               inclusive: bool = True) -> Optional[str]:
        if not isinstance(value, (int, float)):
            return "Value must be a number"

        if inclusive:
            if min_val is not None and value < min_val:
                return f"Minimum value is {min_val}"
            if max_val is not None and value > max_val:
                return f"Maximum value is {max_val}"
        else:
            if min_val is not None and value <= min_val:
                return f"Value must be greater than {min_val}"
            if max_val is not None and value >= max_val:
                return f"Value must be less than {max_val}"

        return None

    @staticmethod
    def string_length(value: Any, min_len: Optional[int] = None,
                      max_len: Optional[int] = None) -> Optional[str]:
        if not isinstance(value, str):
            return "Value must be a string"
        if min_len is not None and len(value) < min_len:
            return f"Minimum {min_len} characters"
        if max_len is not None and len(value) > max_len:
            return f"Maximum {max_len} characters"
        return None

    @staticmethod
    def array_size(value: Any, min_items: Optional[int] = None,
                   max_items: Optional[int] = None) -> Optional[str]:
        if not isinstance(value, (list, tuple)):
            return "Value must be an array"
        if min_items is not None and len(value) < min_items:
            return f"Minimum {min_items} items"
        if max_items is not None and len(value) > max_items:
            return f"Maximum {max_items} items"
        return None

    @staticmethod
    def date_range(value: Any, min_date: Optional[str] = None,
                   max_date: Optional[str] = None) -> Optional[str]:
        from datetime import datetime
        if isinstance(value, str):
            try:
                value = datetime.strptime(value, "%Y-%m-%d")
            except ValueError:
                return "Invalid date format (YYYY-MM-DD)"

        if not isinstance(value, datetime):
            return "Value must be a date"

        if min_date:
            try:
                mindt = datetime.strptime(min_date, "%Y-%m-%d")
                if value < mindt:
                    return f"Date must be on or after {min_date}"
            except ValueError:
                pass

        if max_date:
            try:
                maxdt = datetime.strptime(max_date, "%Y-%m-%d")
                if value > maxdt:
                    return f"Date must be on or before {max_date}"
            except ValueError:
                pass

        return None

rv = RangeValidator()
print(f"Number 5 in [1,10]:  {rv.number(5, 1, 10)}")
print(f"Number -1 in [0,]:   {rv.number(-1, 0)}")
print(f"String 'ab' len 2-5: {rv.string_length('ab', 2, 5)}")
print(f"Array [] min 1:      {rv.array_size([], min_items=1)}")
print(f"Date 2024-01-01:     {rv.date_range('2024-01-01', '2023-01-01', '2024-12-31')}")
print(f"Number 10 excl max:  {rv.number(10, max_val=10, inclusive=False)}")

Expected output:

Number 5 in [1,10]:  None
Number -1 in [0,]:   Minimum value is 0
String 'ab' len 2-5: None
Array [] min 1:      Minimum 1 items
Date 2024-01-01:     None
Number 10 excl max:  Value must be less than 10

Multi-Field Range Validation

# multi_range.py
from typing import Any, Dict, List, Optional

class MultiRangeValidator:
    @staticmethod
    def validate_all(data: Dict, rules: Dict[str, Dict]) -> List[Dict]:
        errors = []
        for field, rule in rules.items():
            value = data.get(field)
            if value is None:
                continue

            if rule.get("type") == "int":
                min_v = rule.get("min")
                max_v = rule.get("max")
                if not isinstance(value, int) or isinstance(value, bool):
                    errors.append({"field": field, "code": "type"})
                else:
                    if min_v is not None and value < min_v:
                        errors.append({"field": field, "code": "min", "min": min_v})
                    if max_v is not None and value > max_v:
                        errors.append({"field": field, "code": "max", "max": max_v})

            elif rule.get("type") == "float":
                if not isinstance(value, (int, float)):
                    errors.append({"field": field, "code": "type"})
                else:
                    if rule.get("min") is not None and value < rule["min"]:
                        errors.append({"field": field, "code": "min", "min": rule["min"]})
                    if rule.get("max") is not None and value > rule["max"]:
                        errors.append({"field": field, "code": "max", "max": rule["max"]})

        return errors

rules = {
    "age": {"type": "int", "min": 13, "max": 150},
    "price": {"type": "float", "min": 0.50, "max": 99999.99},
    "quantity": {"type": "int", "min": 1},
}

print(MultiRangeValidator.validate_all({"age": 10, "price": 0.25, "quantity": 0}, rules))
print(MultiRangeValidator.validate_all({"age": 25, "price": 50.0, "quantity": 2}, rules))

Expected output:

[{'field': 'age', 'code': 'min', 'min': 13}, {'field': 'price', 'code': 'min', 'min': 0.5}, {'field': 'quantity', 'code': 'min', 'min': 1}]
[]

Date Range Business Rules

# date_business.py
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional

class DateBusinessRules:
    @staticmethod
    def future_date(value: str) -> Optional[str]:
        try:
            dt = datetime.strptime(value, "%Y-%m-%d")
            if dt <= datetime.now():
                return "Date must be in the future"
        except ValueError:
            return "Invalid date format"
        return None

    @staticmethod
    def within_days(value: str, days: int = 30) -> Optional[str]:
        try:
            dt = datetime.strptime(value, "%Y-%m-%d")
            if dt > datetime.now() + timedelta(days=days):
                return f"Date must be within {days} days from now"
        except ValueError:
            return "Invalid date format"
        return None

    @staticmethod
    def start_before_end(start: str, end: str) -> Optional[str]:
        try:
            start_dt = datetime.strptime(start, "%Y-%m-%d")
            end_dt = datetime.strptime(end, "%Y-%m-%d")
            if start_dt >= end_dt:
                return "Start date must be before end date"
        except ValueError:
            return "Invalid date format"
        return None

dbr = DateBusinessRules()
print(f"Future (tomorrow): {dbr.future_date('2028-01-01')}")
print(f"Past:              {dbr.future_date('2020-01-01')}")
print(f"Within 30d:        {dbr.within_days('2028-06-01', 30)}")
print(f"Start < End:       {dbr.start_before_end('2024-06-01', '2024-06-15')}")
print(f"Start >= End:      {dbr.start_before_end('2024-06-15', '2024-06-01')}")

Expected output:

Future (tomorrow): None
Past:              Date must be in the future
Within 30d:        Date must be within 30 days from now
Start < End:       None
Start >= End:      Start date must be before end date

Common Mistakes

1. Off-by-One in Inclusive/Exclusive

"Must be at least 18" (>= 18) vs "Must be older than 18" (> 18). Use inclusive for age.

2. No Upper Bound

Accepting any number allows overflow attacks. Always set a reasonable maximum.

3. Date Range Without Timezone

"2024-01-01" means different times in different timezones. Use UTC or include timezone.

4. Same Range for All Fields

Different fields need different ranges. Age (0-150) vs Quantity (0-10000) vs Amount (0-999999.99).

5. Not Validating Both Ends

Check both min and max. Only checking one side leaves the other unvalidated.

Practice Questions

1. What is range validation?

Checking that a value falls within specified minimum and maximum boundaries.

2. What is inclusive vs exclusive range?

Inclusive: value >= min and value <= max. Exclusive: value > min and value < max.

3. Why validate both lower and upper bounds?

Lower prevents absurdly small values. Upper prevents overflow or unrealistic values.

4. How do you validate date ranges?

Parse dates and compare with datetime operators. Validate start < end for date pairs.

Challenge

Build a range validation system for a hotel booking API: check-in date (future, within 1 year), check-out date (after check-in, within 30 days of check-in), guests (1-10), room count (1-5).

FAQ

What is the default for inclusive/exclusive?

Standard validation uses inclusive (>= and <=). Specify when exclusive is needed.

How do I validate percentage ranges?

Min 0, max 100. Integer or float depending on precision needs.

What about negative number validation?

If negative is invalid, set min=0. If allowed, set reasonable negative bounds.

How do I validate monetary ranges?

Use integers (cents) with min=1 and max=99999999 to avoid float precision issues.

Should I validate ranges on both client and server?

Yes. Client for UX, server for security. Both need consistent min/max values.

Mini Project: Range Checker

# range_checker.py
from typing import Any, Dict, List, Optional

def in_range(value: Any, minimum: Any, maximum: Any) -> Optional[str]:
    if value < minimum:
        return f"below minimum {minimum}"
    if value > maximum:
        return f"above maximum {maximum}"
    return None

print(in_range(5, 1, 10))
print(in_range(0, 1, 10))
print(in_range("2024-06-01", "2024-01-01", "2024-12-31"))

Expected output:

None
below minimum 1
None

What's Next

You understand range validation. Next, learn length validation, then regex validation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro