Skip to content

fix(tracing): widen Span.set_tag value annotation to match implementation - #19814

Open
ruidc wants to merge 1 commit into
DataDog:mainfrom
getsortd-co:widen-set-tag-value-typing
Open

fix(tracing): widen Span.set_tag value annotation to match implementation#19814
ruidc wants to merge 1 commit into
DataDog:mainfrom
getsortd-co:widen-set-tag-value-typing

Conversation

@ruidc

@ruidc ruidc commented Aug 22, 2026

Copy link
Copy Markdown

Fixes #19815

Description

Span.set_tag is annotated as accepting value: Optional[str], but the implementation accepts and
handles the historic domain — booleans and bytes are stringified into tags, and int/float values
are stored as metrics. This PR widens the annotation back to value: Any = None (the annotation it
had before v4.0), documents the value handling in the docstring, and deletes the five internal
# type: ignore comments that existed only to silence the consequences of the too-narrow parameter.

No runtime behaviour changes — the diff touches only the annotation, the docstring, comments, and a
release note.

Motivation

The Optional[str] annotation makes type checkers reject valid application code:

span.set_tag("custom.handled_exception", True)

is correct at runtime — the body stringifies bools itself (the isinstance(value, (bool, bytes))
branch), and the emitted tag is byte-identical to passing "True" — yet mypy reports:

error: Argument 2 to "set_tag" of "Span" has incompatible type "bool"; expected "str | None"  [arg-type]

The implementation is the evidence that the annotation is too narrow. Five of its own
# type: ignore comments exist purely because the parameter type doesn't cover what the body does
with it: int(value) for net.TARGET_PORT, value = 1 / set_metric(key, value) for
_SPAN_MEASURED_KEY, and the two _set_attribute calls. All five become dead with the widened
annotation and are removed here (the repository's warn_unused_ignores = true enforces their
removal).

The library's own test suite also pins the wider runtime contract:

  • tests/tracer/test_span_tags.py::test_tags and ::test_numeric_tags — ints and floats passed to
    set_tag are stored as metrics.
  • tests/tracer/test_span_tags.py::test_set_tag_boolset_tag("true", True) produces the tag
    "True".

Relationship to the v4.0 typing change

I'm aware the narrowing was not accidental: it shipped in the v4.0 major-version PR (#14938,
commit 89d69bdfe) together with an explicit upgrade release note
(releasenotes/notes/explicit-span-tag-typing-99abb4d3ec065a55.yaml):

tracing: Span.set_tag typing is now set_tag(key: str, value: Optional[str] = None) -> None

This PR argues that annotation and implementation should agree: v4.0 narrowed the signature but
deliberately kept the lenient runtime handling (and the tests above that pin it), so the annotation
now rejects code the library explicitly supports. There is also direct precedent for resolving this
in favour of the implementation: #7182 (fixing #7175) made the same correction for Span.set_tags
in 2023 — "type-checking would fail on valid application code due to a type hint … that was too
restrictive" — and chose Any. If the v4.0 intent is instead that non-string values become
unsupported, the five ignores and the bool/bytes/int handling would be the code to remove — a
behavioural break this PR deliberately does not make.

Considered alternative: a precise union

Optional[Union[str, bool, bytes, int, float]] was tried first, under the repository's own
toolchain (the pinned mypy 1.15.0 with mypy.ini, which sets warn_unused_ignores = true). It is
strictly worse than Any:

  • it makes only one of the five ignores removable (value = 1), and
  • it introduces a new [assignment] error at self.service = valueservice is
    Optional[str] in ddtrace/internal/native/_native.pyi — which would require a new
    suppression.

value: Any matches the pre-4.0 annotation and the #7182 precedent, removes all five suppressions,
and adds none.

Testing

  • Runtime behaviour is unchanged and already covered by the existing tests listed above
    (tests/tracer/test_span_tags.py).
  • mypy --config-file mypy.ini (mypy 1.15.0, the pinned lint version): the reported error set is
    identical before and after the change, and ddtrace/_trace/span.py reports zero diagnostics —
    including zero unused-ignore warnings after deleting the five comments.
  • ruff check / ruff format --check (ruff 0.14.10, the pinned version) pass on the touched file.

Risks

None at runtime — no executable code changes. The typing change is a widening, so no currently
type-checking caller breaks; code that was previously (incorrectly) rejected now checks.

Additional Notes

A release note is included
(releasenotes/notes/widen-set-tag-value-typing-40a41e0a8804ec18.yaml) since this changes the
public API's typing surface. Span.set_tags remains dict[str, str]; widening it to match (as
#7182 once did) would be a natural follow-up if this direction is accepted, but is deliberately out
of scope here.

…tion

The value parameter is annotated Optional[str] but the implementation
accepts the historic domain: bools and bytes are stringified into tags,
int/float values are stored as metrics. Widening to Any (the pre-4.0
annotation) lets five internal type-ignore comments be deleted. No
runtime behaviour changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ruidc
ruidc requested review from a team as code owners August 22, 2026 08:02
@ruidc
ruidc requested review from juanjux and mabdinur August 22, 2026 08:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef88c14c2d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/_trace/span.py
"""Set a tag key/value pair on the span.

Boolean and bytes values are stored as their string representation.
``int`` and ``float`` values are stored as metrics (see ``set_metric``).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Qualify the numeric-to-metric guarantee

For integers outside the signed 64-bit range or non-finite floats, this new public documentation is incorrect: extract_attribute in src/native/span/span_data.rs stringifies overflowing integers and drops NaN/Infinity rather than storing them as metrics. Callers relying on this documented guarantee will therefore find a string tag or no value at all, so the sentence should state the supported numeric range and finite-value restriction. This docstring is customer-facing because Span is exposed through ddtrace.trace.

AGENTS.md reference: AGENTS.md:L25-L30

Useful? React with 👍 / 👎.

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.

[BUG]: Span.set_tag value annotation (Optional[str]) is narrower than the implementation

1 participant