> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ubiquex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Python Component

> Package reusable infrastructure as a local Python component with a typed schema and auto-detected runtime.

The native `.iac` component format (tutorial 18) is great for portability — it compiles to whatever runtime the consuming stack uses. But sometimes you need the full Python SDK: `pulumi_awsx` for opinionated VPC patterns, custom retry logic, dynamic attribute construction, or a provider that hasn't been mapped to the ubx type system yet. A local Python component gives you all of that while keeping the call site in `.iac` identical to any other component block. Drop a `__main__.py` and a `requirements.txt` into a subdirectory, and ubx auto-detects the Python runtime, merges the pip dependencies into the stack's `requirements.txt`, and emits a native Python `ComponentResource` class in the generated `__main__.py`. The consumer never knows what language the component is written in.

## What you'll learn

* The directory structure for a local Python component
* How ubx auto-detects the Python runtime from file presence
* How `requirements.txt` from the component is merged into the stack automatically
* How `schema.json` validates inputs at the call site before any code runs
* How to call a local Python component from a `.iac` stack

***

## Why this matters

<Info>
  `pulumi_awsx` provides high-level VPC, ECS, and EKS constructs that create dozens of child resources behind a single call. These constructs are only available in the Pulumi SDKs — they can't be expressed as raw `unit` blocks. A local Python component lets you wrap an `awsx.ec2.Vpc` call and expose its outputs as typed values consumed by other blocks in your stack.
</Info>

***

## The source code

<CodeGroup>
  ```hcl networking/stack.iac theme={"theme":"css-variables"}
  component "vpc" {
    source       = "./components/vpc"
    cidr_block   = "10.0.0.0/16"
    nat_strategy = "Single"
  }

  output "vpc_id" {
    value = component.vpc.vpc_id
  }

  output "private_subnet_ids" {
    value = component.vpc.private_subnet_ids
  }
  ```

  ```python networking/components/vpc/__main__.py theme={"theme":"css-variables"}
  from typing import Optional
  import pulumi
  import pulumi_awsx as awsx


  class VpcComponentArgs:
      def __init__(
          self,
          cidr_block: str = "10.0.0.0/16",
          nat_strategy: str = "Single",
      ):
          self.cidr_block = cidr_block
          self.nat_strategy = nat_strategy


  class VpcComponent(pulumi.ComponentResource):
      vpc_id: pulumi.Output[str]
      private_subnet_ids: pulumi.Output[list]

      def __init__(
          self,
          name: str,
          args: VpcComponentArgs,
          opts: Optional[pulumi.ResourceOptions] = None,
      ):
          super().__init__("ubx:component:Vpc", name, {}, opts)

          vpc = awsx.ec2.Vpc(
              f"{name}-vpc",
              cidr_block=args.cidr_block,
              nat_gateways=awsx.ec2.NatGatewayConfigurationArgs(
                  strategy=awsx.ec2.NatGatewayStrategy[args.nat_strategy],
              ),
              opts=pulumi.ResourceOptions(parent=self),
          )

          self.vpc_id = vpc.vpc_id
          self.private_subnet_ids = vpc.private_subnet_ids
          self.register_outputs({
              "vpc_id":             self.vpc_id,
              "private_subnet_ids": self.private_subnet_ids,
          })
  ```

  ```text networking/components/vpc/requirements.txt theme={"theme":"css-variables"}
  pulumi-awsx>=2.0.0,<3.0.0
  ```

  ```json networking/components/vpc/schema.json theme={"theme":"css-variables"}
  {
    "inputs": {
      "cidr_block":   { "type": "string" },
      "nat_strategy": { "type": "string", "default": "Single" }
    },
    "outputs": {
      "vpc_id":             { "type": "string" },
      "private_subnet_ids": { "type": "list(string)" }
    }
  }
  ```

  ```hcl networking/inputs.iac theme={"theme":"css-variables"}
  input "env" {
    type    = "string"
    default = "dev"
  }
  ```

  ```hcl ubx.iac theme={"theme":"css-variables"}
  ubx {
    name    = "networking"
    runtime = "python"
    stacks  = ["dev", "staging", "prod"]
  }

  backend "s3" {
    bucket = "my-state-bucket"
    region = "eu-west-1"
    key    = "${stack}/networking.tfstate"
  }
  ```
</CodeGroup>

***

## How it works

<Steps>
  <Step title="ubx auto-detects the Python runtime from file presence">
    When `source = "./components/vpc"` points at a directory containing `__main__.py` and `requirements.txt`, ubx identifies it as a Python component. No explicit runtime declaration is needed in the component directory — the presence of these files is the signal. If only `.iac` files are present, ubx falls back to the native IR pipeline.
  </Step>

  <Step title="requirements.txt is merged into the stack's dependencies">
    ubx reads `components/vpc/requirements.txt` and merges its entries into the stack-level `requirements.txt` written alongside `__main__.py` in `.ubx/`. Duplicate package names are de-duplicated; the stack's own constraint wins on version conflict. The result: running `pip install -r .ubx/requirements.txt` installs everything the stack and all its components need.
  </Step>

  <Step title="schema.json validates inputs before compilation">
    When `schema.json` is present, `ubx validate` checks that all inputs declared as required (no `default`) are provided at the call site, and that all `component.vpc.X` output references in the consuming stack exist in the schema's `outputs` map. Type mismatches in inputs and references to undeclared outputs become compile errors rather than runtime surprises.
  </Step>

  <Step title="ubx inlines the component class in the generated __main__.py">
    The compiler reads `components/vpc/__main__.py`, wraps it, and emits it at the top of the generated stack `__main__.py` alongside the call site instantiation. No packaging, no pip install of the component, no `import` from an external module — the class is self-contained in the build artifact.
  </Step>
