A linting tool for Apache Airflow DAG files
Project description
DAGLint
A linting tool for Apache Airflow DAG files. Uses Python's AST to enforce standardisation and best practices across your DAG codebase.
Inspired by "Mastering Airflow DAG Standardization with Python's AST"
Features
| Rule | Description |
|---|---|
dag_id_convention |
Enforces snake_case naming for DAG IDs |
task_id_convention |
Enforces snake_case naming for task IDs |
group_id_convention |
Enforces snake_case naming for task group IDs |
owner_validation |
Validates DAG owners against an approved list |
tag_requirements |
Validates required tags are present |
required_dag_params |
Ensures required default_args keys are set |
retry_configuration |
Checks retry values are within configured limits |
no_duplicate_task_ids |
Prevents duplicate task IDs within a DAG (task-group aware: group.task paths) |
max_active_runs_validation |
Ensures max_active_runs is explicitly set |
catchup_validation |
Checks catchup is explicitly set |
schedule_validation |
Ensures schedule_interval / schedule is set |
Installation
# From PyPI
pip install daglint
# From source
pip install -e .
Usage
# Lint a single file or directory
daglint check dags/my_dag.py
daglint check dags/
# Run specific rules only
daglint check dags/ --rules dag_id_convention,owner_validation
# Machine-readable output for CI and editors
daglint check dags/ --format json
# GitHub Actions annotations (issues appear inline on PRs)
daglint check dags/ --format github
# Fail on warnings too, not just errors
daglint check dags/ --strict
# Skip extra directories when scanning (adds to the built-in defaults)
daglint check . --exclude generated --exclude fixtures
# List all available rules
daglint rules
# Generate a default configuration file
daglint init
Example output
$ daglint check examples/invalid_dag.py
✗ examples/invalid_dag.py
ERROR [owner_validation] Line 8: Invalid owner 'invalid-team'. Must be one of: data-team, analytics-team, airflow
ERROR [required_dag_params] Line 8: Missing required parameters in default_args: retries, start_date
ERROR [dag_id_convention] Line 19: DAG ID 'InvalidDAGID' does not match pattern '^[a-z][a-z0-9_]*$'
WARNING [doc_md_validation] Line 19: DAG is missing doc_md documentation
WARNING [tag_requirements] Line 19: Missing required tags: team, environment
WARNING [max_active_runs_validation] Line 19: max_active_runs must be explicitly set to 1
WARNING [catchup_validation] Line 19: Catchup parameter not set. Consider setting it explicitly to False
WARNING [schedule_validation] Line 19: schedule must be explicitly set
ERROR [task_id_convention] Line 30: Task ID 'InvalidTaskID' does not match pattern '^[a-z][a-z0-9_]*$'
ERROR [task_id_convention] Line 36: Task ID 'InvalidTaskID' does not match pattern '^[a-z][a-z0-9_]*$'
ERROR [no_duplicate_task_ids] Line 36: Duplicate task_id 'InvalidTaskID' (first seen at line 30)
--------------------------------------------------
Found 11 issue(s) (6 error(s), 5 warning(s)) in 1 file(s).
$ daglint check examples/valid_dag.py
✓ examples/valid_dag.py
--------------------------------------------------
All checks passed!
Output formats
--format selects how issues are reported (default text):
-
text— colorized human-readable output, as above. -
json— a machine-readable envelope for CI systems, editors, and wrappers:{ "issues": [ { "rule_id": "owner_validation", "severity": "warning", "file": "dags/etl.py", "line": 12, "column": 0, "message": "DAG must have an owner" } ], "summary": {"files_checked": 8, "errors": 0, "warnings": 1, "infos": 0} }
-
github— GitHub Actions workflow commands (::error file=...,line=...::message), so issues show up as inline annotations on pull requests.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No issues found, or only warning/info issues without --strict |
| 1 | At least one error-severity issue (with --strict: any issue at all) |
| 2 | Usage error (unknown rule, bad flag, missing path) |
Warning-severity issues are advisory and do not fail the build by default; pass --strict to make them fail CI.
Configuration
Generate a starter config with daglint init, or create .daglint.yaml manually:
rules:
dag_id_convention:
enabled: true
pattern: "^[a-z][a-z0-9_]*$"
severity: error
owner_validation:
enabled: true
valid_owners:
- data-team
- analytics-team
severity: error
tag_requirements:
enabled: true
required_tags:
- environment
- team
severity: warning
retry_configuration:
enabled: true
min_retries: 1
max_retries: 5
severity: warning
max_active_runs_validation:
enabled: true
max_active_runs: 1
severity: warning
severity must be one of error, warning, or info; any other value fails at startup with exit code 2.
Excluding directories
Directory scans always skip hidden directories (.venv/, .tox/, .git/, …) and these defaults: venv/, env/, build/, dist/, site-packages/. Add your own directory-name patterns on top — they are matched with shell-style globs against each directory name, anywhere in the tree:
exclude:
- generated
- "*_fixtures"
The repeatable --exclude CLI flag is additive with both the defaults and the config list. A file named explicitly on the command line is always linted, regardless of excludes.
Development
make install-dev # Install with dev dependencies
make test # Run tests
make test-cov # Run tests with coverage report
make format # Format code (black + isort)
make lint # Run all linters
make check # lint + test
make help # List all targets
Project structure
src/daglint/
cli.py # Click CLI: check, rules, init commands
linter.py # DAGLinter: orchestrates rule execution
config.py # Config: loads .daglint.yaml or uses defaults
models.py # LintIssue dataclass
rules/
base.py # BaseRule ABC
__init__.py # AVAILABLE_RULES registry
naming.py # dag_id_convention, task_id_convention, group_id_convention
configuration.py # retry_configuration, schedule_validation, catchup_validation
validation.py # no_duplicate_task_ids
metadata/ # owner_validation, tag_requirements, required_dag_params,
# max_active_runs_validation, doc_md_validation
tests/
examples/
valid_dag.py # Compliant DAG example
invalid_dag.py # DAG with multiple violations
Further reading
- CONTRIBUTING.md — development setup, git workflow, adding rules
- DEPLOYMENT.md — CI/CD pipeline, Airflow integration, publishing to PyPI
- VERSION_MANAGEMENT.md — version bumping with bumpver
- CHANGELOG.md — version history
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file daglint-1.1.0.tar.gz.
File metadata
- Download URL: daglint-1.1.0.tar.gz
- Upload date:
- Size: 42.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30a7fae89b62a4dc32bfb138d6a3660b121456c4fe57c542b0f3e00230bcad9a
|
|
| MD5 |
660a0dbed707a11cb85c9019cd4e2ab0
|
|
| BLAKE2b-256 |
cbd1fa8f573e0bb20ab32463476b4a86648024b60d94080792be215b47daf99e
|
Provenance
The following attestation bundles were made for daglint-1.1.0.tar.gz:
Publisher:
release.yml on why-pengo/daglint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
daglint-1.1.0.tar.gz -
Subject digest:
30a7fae89b62a4dc32bfb138d6a3660b121456c4fe57c542b0f3e00230bcad9a - Sigstore transparency entry: 2122838701
- Sigstore integration time:
-
Permalink:
why-pengo/daglint@76e709460cab03a4f08ad1a2d4bf2283ef22e11a -
Branch / Tag:
refs/tags/1.1.0 - Owner: https://github.com/why-pengo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@76e709460cab03a4f08ad1a2d4bf2283ef22e11a -
Trigger Event:
push
-
Statement type:
File details
Details for the file daglint-1.1.0-py3-none-any.whl.
File metadata
- Download URL: daglint-1.1.0-py3-none-any.whl
- Upload date:
- Size: 25.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4729967e954d264df4f4947323fe9a7ab5aae0b478b39861f3815fb867f48ff5
|
|
| MD5 |
abed624ae1bf4fbc751313b388513060
|
|
| BLAKE2b-256 |
38ee8555ee13e140f65b93fee1b83a03946ca7a860ee6e4d113615608270277b
|
Provenance
The following attestation bundles were made for daglint-1.1.0-py3-none-any.whl:
Publisher:
release.yml on why-pengo/daglint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
daglint-1.1.0-py3-none-any.whl -
Subject digest:
4729967e954d264df4f4947323fe9a7ab5aae0b478b39861f3815fb867f48ff5 - Sigstore transparency entry: 2122838812
- Sigstore integration time:
-
Permalink:
why-pengo/daglint@76e709460cab03a4f08ad1a2d4bf2283ef22e11a -
Branch / Tag:
refs/tags/1.1.0 - Owner: https://github.com/why-pengo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@76e709460cab03a4f08ad1a2d4bf2283ef22e11a -
Trigger Event:
push
-
Statement type: