What Function Calling Is

Ma Mahalakshmi V Updated 16 Sep 2026
11 min read ·Lesson 31 of 224

The Problem: A Model That Can't Actually Do Anything

Every capability this course has covered so far — text generation, structured outputs, reading images and documents, generating images and speech — has one thing in common: the model reads input and produces output, and nothing in the outside world changes as a result. The model has no way to look up today's exchange rate, check a real inventory database, send an email, or run a calculation against live data. It can only work with what was included in the request and whatever it happens to know from training, which for a fast-moving fact (an account balance, a stock price, whether a specific flight is delayed) is not knowledge the model can have at all, no matter how capable it is.

Function calling (sometimes called "tool calling") is the mechanism that closes this gap. It lets you describe, as part of a request, a set of functions the model is allowed to ask to have called — and the model, when it decides one of those functions would help answer the current request, responds not with a final answer but with a structured request naming which function it wants called and with what arguments. Your own code then actually runs that function, and feeds the result back to the model, which can use it to produce a final answer or ask for another function call.

What Function Calling Is Not

A common misconception worth clearing up immediately: the model itself never executes any code. It cannot reach out to the internet, query a database, or run a Python function on your machine. Function calling only ever produces a structured description of a function call — a name and a set of arguments — as part of the model's response. Your application code is entirely responsible for interpreting that description, deciding whether to actually run anything, executing the real function if it does, and sending the result back. This distinction matters a great deal for security and control, which Lesson 5 covers in depth: because the model can only request a call, never make one directly, your code retains full control over what actually happens, including the ability to refuse a request, sanitize its arguments, or substitute a safer implementation.

A Minimal End-to-End Example

tools = [
    {
        "type": "function",
        "name": "get_current_temperature",
        "description": "Get the current temperature for a given city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "The city name, e.g. 'Boston'"},
            },
            "required": ["city"],
            "additionalProperties": False,
        },
    }
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What's the temperature in Boston right now?",
    tools=tools,
)

for item in response.output:
    if item.type == "function_call":
        print(f"Model wants to call: {item.name}")
        print(f"With arguments: {item.arguments}")

Running this produces something like Model wants to call: get_current_temperature and With arguments: {"city": "Boston"} — the model has recognized that answering the question requires information it cannot know on its own, and instead of guessing a plausible-sounding temperature (a real risk without function calling, since a model asked a factual question it can't answer will sometimes produce a confident but fabricated answer), it has requested the specific piece of information it needs, in a precise, machine-readable form.

Notice what has not happened yet: no temperature has actually been looked up, and no final answer has been produced. This first response only contains the model's request to call a function — completing the interaction requires your code to actually run get_current_temperature("Boston") (an operation with a real implementation that reaches an actual weather source, not something the model does), and then send that result back to the model in a follow-up call, which Lesson 3 covers as "the full loop."

The Structure of a Tool Definition

Each entry in the tools list follows a consistent shape worth understanding piece by piece, since Lesson 2 covers writing these schemas in much greater depth:

FieldPurpose
typeIdentifies this as a "function" tool, distinguishing it from other tool types (such as the built-in tools Unit 9 covers).
nameThe identifier the model will use to refer to this function when it wants to call it — this should match the actual function name in your code, by convention, though technically it just needs to be a consistent string your code recognizes.
descriptionA natural-language explanation of what the function does and when it's useful — this is the primary signal the model uses to decide whether to call this function at all, making it one of the most consequential pieces of text in the whole tool definition.
parametersA JSON Schema (the same schema format Unit 6 used for structured outputs) describing what arguments the function accepts, their types, which are required, and any constraints on their values.

The description field deserves particular attention: a vague description ("gets weather info") gives the model little basis for deciding when this function is relevant, while a precise one ("Get the current temperature for a given city, in Fahrenheit, using live weather data") tells the model both what the function does and, implicitly, when it should reach for it rather than answering from its own knowledge or asking a clarifying question instead.

Why the Model Doesn't Just Answer From Training Data

It's worth being explicit about why this mechanism exists rather than simply expecting the model to always know the answer or always ask a clarifying question when it doesn't. A model's training data has a cutoff, and even within that cutoff, it was never trained on your specific, private, or constantly-changing data — your company's current inventory levels, a customer's specific order history, today's currency exchange rate. Function calling is the general-purpose bridge between "things a language model is good at" (understanding a request, deciding what information would help answer it, synthesizing a final response in natural language) and "things a language model cannot inherently do" (accessing live, private, or precisely computed data). This division of labor — the model reasons and decides, your code executes and provides ground truth — is the conceptual core of function calling, and everything in this unit builds on it.

When the Model Chooses Not to Call a Function

Providing tools to a request does not force the model to use them. For a question the model can answer confidently from what it already knows, or a request that doesn't call for any of the provided functions, the model will typically just answer directly, exactly as it would without any tools defined at all.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What is the boiling point of water at sea level in Celsius?",
    tools=tools,  # the get_current_temperature tool from before
)

for item in response.output:
    if item.type == "message":
        print(item.content[0].text)
    elif item.type == "function_call":
        print(f"Function call requested: {item.name}")

Here, even though get_current_temperature is available, the model recognizes that the question is about a general physical fact it already knows reliably, not a live temperature reading, and answers directly with a message output item rather than requesting a function call. This is an important behavior to design around: application code that always assumes a response contains a function call (rather than checking the type of each output item, as the example does) will break the moment the model reasonably decides not to use a tool for a given input — checking item.type explicitly, as shown here, is the correct way to handle both possibilities.

Multiple Tools in One Request

A single request can offer the model several tools at once, letting it choose whichever is relevant — or several at once, or none — for a given input.

tools = [
    {
        "type": "function",
        "name": "get_current_temperature",
        "description": "Get the current temperature for a given city.",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
            "additionalProperties": False,
        },
    },
    {
        "type": "function",
        "name": "convert_currency",
        "description": "Convert an amount of money from one currency to another using current exchange rates.",
        "parameters": {
            "type": "object",
            "properties": {
                "amount": {"type": "number"},
                "from_currency": {"type": "string", "description": "Three-letter currency code, e.g. 'USD'"},
                "to_currency": {"type": "string", "description": "Three-letter currency code, e.g. 'EUR'"},
            },
            "required": ["amount", "from_currency", "to_currency"],
            "additionalProperties": False,
        },
    },
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input="How much is 100 US dollars in euros?",
    tools=tools,
)

Given this request, the model should recognize convert_currency as the relevant tool and leave get_current_temperature unused — the model is choosing among the available tools based on which one's description matches the intent behind the input, which is why distinct, non-overlapping descriptions across multiple tools matter: two tools with vague or overlapping descriptions make it harder for the model to reliably pick the right one, a topic Lesson 4 returns to when discussing multiple tools in more depth.

Function Calling Versus Structured Outputs

Unit 6 covered structured outputs (text.format with json_schema, or client.responses.parse() with a Pydantic model) as a way to get the model's final answer back in a specific, predictable shape. Function calling looks superficially similar — both involve a JSON Schema, and both produce structured, validated data — but the two solve different problems, and conflating them is a common early confusion worth resolving directly.

AspectStructured OutputsFunction Calling
PurposeShape the model's final answerLet the model request information or an action mid-conversation
Who acts on the schemaNothing — it's just the final response shapeYour code, which executes the requested function
Conversation flowSingle request, single structured responseRequest → function call → your code runs it → follow-up request with the result
Typical useExtracting data, classifying content, generating a form-filling answerLooking up live data, performing calculations, taking an action (sending an email, updating a record)

A practical rule of thumb: if the goal is "get the model's answer in a predictable shape I can parse," reach for structured outputs (Unit 6). If the goal is "let the model ask for information it doesn't have, or trigger an action outside the model itself," reach for function calling. The two can also be combined — a function's result can itself be used to inform a final structured-output response — but they answer different questions, and it's worth being clear on which one a given problem actually calls for before reaching for either.

Why Argument Schemas Need the Same Discipline as Unit 6's Schemas

The parameters schema in a tool definition is validated the same way Unit 6's json_schema output format is validated: the model is constrained to produce arguments matching the declared types, required fields, and additionalProperties: False restriction, rather than being free to invent an arbitrary argument shape. This matters because a function call whose arguments don't match what your actual Python function expects is a call your code cannot safely execute — if get_current_temperature expects a city string but the model were free to send a nested object or omit the field entirely, the calling code would need defensive parsing logic scattered everywhere a function call is handled, rather than being able to trust that a syntactically valid function call always has arguments in the expected shape.

tools = [
    {
        "type": "function",
        "name": "schedule_meeting",
        "description": "Schedule a meeting with a given title, start time, and duration in minutes.",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "start_time_iso": {"type": "string", "description": "ISO 8601 datetime, e.g. '2026-09-15T14:00:00'"},
                "duration_minutes": {"type": "integer", "minimum": 5, "maximum": 480},
            },
            "required": ["title", "start_time_iso", "duration_minutes"],
            "additionalProperties": False,
        },
    }
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Set up a 30-minute sync called 'Design Review' for tomorrow at 2pm.",
    tools=tools,
)

Adding constraints like minimum and maximum on duration_minutes here does real work beyond documentation: it narrows the space of arguments the model can validly produce, catching an unreasonable value (a meeting scheduled for 50,000 minutes, say) at the schema level rather than requiring schedule_meeting's own implementation to defend against every conceivable bad input the model might otherwise produce. This is the same principle Unit 6, Lesson 2 established for structured-output schemas — push as much validation as reasonably possible into the schema itself, since schema-level constraints are enforced before your code ever sees the arguments, catching a whole class of malformed input for free.

Common Mistakes

Assuming a function_call output item will always be present when tools are provided, rather than checking item.type on each item in response.output, which breaks the moment the model reasonably decides a question doesn't need any tool at all.

Writing a vague or generic function description, leaving the model with too little information to decide reliably whether and when the function is relevant to a given request.

Expecting the model to actually execute the function itself, rather than understanding that a function call is only ever a structured request that application code must interpret and act on.

Confusing function calling with structured outputs because both use JSON Schema, when the two serve genuinely different purposes — shaping a final answer versus requesting information or action mid-conversation.

Best Practices

Write clear, specific function descriptions that state both what the function does and, where relevant, when it should be used, since the description is the model's primary basis for deciding whether to call a function at all.

Always branch on item.type when processing response.output, handling both message and function_call (and any other item types a response might contain) rather than assuming one specific shape.

Keep each function's responsibility narrow and well-defined rather than building one large, multi-purpose function, since a narrowly scoped function with a precise description is easier for the model to select correctly than a broad one that could plausibly apply to many different requests.

Treat function calling and structured outputs as complementary, not interchangeable, choosing the one that matches whether the actual need is "get an answer in a specific shape" or "let the model ask for outside information or trigger an action."

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about What Function Calling Is and get answers drawn from it.

Signed-in readers only.