Skip to content

Type Checking in Data Validation

DodaTech Updated 2026-06-28 5 min read

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

Type checking validates that input values match expected data types before being processed, preventing type-related errors that crash applications.

What You'll Learn

By the end of this lesson, you will implement type validators for primitives, complex types, nullable types, and union types.

Why It Matters

Languages like Python are dynamically typed. A value that arrives as a string where an integer is expected causes a TypeError. Type checking catches this early.

Real-World Use

DodaZIP validates that file metadata fields match expected types: file_size must be int, file_name must be str, created_at must be a datetime.

Type Validator

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

class TypeChecker:
    @staticmethod
    def check(value: Any, expected_type: Type) -> Optional[str]:
        if value is None:
            return None

        if expected_type == int:
            if isinstance(value, bool):
                return "bool is not int"
            if isinstance(value, (int, float)):
                if isinstance(value, float) and not value.is_integer():
                    return "expected integer, got float"
                return None
            return "expected integer"

        if expected_type == float:
            if isinstance(value, (int, float)):
                return None
            return "expected float"

        if expected_type == str:
            if isinstance(value, str):
                return None
            return "expected string"

        if expected_type == bool:
            if isinstance(value, bool):
                return None
            return "expected boolean"

        if expected_type == list:
            if isinstance(value, list):
                return None
            return "expected array"

        if expected_type == dict:
            if isinstance(value, dict):
                return None
            return "expected object"

        if not isinstance(value, expected_type):
            return f"expected {expected_type.__name__}"

        return None

    @staticmethod
    def check_all(data: Dict, schema: Dict[str, Type]) -> List[Dict]:
        errors = []
        for field, expected_type in schema.items():
            value = data.get(field)
            error = TypeChecker.check(value, expected_type)
            if error and value is not None:
                errors.append({"field": field, "code": "type_error", "expected": expected_type.__name__})
            if error and value is None and expected_type != type(None):
                pass
        return errors

tc = TypeChecker()
print(tc.check("hello", int))  # string -> int
print(tc.check(42, int))       # int -> int
print(tc.check(True, int))     # bool -> int (returns error in strict mode)
print(tc.check([1, 2], list))  # list -> list
print(tc.check({"a": 1}, dict)) # dict -> dict

Expected output:

expected integer
None
bool is not int
None
None

Nullable and Union Types

# nullable_types.py
from typing import Any, Dict, List, Optional, Tuple, Type

class AdvancedTypeChecker:
    def __init__(self, strict_bool: bool = True):
        self.strict_bool = strict_bool

    def check(self, value: Any, type_spec: Any) -> Optional[str]:
        if isinstance(type_spec, tuple):
            return self._check_union(value, type_spec)

        if type_spec == "nullable_int":
            return None if value is None else self._check_primitive(value, int)

        if type_spec == "nullable_str":
            return None if value is None else self._check_primitive(value, str)

        return self._check_primitive(value, type_spec)

    def _check_union(self, value: Any, types: Tuple) -> Optional[str]:
        if value is None and type(None) not in types:
            return "required"

        for t in types:
            if t is type(None):
                continue
            if self._check_primitive(value, t) is None:
                return None

        names = [t.__name__ for t in types if t is not type(None)]
        return f"expected {' or '.join(names)}"

    def _check_primitive(self, value: Any, expected: Type) -> Optional[str]:
        if expected == int and isinstance(value, bool) and self.strict_bool:
            return "expected integer"
        if isinstance(value, expected):
            return None
        return f"expected {expected.__name__}"

atc = AdvancedTypeChecker()
print(atc.check(None, (int, type(None))))  # nullable int -> None passes
print(atc.check("42", (int, str)))         # union int|str
print(atc.check(42, (int, str)))           # union int|str
print(atc.check(True, (int, type(None))))  # nullable int with bool
print(atc.check([], list))                 # plain list

Expected output:

None
None
None
expected integer
None

Custom Type Validator

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

class EmailType:
    @staticmethod
    def validate(value: Any) -> Optional[str]:
        if not isinstance(value, str):
            return "email must be a string"
        if "@" not in value or "." not in value.split("@")[-1]:
            return "invalid email format"
        return None

class PositiveInt:
    @staticmethod
    def validate(value: Any) -> Optional[str]:
        if not isinstance(value, int) or isinstance(value, bool):
            return "must be an integer"
        if value <= 0:
            return "must be positive"
        return None

class NonEmptyString:
    @staticmethod
    def validate(value: Any) -> Optional[str]:
        if not isinstance(value, str):
            return "must be a string"
        if value.strip() == "":
            return "cannot be empty"
        return None

class CustomTypeValidator:
    def __init__(self):
        self.fields: Dict[str, Any] = {}

    def add(self, field: str, type_validator: Any):
        self.fields[field] = type_validator

    def validate(self, data: Dict) -> List[Dict]:
        errors = []
        for field, validator in self.fields.items():
            value = data.get(field)
            if value is not None:
                error = validator.validate(value)
                if error:
                    errors.append({"field": field, "message": error})
        return errors

cv = CustomTypeValidator()
cv.add("email", EmailType)
cv.add("count", PositiveInt)
cv.add("title", NonEmptyString)

data = {"email": "bad", "count": -5, "title": ""}
errors = cv.validate(data)
for e in errors:
    print(f"  {e}")

Expected output:

  {'field': 'email', 'message': 'invalid email format'}
  {'field': 'count', 'message': 'must be positive'}
  {'field': 'title', 'message': 'cannot be empty'}

Common Mistakes

1. bool is int in Python

isinstance(True, int) returns True. Always check bool before int.

2. Not Handling None

None is a separate type. Required fields should reject None. Nullable fields should accept it.

3. Overly Strict Types

Accepting only int when float also makes sense. Accept string representations when appropriate.

4. No Type Coercion Before Type Check

HTTP values are strings. Coerce first, then type check.

5. Complex Type Logic in Validation Code

Use type hints and validation libraries instead of manual isinstance chains.

Practice Questions

1. Why is type checking important?

Type errors crash applications. Early type checking prevents NullPointerException and TypeError.

2. What is a union type?

A value that can be one of multiple types: int, float, or None.

3. How do you check for nullable types?

Accept the value if it matches the expected type OR is None.

4. What is the bool/int gotcha in Python?

bool is a subclass of int. isinstance(True, int) returns True. Check isinstance(v, bool) first.

Challenge

Build a type checking system that validates a product API: id (int), name (str), price (float), in_stock (bool), tags (list of strings), dimensions (nullable dict).

FAQ

Should I use isinstance or type()?

isinstance handles inheritance. Use isinstance for most cases.

What about generics like List[str]?

Check the outer type is list, then validate each element type separately.

How do I validate type of JSON data?

JSON parses to Python primitives (dict, list, str, int, float, bool, None). Check these types.

What is duck typing?

If it walks like a duck (has expected methods/attributes), treat it as a duck. Less common in validation.

Is type checking enough?

No. Type checking is one layer. Add format, range, and business rule validation.

Mini Project: Type Validator

# type_validator.py
from typing import Any, Dict, List

def type_check(schema: Dict[str, type], data: Dict) -> List[str]:
    errors = []
    for field, typ in schema.items():
        val = data.get(field)
        if val is not None and not isinstance(val, typ):
            errors.append(f"{field}: expected {typ.__name__}")
    return errors

s = {"name": str, "age": int, "active": bool}
print(type_check(s, {"name": "Alice", "age": "30", "active": True}))
print(type_check(s, {"name": "Alice", "age": 30, "active": True}))

Expected output:

['age: expected int']
[]

What's Next

You understand type checking. Next, learn format validation (email, URL), then range validation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro