Back to AI App Dev Series

Gemini SDK Track Part 6: Function Calling & Tool Integration

May 24, 2026 Wasil Zafar 40 min read

Declare tools with type hints, handle strict response matching requirements, use multimodal function responses, execute parallel tool calls, and combine built-in tools with custom functions.

Table of Contents

  1. Declaring Tools
  2. The Function Calling Loop
  3. Strict Response Matching
  4. Multimodal Function Responses
  5. Parallel Tool Calls
  6. Combining Tools & Function Calling
What You’ll Learn: Deep Research is Gemini’s specialized mode for in-depth investigation — it automatically plans research strategies, executes multi-step searches, evaluates source quality, and synthesizes comprehensive reports. Think of it like hiring a research analyst: you give them a question, and they come back hours later with a thorough, well-sourced report.

1. Declaring Tools

Function calling enables Gemini models to interact with external systems — APIs, databases, file systems, or any executable code. You declare what tools are available, and the model decides when and how to invoke them based on the user’s request.

1.1 FunctionDeclaration Schema

The explicit approach uses types.FunctionDeclaration with a full JSON Schema definition:

from google import genai
from google.genai import types

client = genai.Client()

# Define a tool with explicit schema
weather_tool = types.Tool(function_declarations=[
    types.FunctionDeclaration(
        name="get_weather",
        description="Get the current weather conditions for a specified city. "
                    "Returns temperature, conditions, humidity, and wind speed.",
        parameters=types.Schema(
            type="OBJECT",
            properties={
                "city": types.Schema(
                    type="STRING",
                    description="The city name, e.g. 'London' or 'Tokyo'"
                ),
                "units": types.Schema(
                    type="STRING",
                    description="Temperature units: 'celsius' or 'fahrenheit'",
                    enum=["celsius", "fahrenheit"]
                )
            },
            required=["city"]
        )
    )
])

# Use the tool in a request
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="What's the weather in Paris right now?",
    config=types.GenerateContentConfig(tools=[weather_tool])
)

# Check if the model wants to call a function
part = response.candidates[0].content.parts[0]
if part.function_call:
    print(f"Function: {part.function_call.name}")
    print(f"Args: {part.function_call.args}")

1.2 From Python Functions

The SDK can also generate schemas automatically from Python function signatures with type hints:

from google import genai
from google.genai import types

client = genai.Client()

# Define tools as regular Python functions with type hints
def get_stock_price(ticker: str, exchange: str = "NYSE") -> dict:
    """Get the current stock price for a ticker symbol.
    
    Args:
        ticker: The stock ticker symbol (e.g., AAPL, GOOGL)
        exchange: The stock exchange (NYSE, NASDAQ, LSE)
    
    Returns:
        Dictionary with price, change, and volume
    """
    # This would call a real API in production
    return {"price": 185.42, "change": "+2.3%", "volume": "45M"}

def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
    """Convert an amount between two currencies.
    
    Args:
        amount: The amount to convert
        from_currency: Source currency code (e.g., USD, EUR, GBP)
        to_currency: Target currency code
    
    Returns:
        Dictionary with converted amount and exchange rate
    """
    return {"converted": amount * 0.92, "rate": 0.92}

# Pass Python functions directly — SDK infers the schema
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="How much is 500 USD in euros, and what's Apple's stock price?",
    config=types.GenerateContentConfig(
        tools=[get_stock_price, convert_currency]
    )
)

# The model may request one or both functions
for part in response.candidates[0].content.parts:
    if part.function_call:
        print(f"Call: {part.function_call.name}({part.function_call.args})")
Best Practice: Use descriptive docstrings on your Python functions. The SDK extracts them as the description field in the schema, which helps the model decide when to use each tool. Vague descriptions lead to incorrect tool selection.

2. The Function Calling Loop

Function calling is not a single request–response. It follows an agentic loop: the model requests a function call, you execute it, return the result, and the model either generates a final answer or requests another call.

2.1 Loop Pattern

from google import genai
from google.genai import types

client = genai.Client()

# Tool definition
def get_weather(city: str, units: str = "celsius") -> dict:
    """Get current weather for a city."""
    # Simulated API response
    weather_data = {
        "London": {"temp": 14, "condition": "Overcast", "humidity": 78},
        "Tokyo": {"temp": 26, "condition": "Sunny", "humidity": 55},
        "New York": {"temp": 22, "condition": "Partly cloudy", "humidity": 62},
    }
    data = weather_data.get(city, {"temp": 20, "condition": "Unknown", "humidity": 50})
    if units == "fahrenheit":
        data["temp"] = round(data["temp"] * 9/5 + 32)
    data["units"] = units
    return data

# Dispatcher — maps function names to implementations
tool_functions = {"get_weather": get_weather}

