← All posts

Building Your First Provider Function

This is part 3 of a five-part series. Start with Part 1 — Your First Resource and Part 2 — Your First Data Source. Part 4 adds an ephemeral resource. Part 5 packages it as a distributable binary.

Get the code: all five parts live in provide-io/pyvider-tutorial. Clone and follow along:

git clone https://github.com/provide-io/pyvider-tutorial.git
cd pyvider-tutorial/part3-function
uv sync && uv run pyvider install && tofu init && tofu apply

In parts 1 and 2 we built a resource that manages servers and a data source that queries them. Now we’ll add a provider function — a pure Python callable that Terraform can invoke directly in expressions, with no state involved.

We’ll use it to generate a consistent server name from a prefix and environment, then feed that name into the resource from part 1.

What Makes a Function Different

Resource Data Source Function
Has state yes no no
Reads from backend yes (on refresh) yes no
Called in expressions no no yes
Side effects yes no no

Functions are pure: same inputs always produce the same output.

Add the Function

Create my_provider/names.py:

from pyvider.cty import CtyString
from pyvider.functions import (
    BaseFunction,
    FunctionParameter,
    FunctionReturnType,
    register_function,
)
from pyvider.schema import PvsSchema, a_str, s_function


@register_function("generate_name")
class GenerateNameFunction(BaseFunction):
    """Generate a standardized server name from a prefix and environment."""

    @classmethod
    def get_schema(cls) -> PvsSchema:
        return s_function(
            parameters=[
                a_str(description="Name prefix — e.g. 'web', 'db', 'app'"),
                a_str(description="Environment — e.g. 'prod', 'staging', 'dev'"),
            ],
            return_type=a_str(description="Generated name"),
        )

    def get_parameters(self) -> list[FunctionParameter]:
        return [
            FunctionParameter(name="prefix", type=CtyString()),
            FunctionParameter(name="env",    type=CtyString()),
        ]

    def get_return_type(self) -> FunctionReturnType:
        return FunctionReturnType(type=CtyString())

    async def call(self, prefix: str, env: str) -> str:
        return f"{prefix}-{env}"

Register It

Add the import to my_provider/__init__.py:

from pyvider.providers import BaseProvider, ProviderMetadata, register_provider
from pyvider.schema import PvsSchema, s_provider

import my_provider.server       # registers mycloud_server
import my_provider.server_info  # registers mycloud_server_info
import my_provider.names        # registers mycloud::generate_name


@register_provider("mycloud")
class MyCloudProvider(BaseProvider):
    def __init__(self) -> None:
        super().__init__(
            metadata=ProviderMetadata(
                name="mycloud",
                version="0.1.0",
                protocol_version="6",
            )
        )

    @classmethod
    def get_schema(cls) -> PvsSchema:
        return s_provider({})

The Complete Terraform Configuration

Now the full main.tf — using the function to name the server, then querying it back with the data source:

terraform {
  required_providers {
    mycloud = {
      source  = "example.com/tutorial/mycloud"
      version = "0.1.0"
    }
  }
}

provider "mycloud" {}

locals {
  server_name = provider::mycloud::generate_name("web", "prod")
  # evaluates to: "web-prod"
}

resource "mycloud_server" "web" {
  name = local.server_name
}

data "mycloud_server_info" "web" {
  server_id = mycloud_server.web.id
}

output "server_name"   { value = local.server_name }
output "server_id"     { value = mycloud_server.web.id }
output "server_status" { value = data.mycloud_server_info.web.status }

Run it:

pyvider install
terraform init
terraform apply

Expected outputs:

server_name   = "web-prod"
server_id     = "srv-001"
server_status = "running"

Required Methods on Every Function

Method Purpose
get_schema Declares parameter types and return type for Terraform
get_parameters Returns typed FunctionParameter list (used internally)
get_return_type Returns typed FunctionReturnType (used internally)
call The actual logic — receives native Python values, returns a native value

What’s in the Complete Provider

After all three parts, my_provider/ looks like this:

my_provider/
├── __init__.py        # provider class + component imports
├── server.py          # mycloud_server resource
├── server_info.py     # mycloud_server_info data source
└── names.py           # provider::mycloud::generate_name function

Each file is independent. Pyvider discovers all registered components automatically at startup via the decorator registry.


Next: Part 4 — Adding an Ephemeral Resource for short-lived credentials that never touch .tfstate.


Or jump to: