Photo by Arnold Francisca on Unsplash
To translate an AWS CDK TypeScript app to Python, create a Python CDK project, pin the same aws-cdk-lib version, copy the cdk.json context, and rewrite each construct with snake_case names and props as keyword arguments. Keep every construct ID and stack ID identical so the logical IDs don’t change. You’re done when cdk diff against the deployed stack reports no differences.
This guide is for teams that run a deployed CDK app in TypeScript and want it in Python, usually because the people who own it now write Python every day. The goal isn’t new infrastructure. It’s the same CloudFormation template produced by different source code, so the switch is invisible to AWS. If the resources are still managed by Terraform, see how to migrate a Terraform stack to AWS CDK TypeScript first; the same import steps work for a Python app.
You’ll get the project setup, a translation table for the patterns that trip people up, a worked stack in both languages, and a verification step that proves nothing will be replaced. We tested the example by synthesizing both versions with the same aws-cdk-lib release: the two templates were byte-for-byte identical.
What must stay identical when you translate an AWS CDK TypeScript app to Python
CloudFormation identifies each resource by its logical ID, and CDK derives logical IDs from the construct path: the stack ID, each parent construct ID, and a hash. Change any ID and CloudFormation sees a delete plus a create. For a bucket or a table, that’s data loss or a failed deploy. So these must match exactly:
- Stack IDs passed to
new MyStack(app, "ThumbnailStack"), and any explicitstackName. - Construct IDs, the second argument of every construct, including those inside your own custom constructs.
- The
aws-cdk-libversion. Different versions can synthesize slightly different templates. - The
contextblock incdk.json. Feature flags change synthesized output, and a freshcdk initwrites the flags for the new version, not the ones your app was built with. - Asset directories. Lambda code and Docker contexts must have the same content, because the asset hash is part of the template.
- Environment: the same account and region per stack.
Everything else, variable names, file layout, helper functions, can change freely.
Set up the Python project
- Create the project in a new folderRun
cdk init app --language pythonin an empty directory. Hyphens in the folder name become underscores in the package name. - Activate the virtual environment
source .venv/bin/activateon macOS and Linux,.venv\Scripts\activateon Windows. Do this every time you work on the project. - Pin the same library versionSet
aws-cdk-libinrequirements.txtto the exact version in the TypeScript app’spackage.json, then runpython -m pip install -r requirements.txt. - Copy the contextReplace the
contextobject in the newcdk.jsonwith the one from the TypeScript app. Keep the newappcommand. - Copy asset foldersMove
lambda/and any other asset paths with the same relative location, soCode.from_asset("lambda")resolves the same files.
mkdir thumbnail && cd thumbnail
cdk init app --language python
source .venv/bin/activate
# Match the TypeScript app's version, e.g. "aws-cdk-lib": "2.271.0" in package.json
sed -i.bak 's/^aws-cdk-lib==.*/aws-cdk-lib==2.271.0/' requirements.txt
python -m pip install -r requirements.txt
cp -r ../thumbnail-ts/lambda ./lambda
The part of cdk.json that changes is the app command. TypeScript projects run the entry file through a TypeScript runner; the Python template runs app.py:
{
"app": "python3 app.py",
"context": {
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-lambda:recognizeLayerVersion": true
}
}
The flags above are examples; paste the full context object from your TypeScript cdk.json. cdk init fills in the interpreter name it finds, so on Windows the command is often python app.py instead.
TypeScript to Python naming rules for CDK code
The Python bindings are generated from the same TypeScript library, so every class and property exists in both. The AWS guide to working with the CDK in Python documents the idioms; these are the ones you’ll use on every line:
| TypeScript | Python | Note |
|---|---|---|
import * as s3 from "aws-cdk-lib/aws-s3" |
from aws_cdk import aws_s3 as s3 |
Module names use underscores |
lambda module alias |
lambda_ |
lambda is a Python keyword |
constructor(scope, id, props) |
__init__(self, scope, construct_id, **kwargs) |
Avoid shadowing Python’s id() |
this |
self |
Always the first argument to a construct |
{ memorySize: 512 } |
memory_size=512 |
Props become keyword arguments |
bucket.bucketName, grantReadWrite() |
bucket.bucket_name, grant_read_write() |
camelCase to snake_case |
Code.fromAsset() |
Code.from_asset() |
Static methods too |
Nested object literal { id: "x", ... } |
s3.LifecycleRule(id="x", ...) |
Structs become classes with keyword arguments |
s3.BlockPublicAccess.BLOCK_ALL |
s3.BlockPublicAccess.BLOCK_ALL |
Enums and constants keep their names |
interface MyProps extends StackProps |
Keyword-only argument plus **kwargs |
Pass only **kwargs to super().__init__ |
class X implements IAspect |
@jsii.implements(IAspect) |
Decorator instead of the keyword |
undefined |
None |
Missing values |
Construct IDs are strings, so they are not renamed. "ResizeFunction" stays "ResizeFunction", even though the Python variable holding it becomes resize_fn.
Worked example: the same stack in both languages
A stack with a versioned bucket, a lifecycle rule driven by a custom prop, a Lambda function and a grant. This is the TypeScript original:
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";
export interface ThumbnailStackProps extends cdk.StackProps {
readonly retentionDays: number;
}
export class ThumbnailStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ThumbnailStackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, "ImagesBucket", {
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
id: "expire-old-versions",
noncurrentVersionExpiration: cdk.Duration.days(props.retentionDays),
},
],
});
const resizeFn = new lambda.Function(this, "ResizeFunction", {
runtime: lambda.Runtime.PYTHON_3_12,
handler: "index.handler",
code: lambda.Code.fromAsset("lambda"),
memorySize: 512,
timeout: cdk.Duration.seconds(30),
environment: { BUCKET_NAME: bucket.bucketName },
});
bucket.grantReadWrite(resizeFn);
new cdk.CfnOutput(this, "BucketName", { value: bucket.bucketName });
}
}
const app = new cdk.App();
new ThumbnailStack(app, "ThumbnailStack", {
retentionDays: 30,
env: { account: "123456789012", region: "us-east-1" },
});
And the Python translation. The custom retentionDays prop becomes a keyword-only argument, and everything the base Stack understands (like env) passes through **kwargs:
from aws_cdk import CfnOutput, Duration, Stack
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_s3 as s3
from constructs import Construct
class ThumbnailStack(Stack):
def __init__(self, scope: Construct, construct_id: str, *, retention_days: int, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
bucket = s3.Bucket(
self,
"ImagesBucket",
versioned=True,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
lifecycle_rules=[
s3.LifecycleRule(
id="expire-old-versions",
noncurrent_version_expiration=Duration.days(retention_days),
)
],
)
resize_fn = lambda_.Function(
self,
"ResizeFunction",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="index.handler",
code=lambda_.Code.from_asset("lambda"),
memory_size=512,
timeout=Duration.seconds(30),
environment={"BUCKET_NAME": bucket.bucket_name},
)
bucket.grant_read_write(resize_fn)
CfnOutput(self, "BucketName", value=bucket.bucket_name)
#!/usr/bin/env python3
import aws_cdk as cdk
from thumbnail.thumbnail_stack import ThumbnailStack
app = cdk.App()
ThumbnailStack(
app,
"ThumbnailStack",
retention_days=30,
env=cdk.Environment(account="123456789012", region="us-east-1"),
)
app.synth()
Both versions produce the same four resources with the same logical IDs: ImagesBucket1E86AFB2, ResizeFunction84EF4A77, ResizeFunctionServiceRoleA7F9E221 and ResizeFunctionServiceRoleDefaultPolicyC0B849C6. The role and its policy are created inside the lambda.Function construct, so their IDs match automatically once the parent ID matches.
For a larger app, the free AWS CDK TypeScript to Python converter does this rewrite per file. It keeps construct IDs as written, but check every one against the table above, and see the free converter limits per minute and per day before feeding it a whole repository. Going the other way later is covered by the CDK Python to TypeScript converter.
Verify the translation with cdk synth and cdk diff
Check twice: once locally without AWS, once against the deployed stack.
# 1. Local: compare the synthesized templates (jq -S sorts keys)
(cd ../thumbnail-ts && npx cdk synth ThumbnailStack > /dev/null)
cdk synth ThumbnailStack > /dev/null
diff <(jq -S . ../thumbnail-ts/cdk.out/ThumbnailStack.template.json) \
<(jq -S . cdk.out/ThumbnailStack.template.json) && echo "templates match"
# 2. Against AWS: compare the Python app with what is deployed
cdk diff ThumbnailStack
The local diff should print templates match. If it doesn’t, the output tells you which property or logical ID differs, and it’s almost always a renamed construct ID, a missed prop, or a context flag. The cdk diff run should report no differences for your resources. If the only change is to AWS::CDK::Metadata, that’s CDK’s version-reporting resource, not your infrastructure. Only then run cdk deploy from the Python project, and archive the TypeScript app so nobody deploys the same stack from both.
Permissions needed
The translation itself needs no AWS access; cdk synth runs locally unless your app uses context lookups such as Vpc.from_lookup. cdk diff needs to read the deployed template, through your credentials or the CDK bootstrap roles. The first cdk deploy from Python uses the same bootstrap deploy role the TypeScript pipeline used, so no new IAM is required. Grants like grant_read_write generate the same policy statements in both languages; if you want to audit them, the checklist to review a generated IAM policy for least privilege works on the synthesized template. For the Lambda handler’s own code, the IAM policy generator for Python code drafts a policy from the calls it makes.
Troubleshooting common translation mistakes
ModuleNotFoundError: No module named 'aws_cdk'. The virtual environment isn’t active, or you installed into another interpreter. Activate.venvand reinstall.cdk diffwants to replace a resource. A construct ID changed, often from an automatic rename to snake_case. IDs are strings; copy them exactly.- Every resource shows small property changes. The
contextflags oraws-cdk-libversion differ from the TypeScript app. TypeErrorabout an unexpected keyword argument. A custom prop reachedsuper().__init__. Take it as a keyword-only argument and pass only**kwargsup.- A runtime type error from the jsii layer. Python doesn’t enforce types, so a single value where a list is expected, or an L1
Cfn*object passed to an L2 construct, fails at synth time. The AWS guide recommends runningmypyorpyrightonapp.py. - The Lambda asset hash changed. The
lambda/folder has different content, such as a stray__pycache__or a lock file. Copy it from the TypeScript repository as-is.
After the first deploy from Python, if something in the account doesn’t behave as before, the guide to troubleshoot AWS infrastructure with an AI CLI shows how to question live resources directly, and for the function specifically, investigating Lambda errors with CloudWatch covers the logs and metrics to check.
What the converter can and can’t do
The free AWS SDK and IaC code converters rewrite source code. They don’t run cdk synth, see your cdk.json, or compare templates, so the verification step above is always yours. Output flags spots with no direct equivalent in comments, and it must be reviewed and tested. Before pasting infrastructure code, read what happens to code you paste into an AI converter. If you’re converting many files, a free ChatWithCloud account with higher converter limits raises the caps to 12 per minute and 200 per day. The broader question of whether to use CDK at all is covered in CDK vs Terraform for AWS stacks you already run.
Frequently asked questions
Will translating CDK from TypeScript to Python replace my resources?
Not if construct IDs, stack IDs, the library version and context flags match. Then the synthesized template is identical and CloudFormation has nothing to change.
Which Python version does AWS CDK need?
The AWS CDK developer guide lists Python 3.9 or later for Python CDK apps, plus Node.js for the CDK CLI. The aws-cdk-lib package on PyPI shows the current release and its supported Python versions.
Can I translate one stack at a time?
Yes. Each stack deploys independently, so you can move one stack to a Python app while the rest stay in TypeScript. Cross-stack references between the two apps need to go through exports or SSM parameters instead of object references.
Do CDK construct IDs have to be camelCase or snake_case in Python?
Neither. They’re plain strings. Keep whatever the TypeScript app used, or the logical IDs will change.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