# Start the agentic loop
contents = [
    types.Content(role="user", parts=[
        types.Part(text="What's the weather in London and Tokyo? Compare them.")
    ])
]

while True:
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=contents,
        config=types.GenerateContentConfig(tools=[get_weather])
    )
    
    # Append model response to history
    contents.append(response.candidates[0].content)
    
    # Check if model wants to call functions
    function_calls = [
        part for part in response.candidates[0].content.parts
        if part.function_call
    ]
    
    if not function_calls:
        # No more function calls — model produced final text
        print("Final Answer:")
        print(response.text)
        break
    
    # Execute each function call and collect responses
    function_responses = []
    for part in function_calls:
        fc = part.function_call
        print(f"Executing: {fc.name}({fc.args})")
        
        # Call the actual function
        result = tool_functions[fc.name](**fc.args)
        
        function_responses.append(
            types.Part(function_response=types.FunctionResponse(
                name=fc.name,
                response=result
            ))
        )
    
    # Return all function results to the model
    contents.append(types.Content(role="user", parts=function_responses))

2.2 Sequential Tool Calls

Sometimes the model needs to call tools in sequence — using the result of one call to inform the next:

from google import genai
from google.genai import types

client = genai.Client()

def search_flights(origin: str, destination: str, date: str) -> dict:
    """Search for available flights between two airports."""
    return {
        "flights": [
            {"id": "FL123", "depart": "09:00", "arrive": "11:30", "price": 245},
            {"id": "FL456", "depart": "14:00", "arrive": "16:30", "price": 189},
        ]
    }

def book_flight(flight_id: str, passenger_name: str) -> dict:
    """Book a specific flight for a passenger."""
    return {"confirmation": "BK-78901", "status": "confirmed", "flight_id": flight_id}

tool_functions = {
    "search_flights": search_flights,
    "book_flight": book_flight
}

contents = [
    types.Content(role="user", parts=[
        types.Part(text="Find the cheapest flight from London to Paris tomorrow "
                       "and book it for John Smith.")
    ])
]

# The model will: 1) search flights, 2) pick cheapest, 3) book it
max_turns = 5
for turn in range(max_turns):
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=contents,
        config=types.GenerateContentConfig(
            tools=[search_flights, book_flight]
        )
    )
    
    contents.append(response.candidates[0].content)
    
    function_calls = [p for p in response.candidates[0].content.parts if p.function_call]
    
    if not function_calls:
        print(f"Final (turn {turn + 1}): {response.text}")
        break
    
    responses = []
    for part in function_calls:
        fc = part.function_call
        print(f"  Turn {turn + 1}: {fc.name}({fc.args})")
        result = tool_functions[fc.name](**fc.args)
        responses.append(types.Part(function_response=types.FunctionResponse(
            name=fc.name, response=result
        )))
    
    contents.append(types.Content(role="user", parts=responses))
Real-World Application

Due Diligence Automation

A venture capital firm uses Deep Research for pre-investment due diligence: market size analysis, competitor mapping, regulatory landscape, and technology risk assessment. What previously took an analyst 2 weeks now takes 4 hours, with comparable quality and better source coverage.

Deep ResearchVenture CapitalDue Diligence

3. Strict Response Matching

Gemini enforces strict matching between function calls and function responses. Every function call must receive exactly one corresponding function response, in the correct order.

3.1 Matching Rules

Strict Matching Rules:
  • Every function_call must have a matching function_response with the same name
  • Responses must appear in the same order as the calls
  • You cannot skip, reorder, or add extra function responses
  • When thinking is active, thought signatures from the model’s response MUST be preserved
  • Violating any rule returns a 400 Bad Request error
from google import genai
from google.genai import types

client = genai.Client()

def get_temperature(city: str) -> dict:
    """Get temperature for a city."""
    temps = {"Paris": 18, "Berlin": 15, "Rome": 24}
    return {"city": city, "temp_celsius": temps.get(city, 20)}

def get_population(city: str) -> dict:
    """Get population of a city."""
    pops = {"Paris": 2_161_000, "Berlin": 3_645_000, "Rome": 2_873_000}
    return {"city": city, "population": pops.get(city, 1_000_000)}

tool_functions = {"get_temperature": get_temperature, "get_population": get_population}

contents = [
    types.Content(role="user", parts=[
        types.Part(text="Compare Paris and Berlin: temperature and population.")
    ])
]

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[get_temperature, get_population])
)

# CORRECT: Match each function_call with its response IN ORDER
model_content = response.candidates[0].content
contents.append(model_content)

function_responses = []
for part in model_content.parts:
    if part.function_call:
        fc = part.function_call
        result = tool_functions[fc.name](**fc.args)
        # Name MUST match the function_call name exactly
        function_responses.append(
            types.Part(function_response=types.FunctionResponse(
                name=fc.name,  # Must match fc.name exactly
                response=result
            ))
        )

