Skip to content

docs: clarify local variable annotations#583

Merged
zhongkechen merged 2 commits into
mainfrom
docs/revise-local-variable-typing
Jul 24, 2026
Merged

docs: clarify local variable annotations#583
zhongkechen merged 2 commits into
mainfrom
docs/revise-local-variable-typing

Conversation

@zhongkechen

@zhongkechen zhongkechen commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR revises the contribution guideline that currently requires an explicit
type annotation for every variable, including local variables whose types are
already directly and precisely inferred.

The updated guidance:

  • requires annotated function and method signatures so mypy checks their bodies;
  • relies on local type inference when it is precise and evident;
  • retains explicit local annotations when inference is unavailable, produces
    Any, is too narrow, or does not communicate an intentional contract; and
  • provides examples and links to the corresponding mypy and Google Python Style
    Guide documentation.

Rationale

The existing review guidance is consistent with the repository's current
CONTRIBUTING.md, so this change is about the rule itself rather than any
reviewer or contributor. The issue surfaced in the review discussion on
#582.

Requiring an explicit annotation for every local variable, including variables
whose types are directly and precisely inferred, is contrary to normal Python
typing practice and adds verbosity without necessarily improving type safety.

Mypy explicitly documents local type inference:

For most variables, if you do not explicitly specify its type, mypy will
infer the correct type based on what is initially assigned to the variable.

Source:
Mypy: Type inference and type annotations

For example, in a checked function:

msg = "Invalid input data"

Mypy infers msg as str. It reports errors if msg is subsequently assigned
an incompatible value or used in an operation that does not accept str.
Writing:

msg: str = "Invalid input data"

normally gives the checker no additional information. An explicit annotation
can serve as an intentional constraint on future changes to the initializer,
but that is useful selectively; it does not justify requiring an annotation on
every local.

The Google Python Style Guide, which this repository's contributing guide says
it generally follows, gives more selective guidance:

If an internal variable has a type that is hard or impossible to infer,
specify its type with an annotated assignment.

Source:
Google Python Style Guide: Typing Variables

That guidance treats local annotations as useful when inference is
insufficient, not as mandatory duplication when the inferred type is already
precise.

The variables discussed in #582 illustrate the distinction:

msg = "Invalid input data"
result = context.parallel([branch])
inner_error = StepError(...)

msg is inferred directly as str. result should be inferred from the
annotated return type of context.parallel(). inner_error is inferred
directly from the StepError constructor.

There is a separate consideration for functions without annotated signatures.
By default, mypy does not fully check the bodies of untyped functions. If the
goal is comprehensive checking, the more effective solution is to annotate the
function:

def test_child_handler_preserves_data_across_nested_boundaries() -> None:

Alternatively, a project can enable mypy's check_untyped_defs setting.
Annotating one local inside an otherwise unchecked function gives much narrower
coverage than making the function itself checked.

More generally, blanket local annotations have several costs:

  • They duplicate information already produced by the type checker.
  • They add visual noise and make meaningful annotations harder to identify.
  • They increase maintenance burden during routine refactoring.
  • They can unnecessarily constrain a variable to a concrete implementation
    type when inference or an intentionally selected abstraction would be more
    appropriate.
  • They encourage measuring typing quality by annotation density rather than by
    whether interfaces and checked code paths are correctly typed.

Explicit local annotations remain valuable for cases such as empty
collections, initial None values, values originating from Any or untyped
libraries, intentionally widened interface types, unions across branches, and
expressions whose inferred type is unclear or too narrow. The revised guideline
retains and documents these cases.

AI-assisted development

Explicit type annotations can help AI tools understand code when they add
information that is not evident from the implementation. They are particularly
useful at API boundaries and for ambiguous values, unions, callbacks, values
originating from Any, empty collections, and intentionally broader interface
types.

For obvious locals such as:

msg: str = "Invalid input data"
error: StepError = StepError(...)

the annotation repeats information already present in the initializer. Modern
AI models can infer these straightforward types, just as mypy does. Requiring
this repetition for every local also consumes context, adds visual noise, and
can mislead both developers and AI tools if an annotation becomes stale or
diverges from the implementation.

For AI comprehension, descriptive names, typed function signatures, precise
domain models, and documented invariants provide substantially more useful
semantic information. There is no established evidence that annotating every
obvious local materially improves AI understanding, so AI assistance is not a
strong basis for a blanket requirement. The inference-first guideline preserves
annotations where they provide meaningful signal without treating annotation
density as a proxy for code clarity.

This is also consistent with Python's gradual-typing philosophy. PEP 484
states:

Python will remain a dynamically typed language, and the authors have no
desire to ever make type hints mandatory, even by convention.

Source: PEP 484: Non-goals

That statement does not prevent a repository from adopting a stricter house
style, but it does show that annotating every variable is not a general Python
typing principle.

Validation

  • git diff --check
  • hatch run python .github/scripts/lintcommit.py

Comment thread CONTRIBUTING.md Outdated
yaythomas
yaythomas previously approved these changes Jul 24, 2026
@zhongkechen
zhongkechen merged commit 822c6a5 into main Jul 24, 2026
12 checks passed
@zhongkechen
zhongkechen deleted the docs/revise-local-variable-typing branch July 24, 2026 16:27
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.

2 participants