Context
While fixing pypa/setuptools-scm#1434, we wanted to change a deprecated parameter's default from a module-level constant to None, with the function resolving None to the same constant internally. This is a common pattern for deprecation: stop eagerly providing the default so you can distinguish "caller didn't pass this" from "caller explicitly passed the old default".
Reproducer
# src/mylib/__init__.py — v1.0.0 (before)
import re
from re import Pattern
DEFAULT_PATTERN: Pattern[str] = re.compile(r"v(?P<version>.+)")
def get_version(
root: str = ".",
tag_regex: str | Pattern[str] = DEFAULT_PATTERN,
) -> str:
"""Get the version."""
return "1.0.0"
# src/mylib/__init__.py — v1.1.0 (after)
import re
from re import Pattern
DEFAULT_PATTERN: Pattern[str] = re.compile(r"v(?P<version>.+)")
def get_version(
root: str = ".",
tag_regex: str | Pattern[str] | None = None,
) -> str:
"""Get the version."""
if tag_regex is None:
tag_regex = DEFAULT_PATTERN
return "1.0.0"
$ griffe check mylib -ssrc --against v1.0.0
src/mylib/__init__.py:9: get_version(tag_regex):
Parameter default was changed:
Old: DEFAULT_PATTERN
New: None
Question
This is flagged as a breaking change, but from a caller's perspective:
get_version() (no args) behaves identically before and after
get_version(tag_regex="custom") also behaves identically
- The only observable difference is via introspection (
inspect.signature), where the default representation changes
Is this level of strictness intentional? The "default changed" check makes sense for cases where the actual behavior changes (e.g. timeout=30 → timeout=60), but the CONSTANT → None pattern is specifically used to preserve behavior while enabling the function to detect whether the caller explicitly provided a value.
This relates to #399 (suppression configuration) — in the meantime, we worked around this by keeping the original default and using tag_regex is DEFAULT_PATTERN identity checks instead. But the None-default pattern is idiomatic Python for deprecations and optional parameters, so it'd be nice to understand if griffe considers this a genuine API break or if there's room for a more nuanced check here.
Context
While fixing pypa/setuptools-scm#1434, we wanted to change a deprecated parameter's default from a module-level constant to
None, with the function resolvingNoneto the same constant internally. This is a common pattern for deprecation: stop eagerly providing the default so you can distinguish "caller didn't pass this" from "caller explicitly passed the old default".Reproducer
Question
This is flagged as a breaking change, but from a caller's perspective:
get_version()(no args) behaves identically before and afterget_version(tag_regex="custom")also behaves identicallyinspect.signature), where the default representation changesIs this level of strictness intentional? The "default changed" check makes sense for cases where the actual behavior changes (e.g.
timeout=30→timeout=60), but theCONSTANT→Nonepattern is specifically used to preserve behavior while enabling the function to detect whether the caller explicitly provided a value.This relates to #399 (suppression configuration) — in the meantime, we worked around this by keeping the original default and using
tag_regex is DEFAULT_PATTERNidentity checks instead. But theNone-default pattern is idiomatic Python for deprecations and optional parameters, so it'd be nice to understand if griffe considers this a genuine API break or if there's room for a more nuanced check here.