Skip to main content
Lesson 4 of 14 · Function Calling and Tool Use

Define Functions in Python

Lesson 4 of 14 Updated November 2, 2026 Developer
On this page
  1. From Theory to Code
  2. Install the Mistral SDK
  3. Create Python Functions
  4. Write JSON Specifications
  5. Organize Functions in a Dictionary
  6. The tool_choice Option
  7. Key Takeaways

From Theory to Code

You now understand the five-step process and the JSON Schema structure. It’s time to write real Python code. In this lesson, you’ll create functions, draft their specifications, and prepare everything for the Mistral API—everything that needs to exist before making the first network call.

Install the Mistral SDK

Start by installing the official package:

pip install mistralai

Next, configure your API key. Best practice dictates that you never hardcode it in your source files. Instead, read it from an environment variable so you can safely commit your code to version control.

export MISTRAL_API_KEY="your-api-key"
import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

Create Python Functions

Let’s look at a worked example: a payment tracking system backed by a Pandas DataFrame. This is a classic example from the Mistral documentation, and it perfectly illustrates the concept—here, the DataFrame plays the role a database would in production.

import pandas as pd
import json

# Payment data
data = {
    "transaction_id": ["T1001", "T1002", "T1003", "T1004", "T1005"],
    "customer_id": ["C001", "C002", "C003", "C002", "C001"],
    "payment_amount": [125.50, 89.99, 350.00, 45.00, 210.75],
    "payment_date": ["2026-03-15", "2026-03-16", "2026-03-17", "2026-03-17", "2026-03-18"],
    "payment_status": ["paid", "paid", "pending", "paid", "failed"]
}
df = pd.DataFrame(data)

Next comes the code that actually accesses this data. These are standard Python functions; nothing in their implementation hints that an LLM will drive them.

def retrieve_payment_status(df: pd.DataFrame, transaction_id: str) -> str:
    """Retrieves the status of a payment by its ID."""
    if transaction_id in df["transaction_id"].values:
        status = df[df["transaction_id"] == transaction_id]["payment_status"].values[0]
        return json.dumps({"status": status})
    return json.dumps({"error": f"Transaction {transaction_id} not found"})


def retrieve_payment_date(df: pd.DataFrame, transaction_id: str) -> str:
    """Retrieves the date of a payment by its ID."""
    if transaction_id in df["transaction_id"].values:
        date = df[df["transaction_id"] == transaction_id]["payment_date"].values[0]
        return json.dumps({"date": date})
    return json.dumps({"error": f"Transaction {transaction_id} not found"})

Two implementation details deserve your attention. First, these functions always return a JSON string, never a raw Python object. This is the format the model expects as a tool result, so json.dumps() is non-negotiable. Second, they explicitly handle the case where a transaction isn’t found instead of raising an exception. This error, formatted as JSON, returns to the model just like any other result, allowing it to politely inform the user that transaction T9999 doesn’t exist. You’ll see later in the course why this reflex is essential.

Write JSON Specifications

Each function needs a specification so the model can understand it. Here you apply the structure from the previous lesson to the two functions above—note the strict correspondence between the name fields and the actual Python function names.

tools = [
    {
        "type": "function",
        "function": {
            "name": "retrieve_payment_status",
            "description": "Retrieves the status of a payment (paid, pending, failed) "
                           "based on the transaction ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "transaction_id": {
                        "type": "string",
                        "description": "Unique transaction identifier (e.g., T1001)"
                    }
                },
                "required": ["transaction_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "retrieve_payment_date",
            "description": "Retrieves the date a payment was made "
                           "based on the transaction ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "transaction_id": {
                        "type": "string",
                        "description": "Unique transaction identifier (e.g., T1001)"
                    }
                },
                "required": ["transaction_id"]
            }
        }
    }
]

Organize Functions in a Dictionary

The model returns a function name as a string. You need a way to map that string to the corresponding callable object without resorting to eval or a cascade of if statements. A dispatch dictionary solves this problem in three lines.

from functools import partial

# Bind the DataFrame to each function
available_functions = {
    "retrieve_payment_status": partial(retrieve_payment_status, df),
    "retrieve_payment_date": partial(retrieve_payment_date, df),
}

functools.partial addresses a specific, real-world difficulty. Your functions expect two arguments—the DataFrame and the ID—but the model provides only one. The model knows nothing about df, nor should it. partial pre-fills that first parameter once and for all. This same mechanism applies to database connections, HTTP clients, or authentication tokens. Dynamic invocation then becomes trivial.

function_name = "retrieve_payment_status"
function_params = {"transaction_id": "T1001"}

result = available_functions[function_name](**function_params)
# → '{"status": "paid"}'

The tool_choice Option

When sending these tools to the API, the tool_choice parameter decides how much freedom you give the model. Three values are possible.

# The model decides whether to use a tool or respond directly (default)
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

# Force the model to use a tool
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=tools,
    tool_choice="any"
)

# Prevent the model from using tools
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=tools,
    tool_choice="none"
)

The "auto" mode is most common in production for good reason: a payment tracking assistant should be able to say “hello” without triggering a database query. The model responds directly to general questions and uses functions only when relevant. A full lesson later in the course covers specific use cases for any and none.

Key Takeaways

  • Functions must return JSON strings—never raw Python objects
  • Use a dispatch dictionary to map function names to their implementations
  • functools.partial is ideal for pre-filling parameters like DB connections or DataFrames
  • tool_choice controls whether the model can or must use functions (auto, any, none)
  • Always handle error cases in your functions (invalid parameters, missing data)