This document is the canonical source for Python coding standards in Agent Framework.
It applies to package source, tests, samples, and Python-specific repository tooling under python/.
Exact formatter, linter, and type-checker settings live in the relevant configuration files. This document explains the engineering intent behind those settings and defines the rules that require design judgment.
- MUST and MUST NOT define requirements.
- SHOULD and SHOULD NOT define the expected default. Deviations require a concrete reason.
- MAY identifies an acceptable option, not a preference.
New code and materially edited code MUST follow these standards. Existing code may not yet conform everywhere; do not copy a non-conforming pattern merely because it already exists.
When principles compete, use this order:
- Correctness and public API clarity
- Maintainability and readability
- Measured performance
- Typing purity
- Code MUST preserve public compatibility unless a breaking change has been explicitly approved and documented. Do not invent compatibility aliases or shims without that decision.
- Code SHOULD use the most explicit, unsurprising implementation with the clearest data flow, even when it is slightly more verbose.
- Inputs from users, external services, files, environment variables, and other trust boundaries MUST be validated. Inside trusted typed code, rely on documented invariants and fail at the point where an invariant is broken.
- Mutation MUST be intentional and visible in the API. Accept read-only abstractions when mutation is unnecessary, and do not make defensive copies without a concrete ownership or correctness need.
- A new abstraction SHOULD centralize meaningful policy, remove real duplication, or create a stable boundary. Do not add speculative abstractions or wrappers whose only purpose is satisfying a type checker.
Ruff is the formatter and linter. The current configuration uses a 120-character line length and targets Python 3.10
or later. pyproject.toml and the other tool configuration files are the source of truth for exact values and enabled
rules.
-
Every checked-in
.pyfile MUST start with:# Copyright (c) Microsoft. All rights reserved. -
Code MUST NOT shadow Python built-ins. A compatibility exception requires an explicit API decision.
-
Package source files that are new or materially edited MUST use:
from __future__ import annotations
-
TypeVarnames MUST use a descriptiveTsuffix.# Preferred ChatResponseT = TypeVar("ChatResponseT", bound=ChatResponse) # Avoid TChatResponse = TypeVar("TChatResponse", bound=ChatResponse)
Purely mechanical findings that the required formatter, linter, or type checkers already report should be fixed through those tools rather than duplicated as subjective review guidance.
Typing is a design aid, not an end in itself.
- Public APIs MUST have complete, stable parameter and return annotations.
- Functions and methods in package source MUST annotate every parameter and return value. A trivially inferred local callable, such as a short lambda or nested callback, MAY rely on inference.
- Tests and samples follow their configured relaxed checker profiles; complete annotations are not universally required there. Add annotations when they clarify reusable helpers, fixtures, callbacks, or copied public usage.
- Package source MUST pass the configured strict Pyright checks.
- Tests MUST pass the repository-configured Pyright, mypy, pyrefly, ty, and zuban profiles.
- Samples MUST pass the configured supported subset of those checkers.
- The exact checker responsibilities and commands live in
python-code-quality.
Input parameters that do not mutate a collection SHOULD use the narrowest practical read-only abstraction, such as
Mapping, Sequence, or Collection, instead of requiring a mutable concrete type.
# Preferred
def process_config(config: Mapping[str, Any]) -> None:
...
# Avoid when mutation is not required
def process_config(config: MutableMapping[str, Any]) -> None:
...Resolve type-checker failures in this order:
- Improve the annotations, protocol, overload, or design.
- Use a narrow, rule-specific suppression when the checker cannot represent a valid runtime contract.
- Use
cast()only when it materially improves clarity. - Add a runtime check only when runtime validation is semantically required.
Additional rules:
- Source code SHOULD use a line-level, rule-specific
# pyright: ignore[...]when a suppression is unavoidable. - Internal implementation code MAY use a rule-specific
# type: ignore[...]when it is clearer than checker-specific suppressions. The ignore MUST include a brief inline reason explaining the valid runtime contract or checker limitation. - Tests and samples MUST use only the minimum checker-specific suppressions required by their configured profiles. A line MAY need both a Pyright suppression and a mypy-compatible suppression when the checkers report independent findings.
- File-level suppressions MUST be limited to generated code or a documented file-wide checker limitation that cannot be addressed locally.
- An Agent Framework package MAY intentionally use another Agent Framework package's private helper. Keep the helper
private and use a targeted
reportPrivateUsagesuppression rather than making it public solely for the checker. - Private members of third-party dependencies SHOULD NOT be used unless the integration has a documented, unavoidable reason.
- Code MUST NOT add trivial pass-through helpers or extra assignments solely to satisfy typing.
Python APIs should keep the common path simple without removing advanced extensibility.
- A common operation SHOULD require one primary Agent Framework import for that concept.
- Simple configuration SHOULD use keyword-only parameters, plain Python values, or typed structural data rather than forcing callers to construct several framework-owned data-holder classes.
- Additional framework imports MAY be required for advanced behavior.
- A public configuration class SHOULD exist only when it owns meaningful validation or behavior, is independently reusable, or represents a durable domain concept. Do not create a class merely to group arguments for one call.
- Choose between keyword-only parameters and a
TypedDictcase by case. Prefer keyword-only parameters for direct discoverability. Prefer aTypedDictwhen the values form a cohesive payload that is passed, stored, or forwarded as data. There is no field-count cutoff.
ChatOptions is the preferred model for extensible structured options: common fields are typed without requiring
callers to instantiate a configuration object, provider-specific types can extend the structure, and generic APIs can
carry a caller- or provider-defined TypedDict without waiting for Agent Framework to add another public class.
- An API that expects users or providers to extend structured options SHOULD be generic over their
TypedDictrather than requiring configuration-class inheritance.
- Positional parameters SHOULD be obvious, stable, and difficult to misorder. There is no fixed numeric limit.
- Optional or evolving parameters SHOULD be keyword-only.
- Public APIs SHOULD accept a string alternative to an enum or framework type when that makes the common path simpler and the string vocabulary is stable.
- String alternatives MUST use
Literal[...]to enumerate supported values and MUST normalize to the canonical type at the API boundary.
def create_agent(
name: str,
*,
tool_mode: Literal["auto", "required", "none"] | ChatToolMode = "auto",
) -> Agent:
if isinstance(tool_mode, str):
tool_mode = ChatToolMode(tool_mode)
...**kwargs is an escape route, not the default shape for known data.
- Known framework data MUST use explicit parameters.
**kwargsMAY be used for documented subclass extensibility.**kwargsMAY be used for a narrow passthrough to a large or unstable external SDK surface.- A flexible payload that is part of the API contract SHOULD use a purpose-named mapping such as
additional_properties,client_kwargs, orfunction_invocation_kwargs. - The API MUST document who owns the mapping, where it is forwarded, and whether values are copied or mutated.
- Wrappers MUST NOT accept or forward generic keyword arguments they do not use.
- Behavioral contracts SHOULD use
Protocol. - An abstract base class SHOULD be used only when inheritance provides shared implementation, lifecycle, construction control, or other required runtime semantics.
- Composition and inheritance are both acceptable. Choose based on substitutability, lifecycle, and clarity rather than a blanket preference.
- Use asynchronous APIs for I/O and concurrency. Keep purely synchronous work synchronous.
- Async code MUST NOT block the event loop with synchronous network, file, process, or long-running CPU work.
- Cancellation MUST propagate. Cleanup belongs in
finallyblocks or context managers; cancellation MUST NOT be translated into ordinary success or an unrelated framework exception. - Resource ownership MUST be explicit:
- Close or cancel resources the object creates.
- Do not close caller-injected resources.
- Use synchronous or asynchronous context managers when lifecycle management is part of correct use.
The authoritative classes live in
packages/core/agent_framework/exceptions.py. This document defines
how to choose and use them rather than duplicating a hierarchy that can drift.
- Broken internal invariants SHOULD fail immediately at the point of violation.
- Invalid Python arguments, configuration values, or object state SHOULD use built-in exceptions such as
TypeError,ValueError, orRuntimeError. - Domain failures that callers may handle distinctly MUST use the Agent Framework exception branch owned by that domain, such as Agent, ChatClient, Integration, Workflow, Tool, or Middleware.
- Exception selection MUST follow domain ownership rather than HTTP status code or a generic category alone.
- Translated exceptions MUST preserve their cause with
raise ... from exc. - Code MUST NOT silently swallow errors, return a success-shaped fallback after failure, or use a broad catch without an explicit recovery boundary.
- New exception names SHOULD follow the established naming convention of their domain. Do not force one universal
ErrororExceptionsuffix across all branches. - Renames and removals MUST follow the general public compatibility policy. Do not assume either a clean break or a compatibility alias without an approved API decision.
Use the following selection guide:
| Scenario | Exception family |
|---|---|
| Invalid argument or configuration | Built-in TypeError or ValueError |
| Object in an invalid internal state | Built-in RuntimeError |
| Agent lookup or response handling failure | AgentException branch |
| Chat-provider request, response, auth, or filtering failure | ChatClientException branch |
| Non-chat external dependency failure | IntegrationException branch |
| Workflow execution or validation failure | WorkflowException branch |
| Tool execution failure | ToolException branch |
Connector constructors MUST use load_settings to resolve explicit keyword arguments, an explicitly selected .env
file, process environment variables, and defaults before deciding that a required value is missing.
When a preconfigured SDK client can be injected:
- The client and individual settings MAY both be omitted at the constructor boundary.
- Settings MUST become required only on the branch that constructs the SDK client.
- The constructor MUST NOT reject an injected client merely because optional credentials or model settings were also provided. The injected client owns its transport and authentication.
settings = load_settings(
ProviderSettings,
env_prefix="PROVIDER_",
required_fields=[] if client is not None else ["api_key"],
api_key=api_key,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
if client is None:
client = ProviderClient(
api_key=cast(SecretString, settings.get("api_key")).get_secret_value(),
)Connector implementations SHOULD use these names when they accurately describe the operation:
_prepare_<object>_for_<purpose>for data prepared for an external service._parse_<object>_from_<source>for data received from an external service.
These are consistency patterns, not mandatory templates. Use a clearer verb when the operation has different semantics.
Consumer-facing documentation and samples MUST use stable public imports:
from agent_framework import Agent, Message, tool
from agent_framework.observability import configure_otel_providers
from agent_framework.openai import OpenAIChatClientPrivate defining modules remain implementation details.
- Wildcard imports are prohibited in production
.pyand.pyifiles. Generated code MAY be explicitly exempted. - Package and public import surfaces MUST define an explicit
__all__. - Internal modules SHOULD omit
__all__unless the module intentionally defines a supported public import surface. - Runtime
.pyre-exports SHOULD use direct imports without identity aliases. .pyistubs SHOULD use identity aliases for re-exported names so type checkers recognize the public surface.
When adding, removing, or moving a root agent_framework export, the change MUST:
- Update
_LAZY_MODULE_EXPORTSinpackages/core/agent_framework/__init__.py. - Keep the derived lazy export registry and explicit runtime
__all__synchronized. - Update
packages/core/agent_framework/__init__.pyi. - Keep deprecation behavior in the owning module rather than adding symbol-specific branches to the root
__getattr__. - Validate direct import and wildcard import behavior.
Package-governance decisions and detailed validation commands live in
python-package-management.
- Every public module, including public
__init__.pymodules, MUST have a concise module docstring focused on its purpose. - Namespace modules SHOULD NOT maintain long package or supported-symbol inventories in their docstrings.
- For a module with a docstring, the order MUST be:
- Copyright comment
- Optional top-level metadata comments, such as a PEP 723 block
- Module docstring
from __future__ import annotations, when applicable- Imports
Public APIs MUST use Google-style docstrings.
- The summary MUST use imperative mood and end with a period.
- Docstrings MUST explain behavior, constraints, meaningful errors, and non-obvious semantics rather than restating the signature.
- Positional and ordinary parameters belong under
Args:. - Keyword-only parameters belong under
Keyword Args:. Returns:orYields:SHOULD be present when the value has meaningful semantics beyond its annotation. Omit it forNoneand obvious trivial values.Raises:MUST document intentionally raised Agent Framework exceptions.Raises:SHOULD document built-in exceptions when callers can meaningfully act on a non-obvious condition.- Examples SHOULD use an
Examples:section with.. code-block:: python. - Internal docstrings and comments SHOULD explain important intent, invariants, or tradeoffs. Do not narrate obvious code.
def equal(arg1: str, arg2: str, *, case_sensitive: bool = True) -> bool:
"""Compare two strings.
Args:
arg1: The first string to compare.
arg2: The second string to compare.
Keyword Args:
case_sensitive: Whether comparison preserves letter case.
Returns:
Whether the strings are equal under the selected comparison mode.
"""
...- Log actionable, structured context at the boundary that owns the failure or decision.
- Secrets, credentials, tokens, and sensitive user content MUST NOT be logged or emitted to telemetry by default.
- Exception logging SHOULD preserve the original exception and stack context.
- Logs SHOULD help an operator decide what failed and where; avoid duplicate logging at every layer of the same call.
- Code MUST avoid obvious repeated expensive work and redundant conversions.
- A less-readable micro-optimization SHOULD require measurement or a clearly established hot path.
- Caching SHOULD be introduced only for repeated expensive work with stable inputs and clear lifetime or invalidation semantics.
- Cached mutable values MUST have explicit ownership semantics so callers cannot corrupt shared cached state.
- Choose
isinstance(), a discriminator such as.type, or structural matching based on semantics. Do not replace one with another solely because of a blanket performance claim.
When serializing:
- Compute a representation once and reuse it when several consumers need the same value.
- Use a known model's own
to_dict()or equivalent when its exact type is known. - Use the framework's optimized
make_json_safe()converter for heterogeneous values containing framework models, Pydantic models, dataclasses, containers, or primitives. - Keep provider-specific conversion local when an external API requires exact aliases, JSON modes, or opaque JSON strings.
- Avoid
json.dumps()followed byjson.loads()unless crossing a required wire-format boundary.
Tests are behavior-first. Coverage is a backstop, not the objective.
- Tests MUST validate the public or observable contract rather than mirror implementation details.
- Meaningful new branches, error paths, and edge cases MUST be covered.
- Mocks SHOULD replace external boundaries, not the internal units whose behavior the test is meant to prove.
- Test names SHOULD describe the behavior and condition under test.
- Test directories MUST NOT contain
__init__.py. - Files beginning with
test_MUST contain tests; helper modules MUST use another name. - Non-core packages MUST place tests under a uniquely named
tests/<package>/directory. - Core MAY use
packages/core/tests/directly with topic subdirectories. - Keep the package-level
tests/directory even when tests live in a nested package-specific directory.
- Shared fixtures MUST live in
conftest.py. - Parameterized reusable setup SHOULD use a fixture that returns a callable factory.
- Fixtures MUST be consumed through pytest injection, not imported from
conftest.py. - Non-fixture helpers MUST live in a normal helper module and be imported explicitly.
- Fixture names SHOULD describe the provided value or capability, such as
client,test_request, orconfigured_agent.
Every external-service integration test MUST have:
@pytest.mark.integration- A provider-specific skip condition for missing credentials, endpoints, or required infrastructure
Add @pytest.mark.flaky only when the test is genuinely nondeterministic or service-sensitive.
Task-specific commands, CI workflow synchronization, and integration-job placement live in
python-testing.