Compatibility and certification¶
Use this page to answer two questions: whether an Airflow version is certified, and what the plugin does when Airflow changes an internal interface.
The short answer is that every private Airflow dependency belongs in _compat/. Before a
fixture uses one, the compatibility layer resolves the installed Airflow family, probes the
interfaces it needs, and rejects an incompatible installation with the failing symbol in the
error message.
Supported and certified¶
The plugin supports Apache Airflow 3.1 or newer below 4. Certification records the exact Airflow releases and Python combinations exercised by this repository.
- The package supports CPython 3.10-3.14 and pytest 8 or newer.
- Linux and macOS are supported. On Windows, use WSL2 or the devcontainer.
- Airflow 3 releases at or above 3.1.0 can resolve through the compatibility layer. Releases with a checked-in contract row are certified; other 3.x releases are probed.
- Airflow 2 is a closed certification tier: only 2.7.3, 2.8.4, 2.9.3, 2.10.5, and 2.11.2 resolve. The first two support Python through 3.11; the others support Python through 3.12.
Run pytest --airflow-doctor in the environment you intend to
use. It reports the installed versions, certification tier, and resolved capabilities.
What CI actually exercises¶
The compatibility workflow uses representative pairs rather than every Cartesian combination:
- Airflow 3.1-3.3 across CPython 3.10-3.14, including the pytest 8.0 floor;
- Linux, macOS, ARM Linux, and Alpine/musl;
- parallel runs under
-n auto --dist loadgroup, plus one serial reference leg; and - SQLite throughout the matrix, with a separate real-Docker Postgres job.
Airflow 3 legs run the complete suite. Airflow 2 legs run the consumer contract in
tests/enduser/, including one xdist leg, on Linux and SQLite. See
GitHub Actions and reports for reproducing these environments.
Airflow 2.x is a migration bridge, not a second home¶
The certified Airflow 2 tier exists so one consumer suite can remain green during a 2-to-3
migration. The shared surface includes dag_maker, run_ti, dag_bag, run_dag, cleanup,
seeding, configuration, and smoke checks.
Task SDK runners, structlog capture, the REST API, component sandbox, and executor-driven runs are Airflow 3 only. They fail on Airflow 2 with an alternative where one exists. The Fixtures table is the per-feature source of truth.
The surface it stands on¶
Airflow does not expose a public testing API for the work this plugin performs. Creating
scheduler metadata, running a RuntimeTaskInstance, clearing related ORM tables, and evaluating
asset schedules all require private interfaces that can move between releases.
That movement is expected. Across recent releases, DagBag, SerializedDAG, asset evaluation,
plugin-manager caches, the Task SDK runner, and executor and listener contracts have all changed
location or shape. The compatibility layer turns each variation into one capability; fixtures
consume that capability instead of branching on an Airflow version.
One seam, and it is enforced¶
Any runtime use of Airflow internals must live under src/pytest_airflow_in_a_box/_compat/.
Outside that package, Airflow imports in shipped source are type-checking-only annotations.
tests/compat/test_seam.py enforces the boundary with an AST scan. It catches ordinary imports,
imports inside functions or exception handlers, and literal dynamic imports. The accepted-leak
set is empty. As a result, a new Airflow release should change the compatibility package and its
contract tests -- not scatter version branches through fixtures, collection, or database code.
How a probe works¶
A probe observes the installed code; it does not compare version strings. The capability layer uses four patterns:
- resolve a required module attribute and wrap failure in
AirflowCompatibilityError; - inspect whether a callable accepts a parameter;
- inspect whether a Pydantic model exposes a field; or
- try the known locations or shapes and return a member of a closed enum.
The results form one immutable AirflowCapabilities value, cached once per process. Resolution
is lazy, so loading pytest without using an Airflow feature does not import Airflow.
Closed enums matter: if an interface has a third, unknown shape, resolution fails instead of
guessing. The error names the symbol or contract that moved. --airflow-doctor prints the same
resolved fields in a diagnostic report.
Certified, then verified¶
A certified release has a hand-maintained row in _CERTIFIED_CAPABILITIES. Runtime probes are
compared with that row, including the canonical SerializedDAG location. A mismatch aborts the
session.
Certification combines two kinds of checks:
- Probes verify interface locations, callable parameters, model fields, and other structural differences that can be observed directly.
_REQUIRED_SYMBOLS_BY_FAMILYimports every additional private symbol the plugin depends on. A moved or removed symbol fails even when it is not represented by a capability field.
Family-derived fields are consistency checks, not independent observations. The distinction is intentional: a certified row documents the expected contract, while probes and required-symbol checks enforce the portions that can be verified mechanically.
When your Airflow is newer than the certified set¶
An uncertified Airflow 3 release at or above the supported floor resolves on the PROBED tier.
All capability probes and required-symbol checks still run, but no certified row exists for the
final comparison.
Pytest emits one UncertifiedAirflowWarning, --airflow-doctor reports DEGRADED, and the
component sandbox uses generic snapshot and restore. Tests may continue, but the plugin has not
yet certified that release's private interfaces. Pin a certified release when you require that
assurance.
The weekly Airflow canary installs the newest matching upstream release, runs the compatibility suite, and deliberately fails its certification probe for an uncertified release. That failure files an issue with the resolved environment and reports so certification work begins before a routine upgrade reaches users.
tests/enduser/ is the consumer contract¶
Compatibility-unit tests prove individual shims. tests/enduser/ proves that public fixtures
still behave as a consumer expects. Its dual-family tests cover whole-Dag runs, operators,
sensors, TaskFlow, mapping, triggers, callbacks, hooks, providers, configuration, corpus checks,
and migration behavior. Airflow 3-only modules add assets, REST API, structlog, and executor
coverage.
The complete directory runs on every Airflow 3 compatibility leg. Airflow 2 legs run only this
directory; four modules that import 3-only surfaces are excluded, while individual 3-only tests
are collected and skipped through requires_airflow3.
A probe can prove that a symbol exists. An end-user test proves that the public workflow built on it still works.
Adapted upstream code¶
Some compatibility implementations follow upstream Airflow behavior closely enough to require
an audit trail. PROVENANCE.md
records the upstream files and exact commits used by the adapted task-instance and asset-schedule
code, along with the sources consulted for certified contracts.
For the surrounding runtime boundary, see Test Environments: bootstrap
owns configuration before Airflow imports, and _compat/ supplies the private mechanics after
that boundary.