</Steps>

***

## Plan output

```
● Compile   networking/ → .ubx/__main__.py
● Stack     dev · eu-west-1
● Packages  ready

────────────────────────────────────────────────────

+ component.vpc  will be created

    cidr_block    "10.0.0.0/16"
    nat_strategy  "Single"

    ↳ VPC · 6 subnets · Internet Gateway · NAT Gateway · Elastic IP · 6 route tables

────────────────────────────────────────────────────
  +31 to create    ~0 to change    -0 to destroy
────────────────────────────────────────────────────

  ubx apply --env dev
```

***

## What ubx generates

<CodeGroup>
  ```python networking/.ubx/__main__.py theme={"theme":"css-variables"}
  # Generated by ubx — do not edit
  from typing import Optional
  import pulumi
  import pulumi_awsx as awsx


  # ── inlined from ./components/vpc/__main__.py ──────────────────────────────

  class VpcComponentArgs:
      def __init__(self, cidr_block: str = "10.0.0.0/16", nat_strategy: str = "Single"):
          self.cidr_block = cidr_block
          self.nat_strategy = nat_strategy


  class VpcComponent(pulumi.ComponentResource):
      vpc_id: pulumi.Output[str]
      private_subnet_ids: pulumi.Output[list]

      def __init__(self, name: str, args: VpcComponentArgs, opts: Optional[pulumi.ResourceOptions] = None):
          super().__init__("ubx:component:Vpc", name, {}, opts)
          vpc = awsx.ec2.Vpc(
              f"{name}-vpc",
              cidr_block=args.cidr_block,
              nat_gateways=awsx.ec2.NatGatewayConfigurationArgs(
                  strategy=awsx.ec2.NatGatewayStrategy[args.nat_strategy],
              ),
              opts=pulumi.ResourceOptions(parent=self),
          )
          self.vpc_id = vpc.vpc_id
          self.private_subnet_ids = vpc.private_subnet_ids
          self.register_outputs({"vpc_id": self.vpc_id, "private_subnet_ids": self.private_subnet_ids})


  # ── stack ─────────────────────────────────────────────────────────────────

  config = pulumi.Config()
  env = config.get("env") or "dev"

  # component "vpc"
  vpc = VpcComponent("vpc", VpcComponentArgs(
      cidr_block="10.0.0.0/16",
      nat_strategy="Single",
  ))

  # output "vpc_id"
  pulumi.export("vpc_id", vpc.vpc_id)

  # output "private_subnet_ids"
  pulumi.export("private_subnet_ids", vpc.private_subnet_ids)
  ```

  ```text networking/.ubx/requirements.txt theme={"theme":"css-variables"}
  # Generated by ubx — do not edit
  pulumi>=3.0.0,<4.0.0
  pulumi-aws>=6.0.0,<7.0.0
  pulumi-awsx>=2.0.0,<3.0.0
  ```
</CodeGroup>

***

## Common mistakes

<Warning>
  `schema.json` is optional but strongly recommended. Without it, `ubx validate` skips input type-checking and output reference validation for this component. A missing required input or a typo in `component.vpc.vpd_id` (note the typo) becomes a Python `AttributeError` at apply time rather than a compile error.
</Warning>

<Tip>
  The `"ubx:component:Vpc"` type URN passed to `super().__init__()` appears in `pulumi stack` output and in `ubx state list`. Use a consistent naming scheme: `"ubx:component:YourComponentName"` in PascalCase. The Pulumi state will group child resources under this URN.
</Tip>

***

## When to use each component type

| Approach                                     | Use when                                                                                                                                              |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native `.iac` component                      | You want portability across TypeScript, Python, and Go runtimes. No external SDK calls needed.                                                        |
| Local Python component                       | You need `pulumi_awsx`, `pulumi_kubernetes` high-level constructs, or any Python library not available in raw `.iac`. Stack runtime must be `python`. |
| Registry component (`registry.ubiquex.io/…`) | You want versioning, semantic releases, and sharing across repositories via the Strata registry.                                                      |

***

## Run it

```bash theme={"theme":"css-variables"}
ubx validate              # checks schema.json inputs and output refs
ubx plan --env dev        # compiles and previews — shows component.vpc summary
ubx apply --env dev       # creates all 31 resources in correct order
```

***

## What you learned

<Check>A local Python component is auto-detected from `__main__.py` + `requirements.txt` — no explicit runtime declaration needed</Check>
<Check>`requirements.txt` from each component directory is merged into the stack's `requirements.txt` automatically</Check>
<Check>`schema.json` turns missing inputs and bad output references into compile errors via `ubx validate`</Check>
<Check>The component class is inlined in the generated `__main__.py` — no packaging or publishing step required</Check>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Local .iac component" href="/v1/tutorials/18-local-iac-component">
    The portable, runtime-agnostic alternative
  </Card>

  <Card title="Multi-stack dependency resolution" href="/v1/tutorials/59-multi-stack-dependency-resolution">
    Use component outputs across stacks with @name.output
  </Card>
</CardGroup>

***

<Info>
  Full runnable example: [github.com/ubiquex/ubx-examples/60-local-python-component](https://github.com/ubiquex/ubx-examples/tree/main/60-local-python-component)
</Info>
