PyCharter + Prefect¶
Prefect owns orchestration — scheduling, retries, observability, the flow/task graph. PyCharter owns the contract — the thing Prefect has no primitive for. They compose without overlap: Prefect orchestrates, PyCharter validates.
The adapter targets Prefect 3 (prefect>=3.0,<4; verified against
3.7). A runnable end-to-end flow lives at
examples/prefect_reference_flow.py.
pycharter.contrib.prefect gives you ready task factories. Each captures its
contract/config once and returns a Prefect Task; **task_kwargs pass
straight through to @task, so you set Prefect-native retries,
retry_delay_seconds, cache_policy, and tags per task.
Both sides, one contract¶
The payoff is that the producer-side gate and the post-load quality check derive from the same versioned contract, so they cannot drift.
from prefect import flow
from pycharter import QualityCheckOptions
from pycharter.contrib.prefect import (
pycharter_preflight_task,
pycharter_quality_task,
pycharter_validate_task,
)
CONTRACT = "contracts/orders.yaml" # one artifact, both ends
preflight = pycharter_preflight_task(config="pipelines/orders.yaml")
validate = pycharter_validate_task( # producer-side, per record
contract=CONTRACT, on_invalid="filter", retries=2
)
quality = pycharter_quality_task( # post-load, whole batch
contract=CONTRACT,
options=QualityCheckOptions.monitoring(),
publish_artifact=True, # scores/breaches in the Prefect UI
)
@flow(name="orders-etl")
def orders_etl():
preflight() # fail before any I/O
raw = extract_from_source() # your Prefect task
clean = validate(raw) # gate records in-flight
load_to_warehouse(clean) # your Prefect task
snapshot = read_warehouse_snapshot() # your Prefect task
quality(snapshot) # post-load DQ; fails flow on breach
The validate task¶
pycharter_validate_task(contract=…, on_invalid=…) takes a list[dict] and
returns a list[dict]. on_invalid chooses the failure policy:
on_invalid |
Behaviour |
|---|---|
"raise" (default) |
Raise ContractValidationError if any record fails; otherwise return the coerced valid records. |
"filter" |
Drop failing records; return the coerced valid ones. |
"tag" |
Return every record in input order; failing records gain a _violations key. |
The Validator is built once at factory time and reused per call
(thread-safe), so a high-throughput flow pays the compile cost only once.
Sync vs. async — the one gotcha¶
Validator.validate / validate_records are pure-sync and safe in any task.
The pipeline is different: pycharter.run() calls asyncio.run()
internally and crashes inside a running event loop. So:
- In an async flow,
pycharter_run_task(...)(defaultis_async=True) awaitsPipeline.run()directly — loop-safe. - In a sync flow, pass
is_async=Falseto usepycharter.run(). - The quality task offloads the synchronous
check_qualityto a worker thread (anyio.to_thread), so it never blocks the flow's loop.
Scope: keep Prefect as the orchestrator¶
Use PyCharter for validation and quality inside your Prefect tasks. Reach
for pycharter_run_task only when you deliberately want to run a whole
PyCharter pipeline as a single task — don't rebuild Prefect's scheduling,
retries, or connection management with PyCharter's own extractors/loaders.
See PyCharter vs. the alternatives for where PyCharter fits next to Airflow, Dagster, and warehouse-side quality tools.