Skip to content

Getting started

Using a coding agent?

Give it the agent quickstart, a single markdown page of the patterns to adopt and the rules to follow.

Install

uv add cfn-typed          # the runtime pieces and the cfn-typegen command
uv add --dev ty           # the type checker this project is tested with

Python 3.14 or later is required. YAML output uses ruamel.yaml, which is installed with it.

Generate the types you use

The package ships no resource types. You generate the ones your templates need into your own project:

cfn-typegen --out cfn_types AWS::SQS::Queue 'AWS::Lambda::*' AWS::IAM::Role

Not sure of a name? cfn-typegen search queue lists matching types.

Or point it at a template that already exists and it emits exactly the types that template uses:

cfn-typegen --out cfn_types --template template.json

Each service becomes one module (cfn_types/sqs.py, cfn_types/lambda_.py, ...). Each run writes a clean set of modules and replaces only files it generated earlier. Whether to commit them or generate them in CI is up to you.

A minimal program

The whole program is one file: build the template as data, print it as JSON.

import json
from dataclasses import dataclass

import cfn_typed as cfn
from cfn_types import sqs


@dataclass(frozen=True)
class JobsConfig:
    visibility_timeout: int = 60


def build(config: JobsConfig = JobsConfig()) -> cfn.Template:
    params = cfn.Params()
    stage = params.string("Stage", default="dev", allowed_values=["dev", "test", "prod"])
    return {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Parameters": params.declarations(),
        "Resources": {
            "Jobs": sqs.queue(VisibilityTimeout=config.visibility_timeout, Tags=cfn.tags(Stage=stage)),
        },
        "Outputs": {"JobsUrl": {"Value": cfn.ref("Jobs")}},
    }


if __name__ == "__main__":
    print(json.dumps(build(), indent=2))

build is a pure function of its config, so tests can call it directly. The inputs that change the template's shape go in the config object (JobsConfig); the ones that only change a value, such as the stage, are CloudFormation parameters, so one rendered template serves every stage. The rendered YAML is in the book. Render and deploy:

python minimal.py > template.json
aws cloudformation deploy --template-file template.json --stack-name jobs-prod \
    --parameter-overrides Stage=prod

JSON or YAML

cfn.to_json(template) is json.dumps with a default indent. cfn.to_yaml(template) writes the same template as readable CloudFormation YAML:

print(cfn.to_yaml(build()))
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  Jobs:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 60
      Tags:
        - Key: Stage
          Value: !Ref Stage
Outputs:
  JobsUrl:
    Value: !Ref Jobs

Intrinsics use CloudFormation's short forms (!Ref, !GetAtt Jobs.Arn, !Sub, !Join, ...), keys keep the order you wrote them, lists are indented under their key, and multi-line strings such as UserData become | blocks. A list that is shared between resources (the tags in the examples) is repeated, never turned into a YAML anchor.

Nested short forms

CloudFormation does not allow a short form directly inside another one (!Base64 !Sub ...). When that happens to_yaml keeps the outer function in long form (Fn::Base64: !Sub ...). Functions with no short form, such as Fn::Length, also stay long. Parsing the YAML back gives exactly the template you started with; the test suite checks that round trip.

Check the rendered YAML in CI

Commit what your code renders, and let CI fail when the two disagree, so that every template change appears as a diff in the pull request:

from cfn_typed.testing import assert_matches_file

def test_jobs() -> None:
    assert_matches_file(build(), "rendered/jobs.yaml")

Run the tests with CFN_TYPED_UPDATE=1 to create or accept the files. The testing page has the CI workflow and the details. You can see a committed example under rendered examples.

Let the checker help

ty check

Anywhere a property takes a value you can pass either a literal or an intrinsic such as cfn.ref(...) or cfn.get_att(...): every generated property is Expr[T], meaning T or an intrinsic. Unknown names, wrong types and missing required properties are reported before any template exists. Keep cfn-lint in CI as well: types do not check values, such as whether a Lambda runtime exists.