Skip to content

Python: Add the missing py.typed markers, and check for them going forward - #8823

Merged
Eduard van Valkenburg (eavanvalkenburg) merged 5 commits into
microsoft:mainfrom
feiiiiii5:fix/add-missing-py-typed
Oct 2, 2026
Merged

Eduard van Valkenburg (eavanvalkenburg) merged 5 commits into
microsoft:mainfrom
feiiiiii5:fix/add-missing-py-typed

Conversation

@feiiiiii5

Copy link
Copy Markdown
Contributor

Motivation & Context

18 packages declare Typing :: Typed but ship no py.typed, so the promise never reaches anyone who installs them. Reported in #8820; eavanvalkenburg confirmed it was an oversight and asked for a PR, plus a check so agents catch it themselves.

PEP 561 keeps that classifier through the marker file inside the package directory. Without it a type checker must treat the whole package as untyped. I checked the published artifact rather than the tree: agent_framework_foundry 1.13.1 on PyPI carries Classifier: Typing :: Typed in its METADATA and no py.typed in the wheel, so from agent_framework.foundry import FoundryChatClient resolved to Any while from agent_framework import Agent — from core, which does ship the marker — resolved to a full signature. Same import style, silently different results. foundry is what the root README.md quickstart imports.

The 18: a2a, anthropic, azure-contentunderstanding, azure-cosmos-memory, bedrock, claude, copilotstudio, declarative, devui, foundry, foundry_hosting, github_copilot, hosting, hosting-responses, hosting-telegram, mem0, purview, redis. The other 23 of the 38 declaring packages already have the marker.

Description & Review Guide

  • What are the major changes? 18 empty py.typed files, one per affected package, plus one checklist item in the Python package management skill. No code, no annotations, no packaging configuration.
  • What is the impact? The affected packages become visible to type checkers for the annotations they already carry, and the skill now makes the two drift apart visible: a new package that declares the classifier without the file fails a check the skill tells you to run.
  • What do you want reviewers to focus on? Whether the marker should land for all 18 at once or in batches, and whether the skill is the right home for the check. I had offered a split in Python: 18 packages declare Typing :: Typed but ship no py.typed #8820 and the answer was to go ahead, so this is all 18 in one commit — happy to split if a batch is easier.

How this was verified. uv build --wheel --package agent-framework-foundry on this branch puts py.typed in the wheel; installing that wheel into a clean venv and running mypy over

from agent_framework.foundry import FoundryChatClient
reveal_type(FoundryChatClient)

now reveals the full __init__ signature — project_endpoint: str | None = ..., model: str | None = ..., credential: ... , allow_preview: bool | None = ..., — where the published 1.13.1 wheel gives Any.

What I have not done. I have not type-checked these 18 packages, so I cannot say whether any has annotations that do not hold up. py.typed only makes annotations that are already there visible; it adds none, and some of these may surface further errors downstream once the marker lands. That is a maintainer's call, and why I asked about batching in #8820.

The skill's check reports nothing on this branch and all 18 on main:

cd python/packages
for d in */; do
  d=${d%/}
  [ -f "$d/pyproject.toml" ] || continue
  if grep -q "Typing :: Typed" "$d/pyproject.toml" && ! ls "$d"/agent_framework*/py.typed >/dev/null 2>&1; then
    echo "missing py.typed: $d"
  fi
done

Related Issue

Fixes #8820

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings September 29, 2026 10:18
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Sep 29, 2026
@github-actions github-actions Bot changed the title Add the missing py.typed markers, and check for them going forward Python: Add the missing py.typed markers, and check for them going forward Sep 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The package markers are complete and correctly located; the remaining documentation precision issue is non-blocking.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds PEP 561 markers to 18 typed Python packages and documents a consistency check for future packages.

Changes:

  • Adds missing py.typed markers.
  • Adds package-management guidance for keeping markers and classifiers aligned.
File Description
python/​.github/​skills/​python-package-management/​SKILL.md Documents marker validation.
python/​packages/​a2a/​agent_framework_a2a/​py.typed Marks A2A as typed.
python/​packages/​anthropic/​agent_framework_anthropic/​py.typed Marks Anthropic as typed.
python/​packages/​azure-contentunderstanding/​agent_framework_azure_contentunderstanding/​py.typed Marks Content Understanding as typed.
python/​packages/​azure-cosmos-memory/​agent_framework_azure_cosmos_memory/​py.typed Marks Cosmos memory as typed.
python/​packages/​bedrock/​agent_framework_bedrock/​py.typed Marks Bedrock as typed.
python/​packages/​claude/​agent_framework_claude/​py.typed Marks Claude as typed.
python/​packages/​copilotstudio/​agent_framework_copilotstudio/​py.typed Marks Copilot Studio as typed.
python/​packages/​declarative/​agent_framework_declarative/​py.typed Marks Declarative as typed.
python/​packages/​devui/​agent_framework_devui/​py.typed Marks DevUI as typed.
python/​packages/​foundry/​agent_framework_foundry/​py.typed Marks Foundry as typed.
python/​packages/​foundry_hosting/​agent_framework_foundry_hosting/​py.typed Marks Foundry hosting as typed.
python/​packages/​github_copilot/​agent_framework_github_copilot/​py.typed Marks GitHub Copilot as typed.
python/​packages/​hosting/​agent_framework_hosting/​py.typed Marks Hosting as typed.
python/​packages/​hosting-responses/​agent_framework_hosting_responses/​py.typed Marks Responses hosting as typed.
python/​packages/​hosting-telegram/​agent_framework_hosting_telegram/​py.typed Marks Telegram hosting as typed.
python/​packages/​mem0/​agent_framework_mem0/​py.typed Marks Mem0 as typed.
python/​packages/​purview/​agent_framework_purview/​py.typed Marks Purview as typed.
python/​packages/​redis/​agent_framework_redis/​py.typed Marks Redis as typed.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/.github/skills/python-package-management/SKILL.md Outdated
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Good catch — reworded in bdb73ee. PEP 561 only makes the marker the signal; what a checker does with an untyped import afterwards is its own business, so the skill now states the portable guarantee (without the marker the package is not treated as typed, so the API falls back to the checker's untyped-import behaviour) instead of promising Any. The marker files themselves are unchanged.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Chen Yufeiyang (@feiiiiii5) thanks for taking a look at this, we do need to fix some typing issues now that did not get flagged before, see the failing check

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Thanks for the pointer — I agree they need fixing, and they are all in test files that nothing type-checked before the markers landed.

Reading the job log, all 18 are under packages/*/tests/: 5 in anthropic/tests/test_anthropic_client.py, 4 in declarative/tests/test_graph_executors.py, 5 across foundry_hosting/tests/, 3 in bedrock/tests/, 1 in redis/tests/. That is what adding the markers does — mypy starts checking the tests against the packages instead of skipping them.

Three of them look different from a plain missing fix:

packages/bedrock/tests/test_bedrock_settings.py:17: error: Module
"agent_framework_bedrock._chat_client" has no attribute "BedrockChatClient"

Both BedrockChatClient and BedrockSettings are defined in agent_framework_bedrock/_chat_client.py on this branch (lines 228 and 218), so mypy is resolving a different copy of the package than the workspace source — the tests reach into a private module, and once the package is typed mypy follows the import instead of treating it as Any. That may want a resolution fix rather than an edit to the test.

I could not reproduce the job locally: uv sync --all-packages --all-extras --all-groups does not finish in the time I have, so I have no mypy 2.3.1 run to work against, and I would rather not push 18 edits I cannot verify.

Two questions so I take the right next step:

  1. Scope — fix them in this PR, or a follow-up that only carries the typing fixes so this one stays reviewable on its own?
  2. If here, change the bedrock _chat_client imports to the public surface, or leave them and work around?

I will pick this back up as soon as a sync completes and run the same ci-test-typing before pushing.

auto-merge was automatically disabled September 29, 2026 23:28

Head branch was pushed to by a user without write access

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Fixed all 18 in d0e88ec. All are in test files, and each is now handled the way the surrounding code already handles it:

Monkeypatching → monkeypatch.setattr (10 errors). declarative and foundry_hosting assigned to a module attribute (base_mod.Engine) or to a method (client.aclose, storage.list_checkpoints), which mypy rejects. monkeypatch.setattr is already the idiom in these suites, so I moved those six patches over and dropped the now-pointless # zuban: ignore comments. Where a test asserted on the mock through the patched object, it now asserts on the mock directly, which also satisfies ty and pyright.

bedrock → import from the package (3 errors). test_bedrock_settings.py and test_bedrock_client.py pulled BedrockChatClient/BedrockSettings out of the private agent_framework_bedrock._chat_client. Both are in that package's __all__, and test_bedrock_client.py was already importing BedrockChatClient from the public path on the line above, so this just makes it consistent. The patch("agent_framework_bedrock._chat_client....") targets in those files are unaffected.

_redis_result → overload pair (1 error). Awaitable[_T] | _T left mypy unable to solve the TypeVar — it inferred Never. Rather than casting at the call site I gave the helper an @overload pair, so inference works and test_providers.py needed no change. The one place I touched library code rather than a test; behaviour-preserving, and the cause rather than a symptom.

Suppression codes (4 errors). The default_options argument in anthropic and foundry_hosting, and four async for loops over _inner_get_response, already carried per-checker suppressions; mypy's code was missing, so I added it alongside. warn_unused_ignores is off and the tests pyright config is basic, so the extra codes are inert elsewhere.

How I verified, since the CI job fans out over many checkers:

  • uv run --python 3.11 python scripts/workspace_poe_tasks.py ci-test-typing — the workflow's exact command — reports the same three core failures before and after my change: Import "agent_hooks" could not be resolved at test_agent_hooks.py:1333. agent_hooks is an external top-level package, not in this repository; CI has it installed and my local env does not, and this PR touches no core file. Environment gap on my side, not a regression, and the only thing still red locally.
  • Each of the five gating checkers, run per package the way the job does, is clean on all five packages: mypy, zuban, pyrefly, ty, pyright.
  • pytest over the five affected packages: 1927 passed, 454 skipped, no failures. uv run poe syntax passes all 82 tasks. ruff format --check and ruff check are clean on all eight files I touched.

One unrelated thing worth flagging: poe syntax reformats packages/typesafe/agent_framework_typesafe/_tool_calls.py, which does not conform to ruff format at main either. I reverted that and kept it out of this PR.

18 packages declare `Typing :: Typed` but ship no py.typed, so the
promise never reaches anyone who installs them: PEP 561 keeps it
through the marker file inside the package directory, and without it a
type checker must treat the whole package as untyped. Verified on the
published wheel, not just the tree - agent_framework_foundry 1.13.1
carried the classifier and not the file, so
`from agent_framework.foundry import FoundryChatClient` resolved to
`Any` while `from agent_framework import Agent`, from a package that
does ship the marker, resolved to a full signature.

flit already ships whatever is in the package directory, so the marker
is not being excluded by configuration - it was simply absent. Adding
the file is the whole change.

The package management skill now checks the two against each other,
since a new package can repeat the same mismatch.
The skill said a package that declares `Typing :: Typed` without
shipping `py.typed` "resolves to Any for every type checker", but
PEP 561 only makes the marker the signal; what a checker then does
with an untyped import is its own business. State the portable
guarantee instead: without the marker the package is not treated as
typed, so its API falls back to the checker's untyped-import
behaviour rather than the inline annotations.
Adding the markers makes mypy check these packages' tests instead of
skipping them, which surfaced 18 errors across five packages. All of them
are in test files, and each is fixed the way the surrounding code already
does it:

- module and method monkeypatching moves to monkeypatch.setattr, the idiom
  used elsewhere in these tests, so mypy no longer sees an assignment to a
  type or a method. The assertions now target the mock directly instead of
  reading it back off the patched object.
- bedrock imports BedrockChatClient and BedrockSettings from the package
  instead of the private _chat_client module; both are in __all__ and the
  same test file already imported BedrockChatClient publicly.
- _redis_result gains an overload pair. Awaitable[_T] | _T left mypy unable
  to solve the TypeVar from a coroutine argument; the overloads let it
  infer, so the test needs no cast.
- the two default_options arguments and four async-for loops get mypy's
  error code added to the per-checker suppressions already on those lines.

Verified with the repo's own five gating checkers (mypy, zuban, pyrefly,
ty, pyright) on Python 3.11 for all five packages, and the affected test
suites pass: 1927 passed, 454 skipped.
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Rebased onto the current main (it had moved 23 commits while this was open). One conflict, in test_bedrock_client.py: upstream added BedrockChatOptions to the public agent_framework_bedrock import, and my change moved BedrockSettings onto that same line. Resolved by taking all three names in one multi-line public import, which is what both sides wanted.

Re-verified after the rebase rather than assuming the resolution was right:

  • all five gating checkers on all five affected packages — mypy, zuban, pyrefly, ty, pyright — 25 runs, all clean
  • pytest over the five packages: 1946 passed, 454 skipped, 8 xpassed, no failures
  • ruff format --check and ruff check clean on the eight files

Now 27 files (the 18 py.typed markers, the skill checklist item, and the eight typing fixes) and mergeable again.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Down to 1 error Chen Yufeiyang (@feiiiiii5) thanks for sticking with it!

pyright reported one error here: `current_count: int` at line 300, where
`llen` is annotated `int | Awaitable[int]`.

The overload pair added to silence the union made it worse. For a union
argument the `Awaitable[_T]` arm matches and nothing consumes the other
half, so `_T` binds to the whole union and the awaited value types as
`Awaitable[int] | int`. Listing the union arm first does not fix it --
that arm then shadows the bare-awaitable one.

A single `Awaitable[_T] | _T` parameter returning `_T` resolves to the
awaited type for all three call sites, verified against the annotations
redis-py actually ships (`llen` `int | Awaitable[int]`, `rpush`
`int | Awaitable[int]`, `ltrim` `bool | Awaitable[bool]`).
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Down to zero, in 12321a8f.

The failure was mine: the overload pair I added in 65a74a1a to handle the sync/async union is what made it unresolvable. For llen's int | Awaitable[int], the Awaitable[_T] arm matches and nothing consumes the other half, so _T binds to the whole union and current_count: int is rejected even though the runtime value is an int.

I reproduced it in isolation before changing anything, and checked the orderings rather than guessing:

overload pair, union arg     x Awaitable[int] | int not assignable to int     <- the CI error
overload pair, bare awaitable  x Awaitable[str] not assignable to str         <- what it was there for
union arm listed first         x Awaitable[str] still fails (it shadows the other arm)
single Awaitable[_T] | _T      0 errors on union, plain and awaitable args

The single signature resolves to the awaited type for all three shapes. I then verified it against the annotations redis-py actually ships rather than my assumption, installing the package to read them:

llen -> int | Awaitable[int]
rpush -> int | Awaitable[int]
ltrim -> bool | Awaitable[bool]

All three call sites pass the union shape, so the one case the single signature does not cover — a bare Awaitable[T] with no | T — is not reachable here. I also corrected the docstring: it claimed newer redis-py narrows those annotations to the awaitable alone, which I could not reproduce (redis 8.1 still declares the union), and it now says what is actually verified.

ruff check and ruff format --check clean on the file. I could not run the package's own pyright gate to completion here — this worktree has no venv, so pyright reports 34 unrelated errors from missing dependencies, identically with and without my change. What I did verify is that the specific :301 reportAssignmentType error no longer appears, and that the isolated reproduction of the helper is clean.

If the CI run says otherwise, the error message will tell us which shape I got wrong.

mypy 2.3.1 over the redis tests (`Test Typing Checks (3.11)`) reports
one error now that the package carries a py.typed marker:

    packages/redis/tests/test_providers.py:631: error: Argument 1 to
    "_redis_result" has incompatible type "Coroutine[Any, Any, int]";
    expected "Awaitable[Never]"

`Awaitable[_T] | _T` resolves for a union argument -- llen, rpush and
ltrim all annotate `T | Awaitable[T]` -- but not for a bare awaitable:
mypy then solves `_T` from the non-awaitable arm alone and the
awaitable arm collapses to `Awaitable[Never]`. That is the one shape the
helper's docstring says it does not cover, and the test was passing it.

Annotating the local with the union redis-py declares keeps the runtime
path identical -- the value is still an un-awaited coroutine -- and
matches what the three call sites in `_history_provider.py` pass.
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Fixed in 07d405d. The last red was mine, in the test rather than the source:

packages/redis/tests/test_providers.py:631: error: Argument 1 to
"_redis_result" has incompatible type "Coroutine[Any, Any, int]"; expected "Awaitable[Never]"

Awaitable[_T] | _T resolves for a union argument — llen, rpush and ltrim all annotate T | Awaitable[T] — but not for a bare awaitable: mypy then solves _T from the non-awaitable arm alone and the awaitable arm collapses to Awaitable[Never]. That is the one shape the helper's docstring says it does not cover, and test_awaits_an_awaitable_result was passing exactly that shape.

The commit annotates the local as int | Awaitable[int], which is what redis-py declares and what the three call sites in _history_provider.py pass. The runtime path is unchanged — the value is still an un-awaited coroutine — and the file has from __future__ import annotations, so the annotation is not evaluated.

Verified with the repo's own configuration (mypy 2.3.1, --config-file pyproject.toml --explicit-package-bases --namespace-packages, over packages/redis/tests, with the package resolved through MYPYPATH so it is analysed the way the new py.typed makes CI analyse it): that error is present on 12321a8 and gone on 07d405d. Not run: the redis package's pytest suite needs the full workspace install, and this change only adds a local annotation.

If you would rather the helper accepted bare awaitables too, Awaitable[Any] | _T measures clean under the same mypy for all five shapes I tried — union int, union bool, bare coroutine, an Awaitable[int] local, and a plain int. The cost is that a bare awaitable then types as Never instead of being rejected. Say the word and I will switch to that instead.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 2, 2026
Merged via the queue into microsoft:main with commit e9cb7fe Oct 2, 2026
49 of 51 checks passed

This branch was previously deployed

1 inactive deployment
github-app-auth — 07d405d2 Deployed Oct 1, 2026 by feiiiiii5 via add_label #24165
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: 18 packages declare Typing :: Typed but ship no py.typed

3 participants