Encore.py

An infrastructure SDK for Python

Encore.py is the Python SDK for Encore. It lets you declare the infrastructure your application needs, such as Postgres databases, Pub/Sub topics, object storage buckets, Redis databases, cron jobs and secrets, as module-level variables in your Python code, and use them through async Python APIs.

Encore.py covers infrastructure only. It has no API framework: your HTTP routes stay in the web framework you already use, such as FastAPI, Django or Flask. Encore reads the infrastructure declarations from your code, runs matching infrastructure locally with encore run, and gives each test run its own isolated copy with encore test. With the Encore platform, it provisions the same resources in your own AWS or GCP account when you deploy.

Please note

Encore.py is in early access. APIs may still change between releases, and a few features are recorded by Encore but not enforced at runtime yet. Each page notes where this applies.

What you get with Encore.py

  • Infrastructure declared in Python: Postgres databases, Pub/Sub topics and subscriptions, object storage buckets, Redis databases, cron jobs, secrets and configuration are module-level variables. Encore reads them statically, so it knows which service uses which resource and what it does with it.
  • Your own web framework: Encore.py works alongside FastAPI, Django, Flask or plain http.server. Encore starts your service and tells it which address to listen on; the routing is yours.
  • Local infrastructure, automatically: encore run starts a local Postgres, Pub/Sub broker, object storage and Redis for your app, and applies your migrations. There are no Docker Compose files to maintain.
  • Isolated tests: encore test runs pytest with its own databases, buckets, topics and Redis keys, created on first use and removed afterwards.
  • Observability: logging, metrics and tracing that show up in Encore's local development dashboard, with OpenTelemetry providers for your own spans and instruments.

The infrastructure primitives

Each primitive is a module-level variable, declared in the service that owns it. Encore's parser discovers the declaration; the SDK connects it to the provisioned resource the first time you use it.

from encore.storage import postgres db = postgres.Database("orders", migrations="./migrations") async def count_orders() -> int: row = await db.query("SELECT count(*) AS n FROM orders").one() return row["n"]
from encore import pubsub from pydantic import BaseModel class Order(BaseModel): order_id: int orders = pubsub.Topic("orders") async def process_order(message: pubsub.Message[Order]) -> None: print("Fulfilling order", message.data.order_id) order_processor = pubsub.Subscription[Order]( orders, "process-orders", handler=process_order )
from encore.storage import objects uploads = objects.Bucket("uploads") async def save_greeting() -> None: await uploads.upload("hello.txt", b"Hello!", content_type="text/plain")
from encore.storage import redis sessions = redis.Database("sessions") async def remember(user_id: str) -> None: await sessions.string.set(f"user:{user_id}", b"active", ex=60)
from encore import cron _ = cron.Job( "cleanup", path="/internal/cleanup", title="Nightly cleanup", every="24h", )
from encore import secrets stripe_key = secrets.Secret[str]("StripeKey") def stripe_headers() -> dict[str, str]: return {"Authorization": f"Bearer {stripe_key.value}"}

A cron job has no runtime client: when it is due, Encore sends a POST request for path to the service that declares the job, and your framework handles it like any other request.

Declarations describe what the application needs, not how it's provisioned. Instance sizes, networking and other deployment settings are configured per environment, so the same code runs locally, in preview environments and in production.

How Encore reads your code

Encore reads your declarations without running your code, so they follow a few rules. The parser reports a violation as an error when it parses the app, before anything runs.

  • Declare resources as module-level variables, never inside a function, class or conditional.
  • Pass resource names, and the configuration arguments Encore reads (such as migrations or a cron job's path), as string or other literals.
  • Don't alias a declaration (other = db) or pass it to a function. To hand a database or Redis database to other code, pass a handle from read() or write(). Encore records the access level at that call.
  • Call methods directly on the declaration (db.query(...)), rather than storing a method (q = db.query).
  • Use resources from code inside a service directory. Encore attributes each use to the service whose directory contains the file, and grants that service access to the resource.

The individual pages describe the rules that are specific to each primitive.

How an app is laid out

An Encore app is a directory with an encore.app file at its root. Each service is a directory with an encore.service.json file. A single-service app can keep both at the root:

my-app/ ├── encore.app ├── encore.service.json ├── pyproject.toml ├── uv.lock ├── main.py ├── resources.py └── migrations/ └── 1_create_orders.up.sql

The service file names the service and its language, and tells Encore which HTTP paths the service serves:

encore.service.json
{ "name": "orders", "lang": "python", "api": [ { "type": "http", "prefix": "/", "name": "orders", "expose": true } ] }

See Services for every field, and for apps with several services.

encore app new my-app --template python creates an app like this with a minimal http.server service and no dependencies. To adopt an existing directory, run encore app init in it. Either way, add the SDK and your framework to the service's dependencies.

Installing the SDK

The SDK is the encore-dev package, imported as encore. It needs Python 3.9 or later. Add encore-dev to your project's dependencies alongside your framework:

pyproject.toml
[project] name = "orders" version = "0.1.0" requires-python = ">=3.9" dependencies = [ "encore-dev", "fastapi", "uvicorn", ]

The package is pure Python. Its native runtime ships with the Encore CLI, which points the SDK at it whenever it starts your code: in encore run, encore test and encore exec. Importing encore initializes the runtime and reads its configuration from Encore, so run your app through those commands rather than with python main.py on its own.

Encore runs your code through the package manager it finds in the service's directory, detected from the lockfile:

LockfileRun commandTest command
uv.lockuv run main.pyuv run pytest
poetry.lockpoetry run python main.pypoetry run pytest
Pipfile.lock or Pipfilepipenv run python main.pypipenv run pytest
pdm.lockpdm run python main.pypdm run pytest
nonepython3 main.pypython3 -m pytest

The entry point is main.py, or app.py if there is no main.py. Without a lockfile, the dependencies must already be importable by python3. To run something else, set a custom run command in encore.service.json.

Starting Encore in your app

Your entry point starts your web server on the address Encore gives it in the ENCORE_LISTEN_ADDR environment variable, as host:port. Once your resource declarations are imported, call await encore.start() from your framework's startup hook. It starts the handlers of the Pub/Sub subscriptions your service declares on the application's event loop, and returns.

With FastAPI, that's the lifespan handler:

main.py
import os from contextlib import asynccontextmanager import encore import uvicorn from fastapi import FastAPI # Importing resources.py runs its declarations, including any subscriptions. from resources import db @asynccontextmanager async def lifespan(app: FastAPI): await encore.start() yield app = FastAPI(lifespan=lifespan) @app.get("/orders/{order_id}") async def get_order(order_id: int) -> dict: row = await db.query("SELECT id, total FROM orders WHERE id = $1", order_id).one() return row or {} if __name__ == "__main__": host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":") uvicorn.run(app, host=host, port=int(port))

encore.start() doesn't take over the event loop or install signal handlers. Calling it again on the same loop is safe, and starts subscriptions imported since the last call. A service without subscriptions doesn't need it, but calling it does no harm.

The SDK's I/O methods are async. From synchronous code, such as a Django view, call them through a bridge such as asgiref.sync.async_to_sync:

from asgiref.sync import async_to_sync from .resources import uploads def download(request, name): data = async_to_sync(uploads.download)(name) ...

Initialize Encore separately in each worker process: forking a process after the SDK has been imported isn't supported. For how Encore stops your app, see Graceful shutdown.

Running and testing

  • encore run parses the app, starts the local infrastructure, applies migrations and starts each service. It restarts a service when its code changes, so turn off your framework's own reloader.
  • encore test runs each service's tests with pytest, against infrastructure created for the test run. See Testing.
  • encore exec runs a one-off command, such as a seed script or python manage.py migrate, attached to the same local infrastructure as encore run:
encore exec -- uv run python manage.py migrate

Next steps