Skip to content

Annotate the public declaration API - #1162

Open
Culaii wants to merge 1 commit into
FactoryBoy:masterfrom
Culaii:annotate-public-api
Open

Culaii wants to merge 1 commit into
FactoryBoy:masterfrom
Culaii:annotate-public-api

Conversation

@Culaii

@Culaii Culaii commented Oct 24, 2026

Copy link
Copy Markdown

Refs #1113, #1131. Complements #1114 (__all__), does not depend on it.

Problem

Since 3.3.3 ships py.typed, a project running mypy --strict (disallow_untyped_calls)
gets one error per declaration it writes, because the declarations' constructors are
unannotated:

error: Call to untyped function "SubFactory" in typed context  [no-untyped-call]
error: Untyped decorator makes function "with_tags" untyped  [untyped-decorator]

(the first one was reported in #1113). On a real Django project with ~30 factory modules
this is 138 errors, all of them about factory_boy rather than about the project; the usual
workaround is disallow_untyped_calls = false for the whole package.

Change

Annotate what users call:

  • the constructors of every declaration exported by factory (Sequence, SubFactory,
    LazyAttribute, Trait, Maybe, RelatedFactory, PostGeneration, ...),
    factory.Faker and factory.django.Password;
  • factory.helpers: build/create/... and the decorators (post_generation,
    lazy_attribute, sequence, ...);
  • the public classmethods of BaseFactory (stub, generate, simple_generate,
    reset_sequence, ...);
  • factory.django.mute_signals, so that a decorated function keeps its signature.

What users override (evaluate, call, _create, _build, _adjust_kwargs, ...) is left
untouched on purpose: annotating it would make mypy check user overrides for compatibility.

Why this should not break anyone (cf. #1113)

  • The type of a declaration expression does not change: mypy already inferred
    factory.Sequence(...) as Sequence; only the arguments are now checked.
  • mypy does not check __init__ overrides for compatibility, so subclasses of declarations
    are not affected.
  • tests/test_typing_unannotated.py type-checks the documented, unannotated style
    (@factory.sequence def username(n): ... on a factory). This is why the sequence
    decorator accepts Callable[[Any], Any]: on an unannotated method mypy types n as the
    factory class, and Callable[[int], Any] would have reported a new error there.
  • No runtime change; the suite passes unchanged.

Tests

  • make test now also runs
    mypy --strict --implicit-reexport --follow-imports=silent tests/test_typing.py tests/test_typing_unannotated.py,
    i.e. sees factory the way a strict downstream project does. --implicit-reexport can be
    dropped once Explicitly export values on factory namespace #1114 lands.
  • tests/test_typing.py builds a factory using every annotated declaration (also checked at
    runtime), and contains wrong calls marked # type: ignore[...]: under --strict,
    warn_unused_ignores fails if an annotation stops rejecting them.
  • On master's factory/, the strict run reports 27 errors on these tests; with this change, 0.

Out of scope (happy to follow up)

🤖 Generated with Claude Code

Under `mypy --strict` (`disallow_untyped_calls`), every call to an
unannotated function is an error in the *caller*. Since 3.3.3 ships
`py.typed`, a strict project gets one error per declaration it writes:
`factory.Sequence(...)`, `factory.SubFactory(...)`, `factory.Trait(...)`,
and so on, plus `untyped-decorator` on `@factory.post_generation`,
`@factory.lazy_attribute` and friends.

Annotate what users *call*:
- the constructors of every declaration re-exported by `factory`
  (and `factory.django.Password`, `factory.Faker`);
- the helper functions and decorators of `factory.helpers`;
- the public classmethods of `BaseFactory` (`stub`, `generate`,
  `simple_generate`, `reset_sequence`, ...);
- `factory.django.mute_signals`, so that decorating a function keeps its
  signature.

Deliberately left out: the `_`-prefixed extension points and
`evaluate()`/`call()`, which users *override*; annotating them would make
mypy check user overrides for compatibility, i.e. new errors downstream.
Constructors are exempt from that check, which is why this change cannot
produce an override error in user code.

The type of a declaration expression does not change: mypy already
inferred `factory.Sequence(...)` as `Sequence` before this change; the
only new information is the argument types.

`tests/test_typing.py` is now also checked with `--strict
--follow-imports=silent`, i.e. the way a downstream project sees factory;
`type: ignore` comments on wrong calls act as negative tests through
`warn_unused_ignores`. `--implicit-reexport` keeps that check independent
from the `__all__` discussion (FactoryBoy#1114).

`tests/test_typing_unannotated.py` holds a factory written as the docs
show it, without annotations, and is checked by both mypy runs: the new
annotations must not turn documented code into errors (FactoryBoy#1113). It is why
`@factory.sequence` accepts `Callable[[Any], Any]`: on an unannotated
method mypy types `n` as the factory class, so `Callable[[int], Any]`
rejected `def username(n): ...`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant