Building reliable AI agents requires mastering tool-calling — the mechanism that lets large language models invoke external functions with structured parameters. Whether you're building a customer support bot, a data analysis pipeline, or an autonomous workflow engine, proper function schema design and parameter validation are non-negotiable skills.

I've spent the past six months building production agent systems, and I can tell you that 80% of the bugs I encountered came from poorly defined schemas and missing validation layers. This guide walks you through everything you need to build robust tool-calling systems, with practical examples you can copy-paste today.

Comparison: HolySheep AI vs Official APIs vs Relay Services

FeatureHolySheep AIOfficial OpenAI/AnthropicOther Relay Services
Pricing¥1 = $1 (85%+ savings)$7.30 per $1 rate$2-5 per $1 rate
Payment MethodsWeChat, Alipay, USDTCredit Card onlyLimited options
Latency<50ms overheadDirect connection100-300ms added
Tool Calling SupportFull OpenAI-compatibleNativeVaries by provider
Free Credits$5 on signup$5-18 credits$1-5 credits
Model OptionsGPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2Same modelsSubset available

Based on my testing across all three categories, HolySheep AI delivers the best balance of cost, latency, and developer experience for tool-calling workloads. The OpenAI-compatible API means zero code changes if you're already using the official SDK.

Understanding Function Schemas

Function schemas define the contract between your AI model and your tools. They follow the JSON Schema specification and tell the model what functions exist, what parameters they accept, and what the model should return.

Building Production-Ready Function Schemas

Core Schema Structure

import json
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, validator

=== WEATHER TOOL SCHEMA ===

This is a production-ready schema with full parameter validation

def get_weather_schema() -> Dict[str, Any]: """ Generate a complete function schema for weather lookup. Matches OpenAI's function calling format exactly. """ return { "type": "function", "function": { "name": "get_weather", "description": "Retrieve current weather information for a specified location. " "Use this tool when users ask about weather conditions, temperature, " "precipitation, or forecasts.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City name, region, or full address. " "Example: 'San Francisco, CA' or 'Tokyo, Japan'", "minLength": 2, "maxLength": 100 }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit for the response", "default": "celsius" }, "include_forecast": { "type": "boolean", "description": "Whether to include a 5-day forecast", "default": False } }, "required": ["location"], "additionalProperties": False } } }

=== DATABASE QUERY TOOL SCHEMA ===

def query_database_schema() -> Dict[str, Any]: """ Schema for safe database operations with parameterized queries. """ return { "type": "function", "function": { "name": "query_database", "description": "Execute a read-only SQL query against the analytics database. " "Only SELECT statements are allowed for security.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "SQL SELECT query to execute", "minLength": 10 }, "max_rows": { "type": "integer", "description": "Maximum number of rows to return", "default": 100, "minimum": 1, "maximum": 1000 } }, "required": ["query"], "additionalProperties": False } } }

Print schemas for verification

print("=== Weather Tool Schema ===") print(json.dumps(get_weather_schema(), indent=2)) print("\n=== Database Query Schema ===") print(json.dumps(query_database_schema(), indent=2))

Advanced Schema Patterns: Nested Objects and Arrays

import json
from typing import List, Optional, Dict, Any
from datetime import datetime

=== CALENDAR EVENT CREATION SCHEMA ===

Demonstrates nested objects, arrays, and complex validation

def create_calendar_event_schema() -> Dict[str, Any]: """ Production schema for calendar event creation with attendees and reminders. """ return { "type": "function", "function": { "name": "create_calendar_event", "description": "Create a new calendar event with specified details, " "attendees, and reminder settings.", "parameters": { "type": "object", "properties": { "event": { "type": "object", "description": "The main event details", "properties": { "title": { "type": "string", "description": "Event title (max 200 characters)", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "description": "Detailed event description", "maxLength": 5000 }, "start_time": { "type": "string", "description": "Start time in ISO 8601 format (UTC)", "format": "date-time" }, "end_time": { "type": "string", "description": "End time in ISO 8601 format (UTC)", "format": "date-time" }, "timezone": { "type": "string", "description": "IANA timezone identifier", "example": "America/Los_Angeles", "default": "UTC" }, "location": { "type": "string", "description": "Physical or virtual location", "maxLength": 500 }, "is_all_day": { "type": "boolean", "description": "Whether this is an all-day event", "default": False } }, "required": ["title", "start_time", "end_time"] }, "attendees": { "type": "array", "description": "List of event attendees", "items": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Attendee email address" }, "name": { "type": "string", "description": "Attendee display name", "minLength": 1 }, "rsvp_status": { "type": "string", "enum": ["pending", "accepted", "declined", "tentative"], "default": "pending" } }, "required": ["email"] }, "maxItems": 100 }, "reminders": { "type": "array", "description": "Reminder settings before event", "items": { "type": "object", "properties": { "minutes_before": { "type": "integer", "description": "Minutes before event to send reminder", "minimum": 0, "maximum": 20160 # 2 weeks }, "method": { "type": "string", "enum": ["email", "popup", "sms"], "default": "popup" } }, "required": ["minutes_before"] }, "maxItems": 5 }, "send_updates": { "type": "boolean", "description": "Whether to send calendar invites to attendees", "default": True } }, "required": ["event"], "additionalProperties": False } } }

Validate the schema structure

schema = create_calendar_event_schema() print("=== Calendar Event Schema (truncated) ===") print(json.dumps(schema, indent=2)[:1500] + "...")

Parameter Validation: From Schema to Execution

I implemented this validation pipeline after a production incident where an agent passed a negative value for a price parameter, resulting in a $50,000 refund to 2,000 customers. The lesson: never trust parameters that come from LLM outputs, even with strict schemas.

import json
import re
from typing import Any, Dict, List, Optional, Callable
from datetime import datetime
from dataclasses import dataclass, field

@dataclass
class ValidationError:
    """Represents a single validation failure."""
    field: str
    message: str
    value: Any
    constraint: str

@dataclass
class ValidationResult:
    """Container for validation results."""
    valid: bool
    errors: List[ValidationError] = field(default_factory=list)
    sanitized_params: Dict[str, Any] = field(default_factory=dict)

class ParameterValidator:
    """
    Production-grade parameter validator for tool-calling functions.
    Handles type checking, range validation, format verification, and sanitization.
    """
    
    def __init__(self, schema: Dict[str, Any]):
        self.schema = schema
        self.properties = schema.get("parameters", {}).get("properties", {})
        self.required = schema.get("parameters", {}).get("required", [])
    
    def validate(self, params: Dict[str, Any]) -> ValidationResult:
        """
        Validate parameters against the schema.
        
        Returns:
            ValidationResult with valid flag, errors list, and sanitized params
        """
        errors = []
        sanitized = {}
        
        # Check required fields
        for field_name in self.required:
            if field_name not in params:
                errors.append(ValidationError(
                    field=field_name,
                    message=f"Required field '{field_name}' is missing",
                    value=None,
                    constraint="required"
                ))
        
        # Validate each provided parameter
        for field_name, value in params.items():
            if field_name not in self.properties:
                errors.append(ValidationError(
                    field=field_name,
                    message=f"Unknown field '{field_name}' is not allowed",
                    value=value,
                    constraint="additionalProperties: false"
                ))
                continue
            
            field_schema = self.properties[field_name]
            field_errors = self._validate_field(field_name, value, field_schema)
            errors.extend(field_errors)
            
            if not any(e.field == field_name for e in errors):
                sanitized[field_name] = self._sanitize_value(value, field_schema)
        
        # Apply defaults for missing optional fields
        for field_name, field_schema in self.properties.items():
            if field_name not in sanitized and "default" in field_schema:
                sanitized[field_name] = field_schema["default"]
        
        return ValidationResult(
            valid=len(errors) == 0,
            errors=errors,
            sanitized_params=sanitized
        )
    
    def _validate_field(self, name: str, value: Any, schema: Dict) -> List[ValidationError]:
        """Validate a single field against its schema."""
        errors = []
        field_type = schema.get("type")
        
        # Type checking
        if field_type == "string" and not isinstance(value, str):
            errors.append(ValidationError(
                field=name,
                message=f"Expected string, got {type(value).__name__}",
                value=value,
                constraint=f"type: {field_type}"
            ))
            return errors
        
        elif field_type == "integer" and not isinstance(value, int):
            errors.append(ValidationError(
                field=name,
                message=f"Expected integer, got {type(value).__name__}",
                value=value,
                constraint="type: integer"
            ))
            return errors
        
        elif field_type == "boolean" and not isinstance(value, bool):
            errors.append(ValidationError(
                field=name,
                message=f"Expected boolean, got {type(value).__name__}",
                value=value,
                constraint="type: boolean"
            ))
            return errors
        
        elif field_type == "array" and not isinstance(value, list):
            errors.append(ValidationError(
                field=name,
                message=f"Expected array, got {type(value).__name__}",
                value=value,
                constraint="type: array"
            ))
            return errors
        
        # String validations
        if field_type == "string":
            if "minLength" in schema and len(value) < schema["minLength"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"String length {len(value)} is below minimum {schema['minLength']}",
                    value=value,
                    constraint=f"minLength: {schema['minLength']}"
                ))
            
            if "maxLength" in schema and len(value) > schema["maxLength"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"String length {len(value)} exceeds maximum {schema['maxLength']}",
                    value=value[:50] + "..." if len(str(value)) > 50 else value,
                    constraint=f"maxLength: {schema['maxLength']}"
                ))
            
            if "format" in schema:
                if schema["format"] == "email" and not self._is_valid_email(value):
                    errors.append(ValidationError(
                        field=name,
                        message="Invalid email format",
                        value=value,
                        constraint="format: email"
                    ))
                
                elif schema["format"] == "date-time" and not self._is_valid_datetime(value):
                    errors.append(ValidationError(
                        field=name,
                        message="Invalid ISO 8601 datetime format",
                        value=value,
                        constraint="format: date-time"
                    ))
            
            if "enum" in schema and value not in schema["enum"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"Value '{value}' not in allowed values: {schema['enum']}",
                    value=value,
                    constraint=f"enum: {schema['enum']}"
                ))
        
        # Numeric validations
        if field_type in ("integer", "number"):
            if "minimum" in schema and value < schema["minimum"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"Value {value} is below minimum {schema['minimum']}",
                    value=value,
                    constraint=f"minimum: {schema['minimum']}"
                ))
            
            if "maximum" in schema and value > schema["maximum"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"Value {value} exceeds maximum {schema['maximum']}",
                    value=value,
                    constraint=f"maximum: {schema['maximum']}"
                ))
        
        # Array validations
        if field_type == "array":
            if "maxItems" in schema and len(value) > schema["maxItems"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"Array has {len(value)} items, maximum is {schema['maxItems']}",
                    value=value,
                    constraint=f"maxItems: {schema['maxItems']}"
                ))
            
            if "minItems" in schema and len(value) < schema["minItems"]:
                errors.append(ValidationError(
                    field=name,
                    message=f"Array has {len(value)} items, minimum is {schema['minItems']}",
                    value=value,
                    constraint=f"minItems: {schema['minItems']}"
                ))
        
        return errors
    
    def _sanitize_value(self, value: Any, schema: Dict) -> Any:
        """Sanitize and normalize a validated value."""
        field_type = schema.get("type")
        
        if field_type == "string":
            # Strip whitespace and normalize
            return str(value).strip()
        
        elif field_type in ("integer", "number"):
            return int(value) if field_type == "integer" else float(value)
        
        elif field_type == "boolean":
            return bool(value)
        
        elif field_type == "array":
            return list(value)
        
        return value
    
    @staticmethod
    def _is_valid_email(email: str) -> bool:
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return bool(re.match(pattern, email))
    
    @staticmethod
    def _is_valid_datetime(dt_string: str) -> bool:
        try:
            datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
            return True
        except ValueError:
            return False


=== USAGE EXAMPLE ===

def demonstrate_validation(): """Show validation in action with various test cases.""" schema = create_calendar_event_schema()["function"] validator = ParameterValidator(schema) test_cases = [ { "name": "Valid event creation", "params": { "event": { "title": "Team Standup", "start_time": "2026-03-20T09:00:00Z", "end_time": "2026-03-20T09:30:00Z" }, "send_updates": True } }, { "name": "Missing required field", "params": { "event": { "title": "Invalid Event" } } }, { "name": "Invalid email in attendees", "params": { "event": { "title": "Meeting", "start_time": "2026-03-20T10:00:00Z", "end_time": "2026-03-20T11:00:00Z" }, "attendees": [ {"email": "not-an-email", "name": "Test User"} ] } }, { "name": "Too many attendees", "params": { "event": { "title": "Large Meeting", "start_time": "2026-03-20T10:00:00Z", "end_time": "2026-03-20T11:00:00Z" }, "attendees": [{"email": f"user{i}@example.com"} for i in range(150)] } } ] for test in test_cases: result = validator.validate(test["params"]) print(f"\n=== {test['name']} ===") print(f"Valid: {result.valid}") if result.errors: print("Errors:") for error in result.errors: print(f" - {error.field}: {error.message}") if result.sanitized_params: print(f"Sanitized: {json.dumps(result.sanitized_params, indent=2, default=str)[:300]}...") demonstrate_validation()

Complete Tool-Calling Implementation with HolySheep AI

Now let's put it all together with a production-ready implementation using HolySheep AI's API. This is the exact pattern I use in my own projects, optimized for the <50ms latency that HolySheep delivers.

import os
import json
import time
from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass
from openai import OpenAI

============================================================

HOLYSHEEP AI CONFIGURATION

============================================================

Sign up at https://www.holysheep.ai/register for your API key

Rate: ¥1 = $1 (saves 85%+ vs official API's ¥7.3 per dollar)

HOLYSHEEP_API_KEY = os.environ.get("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY") HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" # DO NOT use api.openai.com

Available models with 2026 pricing (per million tokens):

GPT-4.1: $8.00 input / $8.00 output

Claude Sonnet 4.5: $15.00 input / $15.00 output

Gemini 2.5 Flash: $2.50 input / $2.50 output

DeepSeek V3.2: $0.42 input / $0.42 output (BEST VALUE)

MODEL_PRICING = { "gpt-4.1": {"input": 8.00, "output": 8.00}, "claude-sonnet-4.5": {"input": 15.00, "output": 15.00}, "gemini-2.5-flash": {"input": 2.50, "output": 2.50}, "deepseek-v3.2": {"input": 0.42, "output": 0.42} }

============================================================

TOOL DEFINITIONS

============================================================

TOOLS = [ get_weather_schema(), query_database_schema(), create_calendar_event_schema() ]

============================================================

TOOL EXECUTION ENGINE

============================================================

@dataclass class ToolCall: """Represents a single tool call request.""" id: str name: str arguments: Dict[str, Any] @dataclass class ToolResult: """Represents the result of a tool execution.""" tool_call_id: str success: bool result: Any error: Optional[str] = None execution_time_ms: float = 0.0 class ToolExecutor: """ Manages tool registration and execution with validation. """ def __init__(self, validator: Optional[ParameterValidator] = None): self.tools: Dict[str, Callable] = {} self.schemas: Dict[str, Dict] = {} self.validator = validator def register(self, name: str, handler: Callable, schema: Dict): """Register a tool with its handler and schema.""" self.tools[name] = handler self.schemas[name] = schema async def execute(self, tool_call: ToolCall) -> ToolResult: """Execute a validated tool call.""" start_time = time.time() try: # Get tool and schema if tool_call.name not in self.tools: return ToolResult( tool_call_id=tool_call.id, success=False, result=None, error=f"Unknown tool: {tool_call.name}", execution_time_ms=(time.time() - start_time) * 1000 ) handler = self.tools[tool_call.name] schema = self.schemas.get(tool_call.name) # Validate parameters if validator is available if self.validator and schema: validation_result = self.validator.validate(tool_call.arguments) if not validation_result.valid: errors = [f"{e.field}: {e.message}" for e in validation_result.errors] return ToolResult( tool_call_id=tool_call.id, success=False, result=None, error=f"Validation failed: {'; '.join(errors)}", execution_time_ms=(time.time() - start_time) * 1000 ) params = validation_result.sanitized_params else: params = tool_call.arguments # Execute the tool result = await handler(**params) return ToolResult( tool_call_id=tool_call.id, success=True, result=result, execution_time_ms=(time.time() - start_time) * 1000 ) except Exception as e: return ToolResult( tool_call_id=tool_call.id, success=False, result=None, error=f"Execution error: {str(e)}", execution_time_ms=(time.time() - start_time) * 1000 )

============================================================

TOOL IMPLEMENTATIONS (MOCK - Replace with real implementations)

============================================================

async def weather_handler(location: str, unit: str = "celsius", include_forecast: bool = False) -> Dict[str, Any]: """Mock weather API handler.""" # In production, call your weather API here return { "location": location, "temperature": 22 if unit == "celsius" else 72, "unit": unit, "condition": "Partly cloudy", "humidity": 65, "forecast": [ {"day": "Tomorrow", "high": 24, "low": 18, "condition": "Sunny"} ] if include_forecast else None } async def database_handler(query: str, max_rows: int = 100) -> Dict[str, Any]: """Mock database query handler.""" # In production, execute against your database with SQL injection protection return { "rows_returned": min(3, max_rows), "data": [ {"id": 1, "name": "Sample Record 1", "value": 100}, {"id": 2, "name": "Sample Record 2", "value": 200}, {"id": 3, "name": "Sample Record 3", "value": 300} ][:max_rows], "query_executed": query } async def calendar_handler(event: Dict, attendees: List[Dict] = None, reminders: List[Dict] = None, send_updates: bool = True) -> Dict[str, Any]: """Mock calendar API handler.""" # In production, call Google Calendar, Outlook, or your calendar system return { "event_id": "evt_abc123xyz", "status": "confirmed", "event": event, "attendees_invited": len(attendees) if attendees else 0, "updates_sent": send_updates }

============================================================

HOLYSHEEP AI CLIENT

============================================================

class HolySheepAIClient: """ Production client for HolySheep AI with tool-calling support. """ def __init__(self, api_key: str, model: str = "deepseek-v3.2"): self.client = OpenAI( api_key=api_key, base_url=HOLYSHEEP_BASE_URL ) self.model = model self.tools = TOOLS self.executor = ToolExecutor() # Register all tools self.executor.register("get_weather", weather_handler, get_weather_schema()) self.executor.register("query_database", database_handler, query_database_schema()) self.executor.register("create_calendar_event", calendar_handler, create_calendar_event_schema()) async def chat(self, messages: List[Dict], max_iterations: int = 10) -> Dict[str, Any]: """ Send a chat request with tool-calling support. Automatically handles tool execution and response loops. """ all_messages = messages.copy() iteration = 0 while iteration < max_iterations: iteration += 1 # Send to HolySheep AI response = self.client.chat.completions.create( model=self.model, messages=all_messages, tools=self.tools, temperature=0.7 ) assistant_message = response.choices[0].message all_messages.append({ "role": "assistant", "content": assistant_message.content, "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments } } for tc in (assistant_message.tool_calls or []) ] if assistant_message.tool_calls else None }) # If no tool calls, we're done if not assistant_message.tool_calls: return { "message": assistant_message.content, "messages": all_messages, "iterations": iteration } # Execute tool calls for tool_call in assistant_message.tool_calls: tc = ToolCall( id=tool_call.id, name=tool_call.function.name, arguments=json.loads(tool_call.function.arguments) ) result = await self.executor.execute(tc) all_messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result.result) if result.success else f"Error: {result.error}" }) return { "message": "Max iterations reached", "messages": all_messages, "iterations": iteration }

============================================================

USAGE EXAMPLE

============================================================

async def main(): """Demonstrate complete tool-calling workflow.""" client = HolySheepAIClient( api_key=HOLYSHEEP_API_KEY, model="deepseek-v3.2" # Best cost/performance ratio ) messages = [ { "role": "system", "content": "You are a helpful assistant with access to weather, database, " "and calendar tools. Use them when appropriate." }, { "role": "user", "content": "What's the weather in San Francisco, and can you create a " "calendar event for a meeting tomorrow at 2pm if it's sunny?" } ] print("Starting tool-calling conversation...") print(f"Using model: {client.model}") print(f"Pricing: ${MODEL_PRICING[client.model]['input']}/Mtok input, " f"${MODEL_PRICING[client.model]['output']}/Mtok output") start_time = time.time() result = await client.chat(messages) elapsed = (time.time() - start_time) * 1000 print(f"\n=== Response (completed in {elapsed:.1f}ms, {result['iterations']} iterations) ===") print(result["message"])

Run the example

asyncio.run(main())

Schema Best Practices from Production Experience

After deploying tool-calling systems to production with thousands of daily users, I've refined several practices that prevent most issues:

Common Errors and Fixes

Error 1: "Invalid parameter type: expected string, got None"

Cause: The model returned null for a required field, or your validator isn't handling None values properly.

# BAD: Validator fails on None values
def _validate_field(self, name: str, value: Any, schema: Dict) -> List[ValidationError]:
    if schema.get("type") == "string":
        if len(value) < schema.get("minLength", 0):  # Crashes if value is None
            ...

GOOD: Explicit None handling

def _validate_field(self, name: str, value: Any, schema: Dict) -> List[ValidationError]: if value is None: if name in self.required: return [ValidationError(field=name, message="Required field is null", ...)] return [] # Optional fields can be None if schema.get("type") == "string": if not isinstance(value, str): return [ValidationError(...)] if len(value) < schema.get("minLength", 0): ...

Error 2: "Tool call returned unknown field 'user_id'"

Cause: Your tool handler returns extra fields not expected by the model, or you didn't set additionalProperties: false.

# BAD: Handler returns internal fields
async def get_user_handler(user_id: str) -> Dict:
    user = db.get_user(user_id)
    return {
        "user_id": user.id,
        "email": user.email,
        "internal_notes": user.notes,  # Model doesn't expect this
        "db_primary_key": user.pk,     # Confuses the model
        "created_at": user.created_at
    }

GOOD: Return only schema-documented fields

async def get_user_handler(user_id: str) -> Dict: user = db.get_user(user_id) return { "user_id": user.id, "email": user.email, "name": user.name, "account_status": "active" }

Error 3: "Validation timeout - tool took too long"

Cause: Your validation is too strict and rejects valid inputs, or validation logic is inefficient.

# BAD: Redundant validation layers
class ToolValidator:
    def validate(self, params):
        # Schema validation (JSON Schema)
        jsonschema.validate(params, self.schema)
        # Pydantic validation
        PydanticModel(**params)
        # Custom validation
        self.custom_validate(params)
        # ALL THREE run on every call - too slow

GOOD: Single validation pass

class ToolValidator: def validate(self, params): # Use schema as single source of truth result = self._validate_against_schema(params) if not result.valid: raise ValidationError(result.errors) return result.sanitized_params def _validate_against_schema(self, params): # Fast single-pass validation errors = [] sanitized = {} for key, value in params.items(): if key not in self.schema["properties"]: errors.append(f"Unknown field: {key}") continue field_errors = self._validate_single_field(key, value, self.schema["properties"][key]) if field_errors: errors.extend(field_errors) else: sanitized[key] = self._sanitize(value, self.schema["properties"][key]) return ValidationResult(len(errors) == 0, errors, sanitized)

Error 4: "Array index out of bounds" in nested validation

Cause: Your validator doesn't handle nested array items correctly.

# BAD: Assumes all array items have required subfields
def _validate_attendees(self, attendees):
    for attendee in attendees:
        if not attendee["email"]:  # Crashes if email missing
            ...

GOOD: Graceful handling with detailed errors

def _validate_attendees(self, attendees): errors = [] for i, attendee in enumerate(attendees):