You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Template fields are rendered after an operator's constructor runs. Any logic applied to a template-field parameter's value inside __init__ — validation, type checks, transformation, string interpolation — therefore operates on the un-rendered Jinja expression, not the real value.
Checks that only ask whether an argument was passed are the exception and belong in __init__. Read False positives below before fixing an entry.
The validate-operators-init prek hook previously only verified that template fields are assigned verbatim at the top level of __init__; it could not see validation calls, conditionals, or transformations (and it did not cover sensors, AwsBaseOperator[...] subclasses, or aws_template_fields(...)-based classes at all). The hook has been extended to detect any non-sanctioned read of a template field in __init__ (PR to follow).
A PR that fixes a class must remove its entry in the same PR — the hook fails on stale exemptions, so this cannot be forgotten.
New violations cannot be added: only files listed in the exemption file are suppressed.
This issue is done when the exemption file is empty.
How to fix a class: move the validation / transformation from __init__ into execute() (or the first method that runs after rendering), keep __init__ down to plain self.field = field assignments (defaulting via field or default is fine). Don't forget the corresponding tests.
Please do not open sub-issues for individual entries — comment here or just open a PR referencing this issue.
False positives
Not everything the hook flags should move. A check that only asks whether an argument was passed — the usual "exactly one of a or b" guard — belongs in __init__ and must not be moved:
The constructor is the only place that can answer it. With render_template_as_native_obj=True a provided field renders to None, so the same check in execute() reports a supplied argument as missing.
Raising at construction surfaces a static authoring mistake as a Dag import error, instead of on a worker once per task instance and per retry.
Fix these by rewriting in place, not by moving. Use the is not None polarity — if field: is a truthiness test on the un-rendered string and asks a third question that matches neither:
# in __init__ — correctifnotexactly_one(commandisnotNone, powershellisnotNone, cmdletisnotNone):
raiseValueError("Must provide exactly one of 'command', 'powershell', or 'cmdlet'")
Do not write exactly_one(command is None, ...). It happens to agree for two arguments but is wrong for three or more: it rejects the valid single-argument case and accepts two arguments at once.
Anything that inspects the value — a range check, a format check, an in test against allowed values, .strip(), isinstance(...) — still moves to execute(). And a provision check in __init__ guarantees nothing about the rendered value: code in execute() that needs the field set still needs its own guard.
The hook did not know this distinction until it was narrowed. These five entries were flagged only for a provision check and clear with no operator code change — they are removed from the exemption file by the narrowing PR, so please don't pick them up:
Five already-merged fixes moved a provision check that should have stayed. Nothing is broken, but those files now demonstrate the discouraged pattern — reverting them is tracked in #70503. Please don't use them as a model.
S3DeleteObjectsOperator needs care and is worth reading before you touch it. Its check is a pure provision check, but it hides the comparisons in a comprehension (all(var is None for var in [...])) that the hook does not recognise, so the entry stays even after the narrowing. It also already exists twice — in __init__ and in execute() — and both copies must stay:
The execute() copy is load-bearing. keys = self.keys or self.hook.list_keys(prefix=..., from_datetime=..., to_datetime=...) lists the whole bucket when every filter is None, and the next line deletes it. A templated keys that renders to None (native rendering) reaches that state past a correct __init__ check.
The __init__ copy still catches the static authoring mistake at Dag parse time rather than on a worker.
To clear the entry, unroll the __init__ copy so every read is a direct comparison — don't delete it, and don't touch execute():
by_scan=prefixisnotNoneorfrom_datetimeisnotNoneorto_datetimeisnotNoneifnotexactly_one(keysisnotNone, by_scan):
raiseValueError("Either keys or at least one of prefix, from_datetime, to_datetime should be set.")
That is semantically identical to the current condition on every input combination (including keys=[]) and clears the hook.
Current exemption list (snapshot)
The authoritative list is the exemptions file; this checklist is the snapshot at the time the check was introduced. 83 classes across 22 providers, of which the 5 listed under False positives above need no code change.
amazon
AppflowBaseOperator — appflow.py — logic in __init__
AwsToAwsBaseOperator — base.py — logic in __init__
BedrockCreateKnowledgeBaseOperator — bedrock.py — logic in __init__
Background
Template fields are rendered after an operator's constructor runs. Any logic applied to a template-field parameter's value inside
__init__— validation, type checks, transformation, string interpolation — therefore operates on the un-rendered Jinja expression, not the real value.This is documented in Creating a custom Operator and contributing-docs/05_pull_requests.rst, and it causes real bugs: #69813 (
SSHRemoteJobOperatorvalidated a templatedremote_base_dirin__init__, so cleanup validation failed for any custom base) is a recent example.Important
Checks that only ask whether an argument was passed are the exception and belong in
__init__. Read False positives below before fixing an entry.The
validate-operators-initprek hook previously only verified that template fields are assigned verbatim at the top level of__init__; it could not see validation calls, conditionals, or transformations (and it did not cover sensors,AwsBaseOperator[...]subclasses, oraws_template_fields(...)-based classes at all). The hook has been extended to detect any non-sanctioned read of a template field in__init__(PR to follow).The exemption ratchet
Existing violations are listed in
scripts/ci/prek/validate_operators_init_exemptions.txtaspath::ClassNameentries so the hook can enforce the rule on new code immediately.How to fix a class: move the validation / transformation from
__init__intoexecute()(or the first method that runs after rendering), keep__init__down to plainself.field = fieldassignments (defaulting viafield or defaultis fine). Don't forget the corresponding tests.Please do not open sub-issues for individual entries — comment here or just open a PR referencing this issue.
False positives
Not everything the hook flags should move. A check that only asks whether an argument was passed — the usual "exactly one of
aorb" guard — belongs in__init__and must not be moved:render_template_as_native_obj=Truea provided field renders toNone, so the same check inexecute()reports a supplied argument as missing.Fix these by rewriting in place, not by moving. Use the
is not Nonepolarity —if field:is a truthiness test on the un-rendered string and asks a third question that matches neither:Do not write
exactly_one(command is None, ...). It happens to agree for two arguments but is wrong for three or more: it rejects the valid single-argument case and accepts two arguments at once.Anything that inspects the value — a range check, a format check, an
intest against allowed values,.strip(),isinstance(...)— still moves toexecute(). And a provision check in__init__guarantees nothing about the rendered value: code inexecute()that needs the field set still needs its own guard.The hook did not know this distinction until it was narrowed. These five entries were flagged only for a provision check and clear with no operator code change — they are removed from the exemption file by the narrowing PR, so please don't pick them up:
EmrAddStepsOperatorexactly_one(job_flow_id is None, job_flow_name is None)GCSDeleteObjectsOperatorobjects/prefixexclusivityGCSToLocalFilesystemOperatorfilename/store_to_xcom_keyexclusivityOracleToAzureDataLakeOperatorsql_paramsdefaultOracleToOracleOperatorsource_sql_paramsdefaultFive already-merged fixes moved a provision check that should have stayed. Nothing is broken, but those files now demonstrate the discouraged pattern — reverting them is tracked in #70503. Please don't use them as a model.
S3DeleteObjectsOperatorneeds care and is worth reading before you touch it. Its check is a pure provision check, but it hides the comparisons in a comprehension (all(var is None for var in [...])) that the hook does not recognise, so the entry stays even after the narrowing. It also already exists twice — in__init__and inexecute()— and both copies must stay:execute()copy is load-bearing.keys = self.keys or self.hook.list_keys(prefix=..., from_datetime=..., to_datetime=...)lists the whole bucket when every filter isNone, and the next line deletes it. A templatedkeysthat renders toNone(native rendering) reaches that state past a correct__init__check.__init__copy still catches the static authoring mistake at Dag parse time rather than on a worker.To clear the entry, unroll the
__init__copy so every read is a direct comparison — don't delete it, and don't touchexecute():That is semantically identical to the current condition on every input combination (including
keys=[]) and clears the hook.Current exemption list (snapshot)
The authoritative list is the exemptions file; this checklist is the snapshot at the time the check was introduced. 83 classes across 22 providers, of which the 5 listed under False positives above need no code change.
amazon
AppflowBaseOperator— appflow.py — logic in__init__AwsToAwsBaseOperator— base.py — logic in__init__BedrockCreateKnowledgeBaseOperator— bedrock.py — logic in__init__BedrockRaGOperator— bedrock.py — transformed assignment, logic in__init__, missing assignmentDataSyncOperator— datasync.py — logic in__init__DmsModifyTaskOperator— dms.py — logic in__init__DmsStartReplicationOperator— dms.py — logic in__init__EcsRunTaskOperator— ecs.py — logic in__init__GCSToS3Operator— gcs_to_s3.py — logic in__init__in PR Check GCSToS3Operator match_glob support after template rendering #70723GlueDataQualityOperator— glue.py — transformed assignment, logic in__init__, missing assignmentMongoToS3Operator— mongo_to_s3.py — logic in__init__NeptuneStartDbClusterOperator— neptune.py — transformed assignment, missing assignment in PR Rename Neptune operator db_cluster_id argument to cluster_id #70491NeptuneStopDbClusterOperator— neptune.py — transformed assignment, missing assignment in PR Rename Neptune operator db_cluster_id argument to cluster_id #70491S3DeleteObjectsOperator— s3.py — logic in__init__S3ToRedshiftOperator— s3_to_redshift.py — logic in__init__SageMakerCreateNotebookOperator— sagemaker.py — logic in__init__SageMakerProcessingOperator— sagemaker.py — logic in__init__StepFunctionStartExecutionOperator— step_function.py — transformed assignment, missing assignmentanthropic
AnthropicAgentSessionOperator— agent.py — logic in__init__apache/hive
HivePartitionSensor— hive_partition.py — logic in__init__NamedHivePartitionSensor— named_hive_partition.py — logic in__init__apache/kafka
ProduceToTopicOperator— produce.py — logic in__init__apache/spark
SparkSubmitOperator— spark_submit.py — logic in__init__cncf/kubernetes
KubernetesInstallKueueOperator— kueue.py — logic in__init__KubernetesPodOperator— pod.py — logic in__init__KubernetesResourceBaseOperator— resource.py — logic in__init__cohere
CohereEmbeddingOperator— embedding.py — logic in__init__common/ai
AgentOperator— agent.py — logic in__init__DocumentLoaderOperator— document_loader.py — logic in__init__databricks
DatabricksCopyIntoOperator— databricks_sql.py — logic in__init__DatabricksReposCreateOperator— databricks_repos.py — logic in__init__DatabricksReposDeleteOperator— databricks_repos.py — logic in__init__DatabricksReposUpdateOperator— databricks_repos.py — logic in__init__DatabricksSQLStatementsSensor— databricks.py — logic in__init__dbt/cloud
DbtCloudGetJobRunArtifactOperator— dbt.py — logic in__init__docker
DockerOperator— docker.py — logic in__init__google
AzureFileShareToGCSOperator— azure_fileshare_to_gcs.py — logic in__init__in PR Resolve AzureFileShareToGCSOperator directory_name alias after rendering #70740BigQueryDataTransferServiceTransferRunSensor— bigquery_dts.py — transformed assignment, logic in__init__, missing assignment in PR Normalize BigQuery DTS sensor expected statuses after rendering #70528BigQueryInsertJobOperator— bigquery.py — logic in__init__BigQueryToMsSqlOperator— bigquery_to_mssql.py — logic in__init__CloudBatchSubmitJobOperator— cloud_batch.py — logic in__init__CloudBuildCreateBuildOperator— cloud_build.py — logic in__init__CloudComposerExternalTaskSensor— cloud_composer.py — logic in__init__CloudDataTransferServiceCreateJobOperator— cloud_storage_transfer_service.py — logic in__init__in PR Validate storage transfer job body after template rendering #70529CloudFunctionDeployFunctionOperator— functions.py — logic in__init__in PR Validate Cloud Function deploy body after template rendering #70531ComputeEngineCopyInstanceTemplateOperator— compute.py — logic in__init__ComputeEngineDeleteInstanceGroupManagerOperator— compute.py — logic in__init__ComputeEngineDeleteInstanceOperator— compute.py — logic in__init__ComputeEngineDeleteInstanceTemplateOperator— compute.py — logic in__init__ComputeEngineInsertInstanceFromTemplateOperator— compute.py — logic in__init__ComputeEngineInsertInstanceGroupManagerOperator— compute.py — logic in__init__ComputeEngineInsertInstanceOperator— compute.py — logic in__init__ComputeEngineInsertInstanceTemplateOperator— compute.py — logic in__init__ComputeEngineInstanceGroupUpdateManagerTemplateOperator— compute.py — logic in__init__ComputeEngineSetMachineTypeOperator— compute.py — logic in__init__DataprocCreateClusterOperator— dataproc.py — logic in__init__DataprocSubmitJobOperator— dataproc.py — logic in__init__GCSFileTransformOperator— gcs.py — logic in__init__in PR Resolve GCSFileTransformOperator destination fallbacks after rendering #70488GCSListObjectsOperator— gcs.py — logic in__init__GCSToBigQueryOperator— gcs_to_bigquery.py — logic in__init__in PR Emit GCSToBigQueryOperator deprecation warning after rendering #70542GCSToGCSOperator— gcs_to_gcs.py — logic in__init__in PR Emit GCSToGCSOperator deprecation warnings after rendering #70449GenAIGeminiCreateBatchJobOperator— gen_ai.py — logic in__init__GenAIGeminiCreateEmbeddingsBatchJobOperator— gen_ai.py — logic in__init__GoogleCampaignManagerDeleteReportOperator— campaign_manager.py — logic in__init__in PR Keep Campaign Manager delete report provision check in __init__ #70530microsoft/azure
AzureVirtualMachineStateSensor— compute.py — logic in__init__GCSToAzureBlobStorageOperator— gcs_to_wasb.py — logic in__init__in PR Check GCSToAzureBlobStorageOperator match_glob support after template… #70574microsoft/psrp
PsrpOperator— psrp.py — logic in__init__in PR Fix PsrpOperator option checks and drop the cmdlet task_id default #70347neo4j
Neo4jOperator— neo4j.py — logic in__init__papermill
PapermillOperator— papermill.py — logic in__init__snowflake
SnowparkContainerJobOperator— snowpark_containers.py — logic in__init__ssh
SSHOperator— ssh.py — logic in__init__SSHRemoteJobOperator— ssh_remote_job.py — logic in__init__standard
BashOperator— bash.py — logic in__init__DateTimeSensor— date_time.py — logic in__init__, missing assignmentHITLOperator— hitl.py — logic in__init__TriggerDagRunOperator— trigger_dagrun.py — logic in__init__teradata
TeradataToTeradataOperator— teradata_to_teradata.py — logic in__init__weaviate
WeaviateIngestOperator— weaviate.py — logic in__init__Drafted-by: Claude Code (Opus 4.8) (no human review before posting)