Open-Source Wikis

/

Ansible

/

How to contribute

/

Patterns and conventions

ansible/ansible

Patterns and conventions

The de-facto rules for new code in ansible-core. Most of these are spelled out in AGENTS.md at the repo root; the rest are visible across the existing tree.

Code style

  • Line length: 160. Not 80. The relaxed limit accommodates long imports and SQL-like Jinja expressions in tests.
  • from __future__ import annotations at the top of every Python file. This converts type hints to strings at runtime, allowing forward references and avoiding circular-import issues.
  • No trailing whitespace. Sanity will reject it. Always strip when editing existing files.
  • No obvious comments. # return the result adds no information; skip it.
  • Don't document module parameters in docstrings. Migrate to type hints. The argument_spec and the DOCUMENTATION YAML block are the source of truth for parameter docs.
  • Sentence-case error messages. display.error("Could not parse playbook") not "could not parse playbook".

File headers

Every controller-side file starts with the GPLv3 header (or, in module_utils/, BSD-2-Clause). Look at the top of any existing file for the canonical pattern, e.g., lib/ansible/cli/playbook.py:

#!/usr/bin/env python
# Copyright: (c) 2012, ...
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# PYTHON_ARGCOMPLETE_OK
from __future__ import annotations

The # PYTHON_ARGCOMPLETE_OK marker enables shell tab-completion via argcomplete.

Module file structure

Modules in lib/ansible/modules/ have a strict ordering enforced by sanity:

#!/usr/bin/python
# Copyright + license header
from __future__ import annotations

DOCUMENTATION = r'''
---
module: foo
short_description: ...
description:
  - ...
options:
  ...
'''

EXAMPLES = r'''
- name: do the thing
  foo:
    bar: baz
'''

RETURN = r'''
result:
  description: ...
  returned: always
  type: dict
'''

# Imports come AFTER the YAML strings — these are AST-parsed.
from ansible.module_utils.basic import AnsibleModule
...

def main():
    module = AnsibleModule(argument_spec=dict(...))
    ...

if __name__ == '__main__':
    main()

The DOCUMENTATION/EXAMPLES/RETURN constants are extracted at build time by AST/token parsing — they cannot be dynamically generated, and the order matters.

Display, not print

Use the singleton Display for all controller output:

from ansible.utils.display import Display
display = Display()

display.display("Hello", color=C.COLOR_OK)
display.warning("Be careful")
display.error("Boom", wrap_text=True)
display.deprecated("Old API", version="2.22")
display.vvv("verbose detail")

lib/ansible/utils/display.py serializes output across worker processes and respects color/verbosity settings.

Errors

The custom exception hierarchy lives in lib/ansible/errors/__init__.py:

  • AnsibleError — base class; carries message, obj (the YAML node where the error originated), and _help_text.
  • AnsibleParserError — YAML/playbook parsing failures.
  • AnsibleAssertionError — invariant violations (use sparingly).
  • AnsibleOptionsError — bad CLI arguments.
  • AnsibleUndefinedVariable{{ undefined }} referenced.
  • AnsibleTemplateError and friends — template errors with location info.
  • AnsibleConnectionFailure, AnsibleAuthenticationFailure — connection-layer failures.

Always raise these, not Exception or bare strings. The TQM and callback layer pattern-match on these types to display structured failures.

Plugin lifecycle

Plugin classes follow these conventions:

  • Inherit from the relevant base under lib/ansible/plugins/<type>/__init__.py (e.g., ActionBase, ConnectionBase, LookupBase, BecomeBase, StrategyBase).
  • Class is named after the plugin; instances are constructed by lib/ansible/plugins/loader.py:PluginLoader.get().
  • Documentation lives in a top-of-file DOCUMENTATION = """ ... """ block; the YAML schema mirrors module docs.
  • Configuration options use vars/env/ini declarations in the DOCUMENTATION block; the loader auto-wires them up via Option objects.

Datatag awareness

Code that handles user-supplied YAML data should be tag-aware. The relevant primitives are in lib/ansible/module_utils/_internal/_datatag.py:

  • AnsibleTagHelper.tag(value, *tags) — attach tags.
  • AnsibleTagHelper.untag(value) — strip tags (rarely correct).
  • AnsibleTaggedObject — base class for tagged values.

Tags include Origin (where it came from in source), TrustedAsTemplate (eligible for {{ }} rendering), VaultedValue (Vault-encrypted at rest). Most of the time you don't need to think about tags — they propagate through string operations — but in templating-related code you must respect TrustedAsTemplate rather than blindly running everything through Jinja.

Imports and dependencies

Per AGENTS.md:

  • Prefer Python stdlib over external dependencies.
  • Use existing code from within Ansible. Don't fork helpers.
  • lib/ansible/modules/ can only import from lib/ansible/module_utils/. Modules are bundled and shipped to remote hosts; they cannot import from anywhere else.
  • lib/ansible/module_utils/ cannot import from outside itself. Anything in here gets shipped along with modules, so it must be self-sufficient.

This is enforced by the import sanity test.

Public vs. internal

Anything under lib/ansible/_internal/ is private to ansible-core and may change without notice. The sanity tests block external imports of _internal modules.

The original templating code at lib/ansible/template/__init__.py is the public legacy surface; new code uses the internal lib/ansible/_internal/_templating/_engine.py:TemplateEngine instead.

Deprecation

Use Display.deprecated (controller) or AnsibleModule.deprecate (modules) with a version= argument that's the current version + 3 (4-release deprecation cycle). To find the current version, read lib/ansible/release.py:__version__.

display.deprecated(
    "The old foo() function is deprecated, use new_foo() instead.",
    version="2.22",
)

Mark deprecated public APIs with @deprecated decorators where available, and add a removed_in field if there's a hard removal target.

Testing

See Testing. The summary:

  • Unit tests should be functional, not heavily mock-dependent.
  • Integration tests are required for almost all plugin changes — they exercise the public API end-to-end.
  • Tests must exercise the actual changed code, not just add random coverage.

Changelog fragments

Every PR with behavior change needs a YAML fragment under changelogs/fragments/. See Development workflow.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Ansible wiki | Factory