# All responses in one Content block, same order as calls
contents.append(types.Content(role="user", parts=function_responses))

final = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[get_temperature, get_population])
)
print(final.text)

3.2 Error Handling

When a function execution fails, return an error inside the function response rather than omitting it:

from google import genai
from google.genai import types

client = genai.Client()

def unreliable_api(query: str) -> dict:
    """An API that might fail."""
    raise ConnectionError("Service unavailable")

# When execution fails, still return a function_response with error info
def safe_execute(name: str, args: dict, func) -> types.Part:
    """Execute a function safely, returning errors as responses."""
    try:
        result = func(**args)
        return types.Part(function_response=types.FunctionResponse(
            name=name, response=result
        ))
    except Exception as e:
        # Return error as a valid function response — don't skip it!
        return types.Part(function_response=types.FunctionResponse(
            name=name,
            response={"error": str(e), "error_type": type(e).__name__}
        ))

# The model receives the error and can inform the user gracefully
# rather than causing a 400 validation error from a missing response

4. Multimodal Function Responses

Function responses can include not just text/JSON but also images, audio, and other media. The key requirement: multimodal data must be returned inside the function_response part, not as separate message parts.

Critical: Multimodal content (images, audio) returned from tools MUST be inside the function_response part. Do NOT append them as separate Part objects alongside the function response — this violates the matching protocol.
from google import genai
from google.genai import types
import base64

client = genai.Client()

def generate_chart(data_type: str, values: list) -> dict:
    """Generate a chart image from data values."""
    # In production, this would use matplotlib/plotly to create a chart
    # and return the image bytes
    import io
    try:
        import matplotlib.pyplot as plt
        
        fig, ax = plt.subplots(figsize=(8, 5))
        ax.bar(range(len(values)), values)
        ax.set_title(f"{data_type} Chart")
        
        buf = io.BytesIO()
        fig.savefig(buf, format='png')
        buf.seek(0)
        plt.close(fig)
        
        image_bytes = buf.getvalue()
        return {
            "chart_type": data_type,
            "data_points": len(values),
            "image_base64": base64.b64encode(image_bytes).decode()
        }
    except ImportError:
        return {"chart_type": data_type, "data_points": len(values), "status": "generated"}

# The function response contains the image data INSIDE the response dict
# The model can then describe or reference the chart in its final answer
tool_functions = {"generate_chart": generate_chart}

contents = [
    types.Content(role="user", parts=[
        types.Part(text="Create a bar chart of monthly sales: [45, 52, 38, 61, 55, 70]")
    ])
]

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[generate_chart])
)

contents.append(response.candidates[0].content)

# Execute and return multimodal result INSIDE the function_response
for part in response.candidates[0].content.parts:
    if part.function_call:
        fc = part.function_call
        result = tool_functions[fc.name](**fc.args)
        contents.append(types.Content(role="user", parts=[
            types.Part(function_response=types.FunctionResponse(
                name=fc.name,
                response=result  # Image data is INSIDE this response
            ))
        ]))

final = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[generate_chart])
)
print(final.text)

5. Parallel Tool Calls

When the model determines that multiple independent operations are needed, it can request parallel tool calls in a single response — multiple function_call parts in one turn.

from google import genai
from google.genai import types

client = genai.Client()

def get_weather(city: str) -> dict:
    """Get current weather for a city."""
    data = {
        "London": {"temp": 14, "condition": "Rainy"},
        "Tokyo": {"temp": 28, "condition": "Sunny"},
        "Sydney": {"temp": 19, "condition": "Windy"},
    }
    return data.get(city, {"temp": 20, "condition": "Unknown"})

def get_time(timezone: str) -> dict:
    """Get current time in a timezone."""
    times = {
        "Europe/London": "14:30",
        "Asia/Tokyo": "23:30",
        "Australia/Sydney": "01:30",
    }
    return {"timezone": timezone, "current_time": times.get(timezone, "12:00")}

tool_functions = {"get_weather": get_weather, "get_time": get_time}

contents = [
    types.Content(role="user", parts=[
        types.Part(text="What's the weather and current time in London, Tokyo, and Sydney?")
    ])
]

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[get_weather, get_time])
)

# Detect parallel calls — multiple function_call parts in one response
model_content = response.candidates[0].content
function_calls = [p for p in model_content.parts if p.function_call]

print(f"Model requested {len(function_calls)} parallel tool calls:")
for fc_part in function_calls:
    print(f"  → {fc_part.function_call.name}({fc_part.function_call.args})")

# Execute ALL calls and return ALL responses in matching order
contents.append(model_content)

