Skip to content

question: should changing a parameter default from a constant to None be flagged as breaking? #469

Description

@RonnyPfannschmidt

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=30timeout=60), but the CONSTANTNone 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions