| name | aspire-polyglot-python-apphost |
| description | Python AppHost patterns for Aspire 13.4.6+ polyglot orchestration |
| domain | aspire, python, orchestration |
| confidence | high |
| source | earned (13.4.6 migration) |
Context
Aspire 13.4.6 introduced a breaking change in the Python SDK: create_builder() now returns a context manager (DistributedApplicationBuilder) instead of a plain builder object. This change affects ALL Python AppHost projects.
The builder's internal _handle starts as None and is ONLY populated inside __enter__, which invokes the Aspire.Hosting/createBuilder RPC to the .NET host. Without the context manager, all builder capability calls fail with TypeError: Type mismatch for parameter 'builder': expected unknown, got unknown.
Patterns
1. Context Manager Entry Pattern (REQUIRED in 13.4.6+)
ALWAYS wrap the builder lifecycle in a with statement:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / ".aspire/modules"))
from aspire_app import create_builder
with create_builder() as builder:
cache = builder.add_redis("cache")
db = builder.add_postgres("pg").add_database("mydb")
app = builder.add_dockerfile("myapp", "./src")
app.with_reference(cache)
app.with_reference(db)
app.with_otlp_exporter()
app.with_external_http_endpoints()
app.with_http_endpoint(target_port=8080, env="PORT")
app_instance = builder.build()
app_instance.run()
2. Dependency Lock File (pylock.apphost.toml)
The Python guest runtime bootstraps its venv via uv pip sync pylock.apphost.toml. This file MUST exist at the sample root.
Location: samples/{sample-name}/pylock.apphost.toml (same directory as apphost.py)
Minimum valid content (for apphosts with no dependencies):
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.11"
Python version constraint: MUST be ">=3.11" to match .aspire/modules/pyproject.toml (which is generated at runtime). Do NOT pin to a specific patch version like ">=3.13.3".
When to use an empty lock: When the root requirements.txt is empty or contains only comments, and aspire_app.py has no external dependencies (it's pure stdlib), an empty PEP 751 lock is valid.
3. Sys.Path Configuration
The generated Python bindings live at .aspire/modules/aspire_app.py (13.4.6+ layout).
Standard sys.path setup:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / ".aspire/modules"))
from aspire_app import create_builder
Legacy layout (.modules/ — pre-13.4.6) is deprecated. Always use .aspire/modules/.
4. Common Builder Patterns
Redis cache:
cache = builder.add_redis("cache")
app.with_reference(cache)
PostgreSQL database:
postgres = builder.add_postgres("pg")
db = postgres.add_database("mydb")
app.with_reference(db)
Dockerfile-based app:
app = builder.add_dockerfile("app-name", "./src")
app.with_otlp_exporter()
app.with_external_http_endpoints()
app.with_http_endpoint(target_port=8080, env="PORT")
Examples
Complete Flask AppHost (flask-markdown-wiki)
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / ".aspire/modules"))
from aspire_app import create_builder
with create_builder() as builder:
cache = builder.add_redis("cache")
wiki = builder.add_dockerfile("wiki", "./src")
wiki.with_reference(cache)
wiki.with_otlp_exporter()
wiki.with_external_http_endpoints()
wiki.with_http_endpoint(target_port=8080, env="PORT")
app = builder.build()
app.run()
Complete Django AppHost (django-htmx-polls)
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / ".aspire/modules"))
from aspire_app import create_builder
with create_builder() as builder:
postgres = builder.add_postgres("pg")
pollsdb = postgres.add_database("pollsdb")
polls = builder.add_dockerfile("polls", "./src")
polls.with_reference(pollsdb)
polls.with_otlp_exporter()
polls.with_external_http_endpoints()
polls.with_http_endpoint(target_port=8080, env="PORT")
app = builder.build()
app.run()
Anti-Patterns
❌ OLD Pattern (13.2.4 and earlier — BREAKS in 13.4.6)
from aspire_app import create_builder
builder = create_builder()
cache = builder.add_redis("cache")
app = builder.build()
app.run()
Why this fails: __enter__ never runs, so _handle is None, and the .NET host rejects all builder calls.
❌ Missing pylock.apphost.toml
samples/myapp/
├── apphost.py
├── aspire.config.json
└── src/
└── ...
# ❌ NO pylock.apphost.toml — runtime bootstrap fails
Why this fails: The Python guest runtime calls uv pip sync pylock.apphost.toml to set up its venv. Without this file, the bootstrap fails.
❌ Pinned Python Version
requires-python = ">=3.13.3"
Why this fails: .aspire/modules/pyproject.toml (generated at runtime) specifies requires-python = ">=3.11". Pinning to a specific patch version creates a version conflict.
Correct:
requires-python = ">=3.11"
❌ Legacy Import Path
sys.path.insert(0, str(Path(__file__).parent / ".modules"))
from aspire import create_builder
Why this fails: The 13.4.6 binding layout moved from .modules/aspire.py → .aspire/modules/aspire_app.py.
Correct:
sys.path.insert(0, str(Path(__file__).parent / ".aspire/modules"))
from aspire_app import create_builder
Verification
Syntax check:
python3 -c "import ast; ast.parse(open('apphost.py').read()); print('OK')"
Runtime launch (only via coordinator on shared Docker host):
cd samples/myapp
aspire run
Cold-Start Ordering: wait_for() for Datastore Dependencies
Problem
App containers can start BEFORE their datastores are reachable on cold launches (when images must pull or containers must initialize). This causes DNS resolution failures or connection errors.
Example failure (django-htmx-polls on cold launch):
django.db.utils.OperationalError: could not translate host name "pg.dev.internal" to address
Root cause: .with_reference(db_resource) only injects the connection string — it does NOT establish startup ordering.
Solution
ALWAYS pair .with_reference() with .wait_for() when your app depends on a datastore:
postgres = builder.add_postgres("pg")
pollsdb = postgres.add_database("pollsdb")
polls = builder.add_dockerfile("polls", "./src")
polls.with_reference(pollsdb)
polls.wait_for(pollsdb)
polls.with_otlp_exporter()
polls.with_external_http_endpoints()
polls.with_http_endpoint(target_port=8080, env="PORT")
cache = builder.add_redis("cache")
wiki = builder.add_dockerfile("wiki", "./src")
wiki.with_reference(cache)
wiki.wait_for(cache)
wiki.with_otlp_exporter()
wiki.with_external_http_endpoints()
wiki.with_http_endpoint(target_port=8080, env="PORT")
When to Use wait_for()
Use .wait_for() when:
- Your app runs migrations at startup (Django
manage.py migrate, Alembic, Flyway)
- Your app connects to the datastore immediately on launch (no retry logic)
- Your app cannot gracefully handle "connection refused" or DNS failures
Applies to:
- PostgreSQL (
add_postgres + add_database)
- Redis (
add_redis)
- MySQL (
add_mysql + add_database)
- SQL Server (
add_sql_server + add_database)
Pattern
db = builder.add_<datastore>("<name>")
app = builder.add_dockerfile("app", "./src")
app.with_reference(db)
app.wait_for(db)
Chain order:
with_reference(db) — Injects connection string
wait_for(db) — Establishes startup ordering
with_otlp_exporter() — Enables observability
with_external_http_endpoints() — Exposes ports
with_http_endpoint() — Configures specific port
Related
.squad/decisions/inbox/banner-python-waitfor-coldstart.md — cold-start fix decision
.squad/decisions/inbox/banner-python-apphost-13.4.6-migration.md — migration decision
.squad/agents/banner/history.md — historical context
- Aspire 13.4.6 Python SDK bindings:
.aspire/modules/aspire_app.py (generated at runtime)