function_responses = []
for fc_part in function_calls:
    fc = fc_part.function_call
    result = tool_functions[fc.name](**fc.args)
    function_responses.append(
        types.Part(function_response=types.FunctionResponse(
            name=fc.name,
            response=result
        ))
    )

# Return all responses in a single Content block
contents.append(types.Content(role="user", parts=function_responses))

# Model synthesizes all results into a coherent answer
final = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=contents,
    config=types.GenerateContentConfig(tools=[get_weather, get_time])
)
print(f"\nFinal Answer:\n{final.text}")
Parallel Execution Tip: Since parallel function calls are independent, you can execute them concurrently using asyncio.gather() or concurrent.futures to minimize latency. The results must still be returned in the same order as the requests.
from google import genai
from google.genai import types
import asyncio

client = genai.Client()

async def execute_parallel_calls(function_calls: list, tool_functions: dict) -> list:
    """Execute multiple function calls concurrently."""
    async def run_one(fc_part):
        fc = fc_part.function_call
        # In production, these would be async API calls
        result = tool_functions[fc.name](**fc.args)
        return types.Part(function_response=types.FunctionResponse(
            name=fc.name,
            response=result
        ))
    
    # Execute all calls concurrently
    tasks = [run_one(fc_part) for fc_part in function_calls]
    results = await asyncio.gather(*tasks)
    return list(results)

# Usage: responses = await execute_parallel_calls(function_calls, tool_functions)
print("Parallel execution pattern ready for async tool calls")

6. Combining Tools & Function Calling (Preview)

You can combine built-in tools (Google Search, Code Execution) alongside your custom function declarations. The model intelligently decides which tool to use based on the query.

from google import genai
from google.genai import types

client = genai.Client()

# Custom function
def calculate_mortgage(principal: float, annual_rate: float, years: int) -> dict:
    """Calculate monthly mortgage payment and total interest."""
    monthly_rate = annual_rate / 100 / 12
    num_payments = years * 12
    if monthly_rate == 0:
        monthly_payment = principal / num_payments
    else:
        monthly_payment = principal * (monthly_rate * (1 + monthly_rate)**num_payments) / \
                         ((1 + monthly_rate)**num_payments - 1)
    total_paid = monthly_payment * num_payments
    return {
        "monthly_payment": round(monthly_payment, 2),
        "total_interest": round(total_paid - principal, 2),
        "total_paid": round(total_paid, 2)
    }

# Combine built-in Google Search with custom function
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="What are current UK mortgage rates, and if I borrow £300,000 "
             "at that rate for 25 years, what's my monthly payment?",
    config=types.GenerateContentConfig(
        tools=[
            # Built-in: Google Search for current rates
            types.Tool(google_search=types.GoogleSearch()),
            # Custom: Mortgage calculator
            calculate_mortgage
        ]
    )
)

# The model may:
# 1. Use Google Search to find current rates
# 2. Use calculate_mortgage with the discovered rate
# 3. Synthesize both results into a final answer
for part in response.candidates[0].content.parts:
    if part.function_call:
        print(f"Tool used: {part.function_call.name}")
    elif part.text:
        print(f"Text: {part.text[:200]}...")
Tool Priority: When multiple tools can answer a query, the model selects based on specificity. Custom functions with precise descriptions are preferred for domain-specific tasks, while built-in tools handle general knowledge retrieval. You cannot explicitly force tool selection — rely on clear descriptions.
from google import genai
from google.genai import types

client = genai.Client()

def get_internal_inventory(product_id: str) -> dict:
    """Check internal warehouse inventory for a product by ID."""
    inventory = {"SKU-001": 45, "SKU-002": 0, "SKU-003": 128}
    qty = inventory.get(product_id, -1)
    return {"product_id": product_id, "in_stock": qty > 0, "quantity": qty}

# Combine: search for product info + check internal stock
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Look up reviews for the Sony WH-1000XM5 headphones, "
             "and check if we have SKU-001 in stock.",
    config=types.GenerateContentConfig(
        tools=[
            types.Tool(google_search=types.GoogleSearch()),
            get_internal_inventory
        ]
    )
)

# Model uses Google Search for reviews AND custom function for inventory
for part in response.candidates[0].content.parts:
    if part.function_call:
        print(f"Calling: {part.function_call.name}({part.function_call.args})")
Try It Yourself: Use Deep Research to investigate a complex topic: ‘Compare the approaches of 5 countries to AI regulation in 2025-2026.’ Analyze the research plan it generates, evaluate source quality in the results, and compare the output quality to a single-shot prompt on the same topic.

Next in the Gemini SDK Track

In Part 7: Built-in Tools: Search, Maps, URL & Code Execution, we’ll ground Gemini responses with Google Search, Google Maps, and URL context tools, use the code execution sandbox for calculations and data processing, and combine multiple built-in tools in a single request.